1use crate::lex::lex;
2use crate::types::{
3 ComponentType, Compression, GitExport, GitMode, Mode, PgpMode, Pretty, SearchMode,
4};
5use crate::SyntaxKind;
6use crate::SyntaxKind::*;
7use crate::DEFAULT_VERSION;
8use std::io::Read;
9use std::marker::PhantomData;
10use std::str::FromStr;
11
12#[cfg(test)]
13use crate::types::VersionPolicy;
14
15pub(crate) fn watch_option_to_key(option: &crate::types::WatchOption) -> &'static str {
17 use crate::types::WatchOption;
18
19 match option {
20 WatchOption::Component(_) => "component",
21 WatchOption::Compression(_) => "compression",
22 WatchOption::UserAgent(_) => "user-agent",
23 WatchOption::Pagemangle(_) => "pagemangle",
24 WatchOption::Uversionmangle(_) => "uversionmangle",
25 WatchOption::Dversionmangle(_) => "dversionmangle",
26 WatchOption::Dirversionmangle(_) => "dirversionmangle",
27 WatchOption::Oversionmangle(_) => "oversionmangle",
28 WatchOption::Downloadurlmangle(_) => "downloadurlmangle",
29 WatchOption::Pgpsigurlmangle(_) => "pgpsigurlmangle",
30 WatchOption::Filenamemangle(_) => "filenamemangle",
31 WatchOption::VersionPolicy(_) => "version-policy",
32 WatchOption::Searchmode(_) => "searchmode",
33 WatchOption::Mode(_) => "mode",
34 WatchOption::Pgpmode(_) => "pgpmode",
35 WatchOption::Gitexport(_) => "gitexport",
36 WatchOption::Gitmode(_) => "gitmode",
37 WatchOption::Pretty(_) => "pretty",
38 WatchOption::Ctype(_) => "ctype",
39 WatchOption::Repacksuffix(_) => "repacksuffix",
40 WatchOption::Unzipopt(_) => "unzipopt",
41 WatchOption::Script(_) => "script",
42 WatchOption::Decompress => "decompress",
43 WatchOption::Bare => "bare",
44 WatchOption::Repack => "repack",
45 }
46}
47
48pub(crate) fn watch_option_to_value(option: &crate::types::WatchOption) -> String {
50 use crate::types::WatchOption;
51
52 match option {
53 WatchOption::Component(v) => v.clone(),
54 WatchOption::Compression(v) => v.to_string(),
55 WatchOption::UserAgent(v) => v.clone(),
56 WatchOption::Pagemangle(v) => v.clone(),
57 WatchOption::Uversionmangle(v) => v.clone(),
58 WatchOption::Dversionmangle(v) => v.clone(),
59 WatchOption::Dirversionmangle(v) => v.clone(),
60 WatchOption::Oversionmangle(v) => v.clone(),
61 WatchOption::Downloadurlmangle(v) => v.clone(),
62 WatchOption::Pgpsigurlmangle(v) => v.clone(),
63 WatchOption::Filenamemangle(v) => v.clone(),
64 WatchOption::VersionPolicy(v) => v.to_string(),
65 WatchOption::Searchmode(v) => v.to_string(),
66 WatchOption::Mode(v) => v.to_string(),
67 WatchOption::Pgpmode(v) => v.to_string(),
68 WatchOption::Gitexport(v) => v.to_string(),
69 WatchOption::Gitmode(v) => v.to_string(),
70 WatchOption::Pretty(v) => v.to_string(),
71 WatchOption::Ctype(v) => v.to_string(),
72 WatchOption::Repacksuffix(v) => v.clone(),
73 WatchOption::Unzipopt(v) => v.clone(),
74 WatchOption::Script(v) => v.clone(),
75 WatchOption::Decompress => String::new(),
76 WatchOption::Bare => String::new(),
77 WatchOption::Repack => String::new(),
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash)]
82pub struct ParseError(pub Vec<String>);
84
85impl std::fmt::Display for ParseError {
86 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
87 for err in &self.0 {
88 writeln!(f, "{}", err)?;
89 }
90 Ok(())
91 }
92}
93
94impl std::error::Error for ParseError {}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
100pub enum Lang {}
101impl rowan::Language for Lang {
102 type Kind = SyntaxKind;
103 fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
104 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
105 }
106 fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
107 kind.into()
108 }
109}
110
111use rowan::GreenNode;
114
115use rowan::GreenNodeBuilder;
119
120#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct Parse<T> {
124 green: GreenNode,
126 errors: Vec<String>,
128 _ty: PhantomData<T>,
130}
131
132impl<T> Parse<T> {
133 pub(crate) fn new(green: GreenNode, errors: Vec<String>) -> Self {
135 Parse {
136 green,
137 errors,
138 _ty: PhantomData,
139 }
140 }
141
142 pub fn green(&self) -> &GreenNode {
144 &self.green
145 }
146
147 pub fn errors(&self) -> &[String] {
149 &self.errors
150 }
151
152 pub fn is_ok(&self) -> bool {
154 self.errors.is_empty()
155 }
156}
157
158impl Parse<WatchFile> {
159 pub fn tree(&self) -> WatchFile {
161 WatchFile::cast(SyntaxNode::new_root_mut(self.green.clone()))
162 .expect("root node should be a WatchFile")
163 }
164}
165
166unsafe impl<T> Send for Parse<T> {}
169unsafe impl<T> Sync for Parse<T> {}
170
171struct InternalParse {
173 green_node: GreenNode,
174 errors: Vec<String>,
175}
176
177fn parse(text: &str) -> InternalParse {
178 struct Parser {
179 tokens: Vec<(SyntaxKind, String)>,
182 builder: GreenNodeBuilder<'static>,
184 errors: Vec<String>,
187 }
188
189 impl Parser {
190 fn parse_version(&mut self) -> Option<u32> {
191 let mut version = None;
192 if self.tokens.last() == Some(&(KEY, "version".to_string())) {
193 self.builder.start_node(VERSION.into());
194 self.bump();
195 self.skip_ws();
196 if self.current() != Some(EQUALS) {
197 self.builder.start_node(ERROR.into());
198 self.errors.push("expected `=`".to_string());
199 self.bump();
200 self.builder.finish_node();
201 } else {
202 self.bump();
203 }
204 if self.current() != Some(VALUE) {
205 self.builder.start_node(ERROR.into());
206 self.errors
207 .push(format!("expected value, got {:?}", self.current()));
208 self.bump();
209 self.builder.finish_node();
210 } else if let Some((_, value)) = self.tokens.last() {
211 let version_str = value;
212 match version_str.parse() {
213 Ok(v) => {
214 version = Some(v);
215 self.bump();
216 }
217 Err(_) => {
218 self.builder.start_node(ERROR.into());
219 self.errors
220 .push(format!("invalid version: {}", version_str));
221 self.bump();
222 self.builder.finish_node();
223 }
224 }
225 } else {
226 self.builder.start_node(ERROR.into());
227 self.errors.push("expected version value".to_string());
228 self.builder.finish_node();
229 }
230 if self.current() != Some(NEWLINE) {
231 self.builder.start_node(ERROR.into());
232 self.errors.push("expected newline".to_string());
233 self.bump();
234 self.builder.finish_node();
235 } else {
236 self.bump();
237 }
238 self.builder.finish_node();
239 }
240 version
241 }
242
243 fn parse_watch_entry(&mut self) -> bool {
244 loop {
246 self.skip_ws();
247 if self.current() == Some(NEWLINE) {
248 self.bump();
249 } else {
250 break;
251 }
252 }
253 if self.current().is_none() {
254 return false;
255 }
256 self.builder.start_node(ENTRY.into());
257 self.parse_options_list();
258 for i in 0..4 {
259 if self.current() == Some(NEWLINE) || self.current().is_none() {
260 break;
261 }
262 if self.current() == Some(CONTINUATION) {
263 self.bump();
264 self.skip_ws();
265 continue;
266 }
267 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
268 self.builder.start_node(ERROR.into());
269 self.errors.push(format!(
270 "expected value, got {:?} (i={})",
271 self.current(),
272 i
273 ));
274 if self.current().is_some() {
275 self.bump();
276 }
277 self.builder.finish_node();
278 } else {
279 match i {
281 0 => {
282 self.builder.start_node(URL.into());
284 self.bump();
285 self.builder.finish_node();
286 }
287 1 => {
288 self.builder.start_node(MATCHING_PATTERN.into());
290 self.bump();
291 self.builder.finish_node();
292 }
293 2 => {
294 self.builder.start_node(VERSION_POLICY.into());
296 self.bump();
297 self.builder.finish_node();
298 }
299 3 => {
300 self.builder.start_node(SCRIPT.into());
302 self.bump();
303 self.builder.finish_node();
304 }
305 _ => {
306 self.bump();
307 }
308 }
309 }
310 self.skip_ws();
311 }
312 if self.current() != Some(NEWLINE) && self.current().is_some() {
313 self.builder.start_node(ERROR.into());
314 self.errors
315 .push(format!("expected newline, not {:?}", self.current()));
316 if self.current().is_some() {
317 self.bump();
318 }
319 self.builder.finish_node();
320 } else if self.current().is_some() {
321 self.bump();
323 }
324 self.builder.finish_node();
325 true
326 }
327
328 fn parse_option(&mut self) -> bool {
329 if self.current().is_none() {
330 return false;
331 }
332 while self.current() == Some(CONTINUATION) {
333 self.bump();
334 }
335 if self.current() == Some(WHITESPACE) {
336 return false;
337 }
338 self.builder.start_node(OPTION.into());
339 if self.current() != Some(KEY) {
340 self.builder.start_node(ERROR.into());
341 self.errors.push("expected key".to_string());
342 self.bump();
343 self.builder.finish_node();
344 } else {
345 self.bump();
346 }
347 if self.current() == Some(EQUALS) {
348 self.bump();
349 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
350 self.builder.start_node(ERROR.into());
351 self.errors
352 .push(format!("expected value, got {:?}", self.current()));
353 self.bump();
354 self.builder.finish_node();
355 } else {
356 self.bump();
357 }
358 } else if self.current() == Some(COMMA) {
359 } else {
360 self.builder.start_node(ERROR.into());
361 self.errors.push("expected `=`".to_string());
362 if self.current().is_some() {
363 self.bump();
364 }
365 self.builder.finish_node();
366 }
367 self.builder.finish_node();
368 true
369 }
370
371 fn parse_options_list(&mut self) {
372 self.skip_ws();
373 if self.tokens.last() == Some(&(KEY, "opts".to_string()))
374 || self.tokens.last() == Some(&(KEY, "options".to_string()))
375 {
376 self.builder.start_node(OPTS_LIST.into());
377 self.bump();
378 self.skip_ws();
379 if self.current() != Some(EQUALS) {
380 self.builder.start_node(ERROR.into());
381 self.errors.push("expected `=`".to_string());
382 if self.current().is_some() {
383 self.bump();
384 }
385 self.builder.finish_node();
386 } else {
387 self.bump();
388 }
389 let quoted = if self.current() == Some(QUOTE) {
390 self.bump();
391 true
392 } else {
393 false
394 };
395 loop {
396 if quoted {
397 if self.current() == Some(QUOTE) {
398 self.bump();
399 break;
400 }
401 self.skip_ws();
402 }
403 if !self.parse_option() {
404 break;
405 }
406 if self.current() == Some(COMMA) {
407 self.builder.start_node(OPTION_SEPARATOR.into());
408 self.bump();
409 self.builder.finish_node();
410 } else if !quoted {
411 break;
412 }
413 }
414 self.builder.finish_node();
415 self.skip_ws();
416 }
417 }
418
419 fn parse(mut self) -> InternalParse {
420 self.builder.start_node(ROOT.into());
422 while self.current() == Some(WHITESPACE)
424 || self.current() == Some(CONTINUATION)
425 || self.current() == Some(COMMENT)
426 || self.current() == Some(NEWLINE)
427 {
428 self.bump();
429 }
430 if let Some(_v) = self.parse_version() {
431 }
433 loop {
435 if !self.parse_watch_entry() {
436 break;
437 }
438 }
439 self.skip_ws();
441 if self.current().is_some() {
444 self.builder.start_node(ERROR.into());
445 self.errors
446 .push("unexpected tokens after last entry".to_string());
447 while self.current().is_some() {
448 self.bump();
449 }
450 self.builder.finish_node();
451 }
452 self.builder.finish_node();
454
455 InternalParse {
457 green_node: self.builder.finish(),
458 errors: self.errors,
459 }
460 }
461 fn bump(&mut self) {
463 if let Some((kind, text)) = self.tokens.pop() {
464 self.builder.token(kind.into(), text.as_str());
465 }
466 }
467 fn current(&self) -> Option<SyntaxKind> {
469 self.tokens.last().map(|(kind, _)| *kind)
470 }
471 fn skip_ws(&mut self) {
472 while self.current() == Some(WHITESPACE)
473 || self.current() == Some(CONTINUATION)
474 || self.current() == Some(COMMENT)
475 {
476 self.bump()
477 }
478 }
479 }
480
481 let mut tokens = lex(text);
482 tokens.reverse();
483 Parser {
484 tokens,
485 builder: GreenNodeBuilder::new(),
486 errors: Vec::new(),
487 }
488 .parse()
489}
490
491type SyntaxNode = rowan::SyntaxNode<Lang>;
497#[allow(unused)]
498type SyntaxToken = rowan::SyntaxToken<Lang>;
499#[allow(unused)]
500type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
501
502impl InternalParse {
503 fn syntax(&self) -> SyntaxNode {
504 SyntaxNode::new_root_mut(self.green_node.clone())
505 }
506
507 fn root(&self) -> WatchFile {
508 WatchFile::cast(self.syntax()).expect("root node should be a WatchFile")
509 }
510}
511
512fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
515 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
516 let mut line = 0;
517 let mut last_newline_offset = rowan::TextSize::from(0);
518
519 for element in root.preorder_with_tokens() {
520 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
521 if token.text_range().start() >= offset {
522 break;
523 }
524
525 for (idx, _) in token.text().match_indices('\n') {
527 line += 1;
528 last_newline_offset =
529 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
530 }
531 }
532 }
533
534 let column: usize = (offset - last_newline_offset).into();
535 (line, column)
536}
537
538macro_rules! ast_node {
539 ($ast:ident, $kind:ident) => {
540 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
541 #[repr(transparent)]
542 pub struct $ast(SyntaxNode);
544 impl $ast {
545 #[allow(unused)]
546 fn cast(node: SyntaxNode) -> Option<Self> {
547 if node.kind() == $kind {
548 Some(Self(node))
549 } else {
550 None
551 }
552 }
553
554 pub fn line(&self) -> usize {
556 line_col_at_offset(&self.0, self.0.text_range().start()).0
557 }
558
559 pub fn column(&self) -> usize {
561 line_col_at_offset(&self.0, self.0.text_range().start()).1
562 }
563
564 pub fn line_col(&self) -> (usize, usize) {
567 line_col_at_offset(&self.0, self.0.text_range().start())
568 }
569 }
570
571 impl std::fmt::Display for $ast {
572 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
573 write!(f, "{}", self.0.text())
574 }
575 }
576 };
577}
578
579ast_node!(WatchFile, ROOT);
580ast_node!(Version, VERSION);
581ast_node!(Entry, ENTRY);
582ast_node!(_Option, OPTION);
583ast_node!(Url, URL);
584ast_node!(MatchingPattern, MATCHING_PATTERN);
585ast_node!(VersionPolicyNode, VERSION_POLICY);
586ast_node!(ScriptNode, SCRIPT);
587
588#[derive(Clone, PartialEq, Eq, Hash)]
590#[repr(transparent)]
591pub struct OptionList(SyntaxNode);
593
594impl OptionList {
595 #[allow(unused)]
596 fn cast(node: SyntaxNode) -> Option<Self> {
597 if node.kind() == OPTS_LIST {
598 Some(Self(node))
599 } else {
600 None
601 }
602 }
603
604 pub fn line(&self) -> usize {
606 line_col_at_offset(&self.0, self.0.text_range().start()).0
607 }
608
609 pub fn column(&self) -> usize {
611 line_col_at_offset(&self.0, self.0.text_range().start()).1
612 }
613
614 pub fn line_col(&self) -> (usize, usize) {
617 line_col_at_offset(&self.0, self.0.text_range().start())
618 }
619}
620
621impl std::fmt::Display for OptionList {
622 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
623 write!(f, "{}", self.0.text())
624 }
625}
626
627impl WatchFile {
628 pub fn syntax(&self) -> &SyntaxNode {
630 &self.0
631 }
632
633 pub fn new(version: Option<u32>) -> WatchFile {
635 let mut builder = GreenNodeBuilder::new();
636
637 builder.start_node(ROOT.into());
638 if let Some(version) = version {
639 builder.start_node(VERSION.into());
640 builder.token(KEY.into(), "version");
641 builder.token(EQUALS.into(), "=");
642 builder.token(VALUE.into(), version.to_string().as_str());
643 builder.token(NEWLINE.into(), "\n");
644 builder.finish_node();
645 }
646 builder.finish_node();
647 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
648 }
649
650 pub fn version_node(&self) -> Option<Version> {
652 self.0.children().find_map(Version::cast)
653 }
654
655 pub fn version(&self) -> u32 {
657 self.version_node()
658 .map(|it| it.version())
659 .unwrap_or(DEFAULT_VERSION)
660 }
661
662 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
664 self.0.children().filter_map(Entry::cast)
665 }
666
667 pub fn set_version(&mut self, new_version: u32) {
669 let mut builder = GreenNodeBuilder::new();
671 builder.start_node(VERSION.into());
672 builder.token(KEY.into(), "version");
673 builder.token(EQUALS.into(), "=");
674 builder.token(VALUE.into(), new_version.to_string().as_str());
675 builder.token(NEWLINE.into(), "\n");
676 builder.finish_node();
677 let new_version_green = builder.finish();
678
679 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
681
682 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
684
685 if let Some(pos) = version_pos {
686 self.0
688 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
689 } else {
690 self.0.splice_children(0..0, vec![new_version_node.into()]);
692 }
693 }
694
695 #[cfg(feature = "discover")]
715 pub async fn uscan(
716 &self,
717 package: impl Fn() -> String + Send + Sync,
718 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
719 let mut all_releases = Vec::new();
720
721 for entry in self.entries() {
722 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
723 let releases = parsed_entry.discover(|| package()).await?;
724 all_releases.push(releases);
725 }
726
727 Ok(all_releases)
728 }
729
730 #[cfg(all(feature = "discover", feature = "blocking"))]
748 pub fn uscan_blocking(
749 &self,
750 package: impl Fn() -> String,
751 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
752 let mut all_releases = Vec::new();
753
754 for entry in self.entries() {
755 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
756 let releases = parsed_entry.discover_blocking(|| package())?;
757 all_releases.push(releases);
758 }
759
760 Ok(all_releases)
761 }
762
763 pub fn add_entry(&mut self, entry: Entry) -> Entry {
790 let insert_pos = self.0.children_with_tokens().count();
792
793 let entry_green = entry.0.green().into_owned();
795 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
796
797 self.0
799 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
800
801 Entry::cast(
803 self.0
804 .children()
805 .nth(insert_pos)
806 .expect("Entry was just inserted"),
807 )
808 .expect("Inserted node should be an Entry")
809 }
810
811 pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
813 let mut buf_reader = std::io::BufReader::new(reader);
814 let mut content = String::new();
815 buf_reader
816 .read_to_string(&mut content)
817 .map_err(|e| ParseError(vec![e.to_string()]))?;
818 content.parse()
819 }
820
821 pub fn from_reader_relaxed<R: std::io::Read>(mut r: R) -> Result<Self, std::io::Error> {
823 let mut content = String::new();
824 r.read_to_string(&mut content)?;
825 let parsed = parse(&content);
826 Ok(parsed.root())
827 }
828
829 pub fn from_str_relaxed(s: &str) -> Self {
831 let parsed = parse(s);
832 parsed.root()
833 }
834}
835
836impl FromStr for WatchFile {
837 type Err = ParseError;
838
839 fn from_str(s: &str) -> Result<Self, Self::Err> {
840 let parsed = parse(s);
841 if parsed.errors.is_empty() {
842 Ok(parsed.root())
843 } else {
844 Err(ParseError(parsed.errors))
845 }
846 }
847}
848
849pub fn parse_watch_file(text: &str) -> Parse<WatchFile> {
852 let parsed = parse(text);
853 Parse::new(parsed.green_node, parsed.errors)
854}
855
856impl Version {
857 pub fn version(&self) -> u32 {
859 self.0
860 .children_with_tokens()
861 .find_map(|it| match it {
862 SyntaxElement::Token(token) => {
863 if token.kind() == VALUE {
864 token.text().parse().ok()
865 } else {
866 None
867 }
868 }
869 _ => None,
870 })
871 .unwrap_or(DEFAULT_VERSION)
872 }
873}
874
875#[derive(Debug, Clone, Default)]
899pub struct EntryBuilder {
900 url: Option<String>,
901 matching_pattern: Option<String>,
902 version_policy: Option<String>,
903 script: Option<String>,
904 opts: std::collections::HashMap<String, String>,
905}
906
907impl EntryBuilder {
908 pub fn new(url: impl Into<String>) -> Self {
910 EntryBuilder {
911 url: Some(url.into()),
912 matching_pattern: None,
913 version_policy: None,
914 script: None,
915 opts: std::collections::HashMap::new(),
916 }
917 }
918
919 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
921 self.matching_pattern = Some(pattern.into());
922 self
923 }
924
925 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
927 self.version_policy = Some(policy.into());
928 self
929 }
930
931 pub fn script(mut self, script: impl Into<String>) -> Self {
933 self.script = Some(script.into());
934 self
935 }
936
937 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
939 self.opts.insert(key.into(), value.into());
940 self
941 }
942
943 pub fn flag(mut self, key: impl Into<String>) -> Self {
947 self.opts.insert(key.into(), String::new());
948 self
949 }
950
951 pub fn build(self) -> Entry {
957 let url = self.url.expect("URL is required for entry");
958
959 let mut builder = GreenNodeBuilder::new();
960
961 builder.start_node(ENTRY.into());
962
963 if !self.opts.is_empty() {
965 builder.start_node(OPTS_LIST.into());
966 builder.token(KEY.into(), "opts");
967 builder.token(EQUALS.into(), "=");
968
969 let mut first = true;
970 for (key, value) in self.opts.iter() {
971 if !first {
972 builder.token(COMMA.into(), ",");
973 }
974 first = false;
975
976 builder.start_node(OPTION.into());
977 builder.token(KEY.into(), key);
978 if !value.is_empty() {
979 builder.token(EQUALS.into(), "=");
980 builder.token(VALUE.into(), value);
981 }
982 builder.finish_node();
983 }
984
985 builder.finish_node();
986 builder.token(WHITESPACE.into(), " ");
987 }
988
989 builder.start_node(URL.into());
991 builder.token(VALUE.into(), &url);
992 builder.finish_node();
993
994 if let Some(pattern) = self.matching_pattern {
996 builder.token(WHITESPACE.into(), " ");
997 builder.start_node(MATCHING_PATTERN.into());
998 builder.token(VALUE.into(), &pattern);
999 builder.finish_node();
1000 }
1001
1002 if let Some(policy) = self.version_policy {
1004 builder.token(WHITESPACE.into(), " ");
1005 builder.start_node(VERSION_POLICY.into());
1006 builder.token(VALUE.into(), &policy);
1007 builder.finish_node();
1008 }
1009
1010 if let Some(script_val) = self.script {
1012 builder.token(WHITESPACE.into(), " ");
1013 builder.start_node(SCRIPT.into());
1014 builder.token(VALUE.into(), &script_val);
1015 builder.finish_node();
1016 }
1017
1018 builder.token(NEWLINE.into(), "\n");
1019 builder.finish_node();
1020
1021 Entry(SyntaxNode::new_root_mut(builder.finish()))
1022 }
1023}
1024
1025impl Entry {
1026 pub fn syntax(&self) -> &SyntaxNode {
1028 &self.0
1029 }
1030
1031 pub fn builder(url: impl Into<String>) -> EntryBuilder {
1045 EntryBuilder::new(url)
1046 }
1047
1048 pub fn option_list(&self) -> Option<OptionList> {
1050 self.0.children().find_map(OptionList::cast)
1051 }
1052
1053 pub fn get_option(&self, key: &str) -> Option<String> {
1055 self.option_list().and_then(|ol| ol.get_option(key))
1056 }
1057
1058 pub fn has_option(&self, key: &str) -> bool {
1060 self.option_list().is_some_and(|ol| ol.has_option(key))
1061 }
1062
1063 pub fn component(&self) -> Option<String> {
1065 self.get_option("component")
1066 }
1067
1068 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
1070 self.try_ctype().map_err(|_| ())
1071 }
1072
1073 pub fn try_ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
1075 self.get_option("ctype").map(|s| s.parse()).transpose()
1076 }
1077
1078 pub fn compression(&self) -> Result<Option<Compression>, ()> {
1080 self.try_compression().map_err(|_| ())
1081 }
1082
1083 pub fn try_compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
1085 self.get_option("compression")
1086 .map(|s| s.parse())
1087 .transpose()
1088 }
1089
1090 pub fn repack(&self) -> bool {
1092 self.has_option("repack")
1093 }
1094
1095 pub fn repacksuffix(&self) -> Option<String> {
1097 self.get_option("repacksuffix")
1098 }
1099
1100 pub fn mode(&self) -> Result<Mode, ()> {
1102 self.try_mode().map_err(|_| ())
1103 }
1104
1105 pub fn try_mode(&self) -> Result<Mode, crate::types::ParseError> {
1107 Ok(self
1108 .get_option("mode")
1109 .map(|s| s.parse())
1110 .transpose()?
1111 .unwrap_or_default())
1112 }
1113
1114 pub fn pretty(&self) -> Result<Pretty, ()> {
1116 self.try_pretty().map_err(|_| ())
1117 }
1118
1119 pub fn try_pretty(&self) -> Result<Pretty, crate::types::ParseError> {
1121 Ok(self
1122 .get_option("pretty")
1123 .map(|s| s.parse())
1124 .transpose()?
1125 .unwrap_or_default())
1126 }
1127
1128 pub fn date(&self) -> String {
1131 self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
1132 }
1133
1134 pub fn gitexport(&self) -> Result<GitExport, ()> {
1136 self.try_gitexport().map_err(|_| ())
1137 }
1138
1139 pub fn try_gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
1141 Ok(self
1142 .get_option("gitexport")
1143 .map(|s| s.parse())
1144 .transpose()?
1145 .unwrap_or_default())
1146 }
1147
1148 pub fn gitmode(&self) -> Result<GitMode, ()> {
1150 self.try_gitmode().map_err(|_| ())
1151 }
1152
1153 pub fn try_gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
1155 Ok(self
1156 .get_option("gitmode")
1157 .map(|s| s.parse())
1158 .transpose()?
1159 .unwrap_or_default())
1160 }
1161
1162 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
1164 self.try_pgpmode().map_err(|_| ())
1165 }
1166
1167 pub fn try_pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
1169 Ok(self
1170 .get_option("pgpmode")
1171 .map(|s| s.parse())
1172 .transpose()?
1173 .unwrap_or_default())
1174 }
1175
1176 pub fn searchmode(&self) -> Result<SearchMode, ()> {
1178 self.try_searchmode().map_err(|_| ())
1179 }
1180
1181 pub fn try_searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
1183 Ok(self
1184 .get_option("searchmode")
1185 .map(|s| s.parse())
1186 .transpose()?
1187 .unwrap_or_default())
1188 }
1189
1190 pub fn decompress(&self) -> bool {
1192 self.has_option("decompress")
1193 }
1194
1195 pub fn bare(&self) -> bool {
1198 self.has_option("bare")
1199 }
1200
1201 pub fn user_agent(&self) -> Option<String> {
1203 self.get_option("user-agent")
1204 }
1205
1206 pub fn passive(&self) -> Option<bool> {
1208 if self.has_option("passive") || self.has_option("pasv") {
1209 Some(true)
1210 } else if self.has_option("active") || self.has_option("nopasv") {
1211 Some(false)
1212 } else {
1213 None
1214 }
1215 }
1216
1217 pub fn unzipoptions(&self) -> Option<String> {
1220 self.get_option("unzipopt")
1221 }
1222
1223 pub fn dversionmangle(&self) -> Option<String> {
1225 self.get_option("dversionmangle")
1226 .or_else(|| self.get_option("versionmangle"))
1227 }
1228
1229 pub fn dirversionmangle(&self) -> Option<String> {
1233 self.get_option("dirversionmangle")
1234 }
1235
1236 pub fn pagemangle(&self) -> Option<String> {
1238 self.get_option("pagemangle")
1239 }
1240
1241 pub fn uversionmangle(&self) -> Option<String> {
1245 self.get_option("uversionmangle")
1246 .or_else(|| self.get_option("versionmangle"))
1247 }
1248
1249 pub fn versionmangle(&self) -> Option<String> {
1251 self.get_option("versionmangle")
1252 }
1253
1254 pub fn hrefdecode(&self) -> bool {
1259 self.get_option("hrefdecode").is_some()
1260 }
1261
1262 pub fn downloadurlmangle(&self) -> Option<String> {
1265 self.get_option("downloadurlmangle")
1266 }
1267
1268 pub fn filenamemangle(&self) -> Option<String> {
1276 self.get_option("filenamemangle")
1277 }
1278
1279 pub fn pgpsigurlmangle(&self) -> Option<String> {
1281 self.get_option("pgpsigurlmangle")
1282 }
1283
1284 pub fn oversionmangle(&self) -> Option<String> {
1287 self.get_option("oversionmangle")
1288 }
1289
1290 pub fn apply_uversionmangle(
1303 &self,
1304 version: &str,
1305 ) -> Result<String, crate::mangle::MangleError> {
1306 if let Some(vm) = self.uversionmangle() {
1307 crate::mangle::apply_mangle(&vm, version)
1308 } else {
1309 Ok(version.to_string())
1310 }
1311 }
1312
1313 pub fn apply_dversionmangle(
1326 &self,
1327 version: &str,
1328 ) -> Result<String, crate::mangle::MangleError> {
1329 if let Some(vm) = self.dversionmangle() {
1330 crate::mangle::apply_mangle(&vm, version)
1331 } else {
1332 Ok(version.to_string())
1333 }
1334 }
1335
1336 pub fn apply_oversionmangle(
1349 &self,
1350 version: &str,
1351 ) -> Result<String, crate::mangle::MangleError> {
1352 if let Some(vm) = self.oversionmangle() {
1353 crate::mangle::apply_mangle(&vm, version)
1354 } else {
1355 Ok(version.to_string())
1356 }
1357 }
1358
1359 pub fn apply_dirversionmangle(
1372 &self,
1373 version: &str,
1374 ) -> Result<String, crate::mangle::MangleError> {
1375 if let Some(vm) = self.dirversionmangle() {
1376 crate::mangle::apply_mangle(&vm, version)
1377 } else {
1378 Ok(version.to_string())
1379 }
1380 }
1381
1382 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1398 if let Some(vm) = self.filenamemangle() {
1399 crate::mangle::apply_mangle(&vm, url)
1400 } else {
1401 Ok(url.to_string())
1402 }
1403 }
1404
1405 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1421 if let Some(vm) = self.pagemangle() {
1422 let page_str = String::from_utf8_lossy(page);
1423 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1424 Ok(mangled.into_bytes())
1425 } else {
1426 Ok(page.to_vec())
1427 }
1428 }
1429
1430 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1446 if let Some(vm) = self.downloadurlmangle() {
1447 crate::mangle::apply_mangle(&vm, url)
1448 } else {
1449 Ok(url.to_string())
1450 }
1451 }
1452
1453 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1455 let mut options = std::collections::HashMap::new();
1456
1457 if let Some(ol) = self.option_list() {
1458 for opt in ol.options() {
1459 let key = opt.key();
1460 let value = opt.value();
1461 if let (Some(key), Some(value)) = (key, value) {
1462 options.insert(key.to_string(), value.to_string());
1463 }
1464 }
1465 }
1466
1467 options
1468 }
1469
1470 fn items(&self) -> impl Iterator<Item = String> + '_ {
1471 self.0.children_with_tokens().filter_map(|it| match it {
1472 SyntaxElement::Token(token) => {
1473 if token.kind() == VALUE || token.kind() == KEY {
1474 Some(token.text().to_string())
1475 } else {
1476 None
1477 }
1478 }
1479 SyntaxElement::Node(node) => {
1480 match node.kind() {
1482 URL => Url::cast(node).map(|n| n.url()),
1483 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1484 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1485 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1486 _ => None,
1487 }
1488 }
1489 })
1490 }
1491
1492 pub fn url_node(&self) -> Option<Url> {
1494 self.0.children().find_map(Url::cast)
1495 }
1496
1497 pub fn url(&self) -> String {
1499 self.url_node().map(|it| it.url()).unwrap_or_else(|| {
1500 self.items().next().unwrap()
1502 })
1503 }
1504
1505 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1507 self.0.children().find_map(MatchingPattern::cast)
1508 }
1509
1510 pub fn matching_pattern(&self) -> Option<String> {
1512 self.matching_pattern_node()
1513 .map(|it| it.pattern())
1514 .or_else(|| {
1515 self.items().nth(1)
1517 })
1518 }
1519
1520 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1522 self.0.children().find_map(VersionPolicyNode::cast)
1523 }
1524
1525 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1527 self.version_node()
1528 .map(|it| it.policy().parse())
1529 .transpose()
1530 .map_err(|e: crate::types::ParseError| e.to_string())
1531 .or_else(|_e| {
1532 self.items()
1534 .nth(2)
1535 .map(|it| it.parse())
1536 .transpose()
1537 .map_err(|e: crate::types::ParseError| e.to_string())
1538 })
1539 }
1540
1541 pub fn script_node(&self) -> Option<ScriptNode> {
1543 self.0.children().find_map(ScriptNode::cast)
1544 }
1545
1546 pub fn script(&self) -> Option<String> {
1548 self.script_node().map(|it| it.script()).or_else(|| {
1549 self.items().nth(3)
1551 })
1552 }
1553
1554 pub fn format_url(
1556 &self,
1557 package: impl FnOnce() -> String,
1558 component: impl FnOnce() -> String,
1559 ) -> url::Url {
1560 crate::subst::subst(self.url().as_str(), package, component)
1561 .parse()
1562 .unwrap()
1563 }
1564
1565 pub fn set_url(&mut self, new_url: &str) {
1567 let mut builder = GreenNodeBuilder::new();
1569 builder.start_node(URL.into());
1570 builder.token(VALUE.into(), new_url);
1571 builder.finish_node();
1572 let new_url_green = builder.finish();
1573
1574 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1576
1577 let url_pos = self
1579 .0
1580 .children_with_tokens()
1581 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1582
1583 if let Some(pos) = url_pos {
1584 self.0
1586 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1587 }
1588 }
1589
1590 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1596 let mut builder = GreenNodeBuilder::new();
1598 builder.start_node(MATCHING_PATTERN.into());
1599 builder.token(VALUE.into(), new_pattern);
1600 builder.finish_node();
1601 let new_pattern_green = builder.finish();
1602
1603 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1605
1606 let pattern_pos = self.0.children_with_tokens().position(
1608 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1609 );
1610
1611 if let Some(pos) = pattern_pos {
1612 self.0
1614 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1615 }
1616 }
1618
1619 pub fn set_version_policy(&mut self, new_policy: &str) {
1625 let mut builder = GreenNodeBuilder::new();
1627 builder.start_node(VERSION_POLICY.into());
1628 builder.token(VALUE.into(), new_policy);
1630 builder.finish_node();
1631 let new_policy_green = builder.finish();
1632
1633 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1635
1636 let policy_pos = self.0.children_with_tokens().position(
1638 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1639 );
1640
1641 if let Some(pos) = policy_pos {
1642 self.0
1644 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1645 }
1646 }
1648
1649 pub fn set_script(&mut self, new_script: &str) {
1655 let mut builder = GreenNodeBuilder::new();
1657 builder.start_node(SCRIPT.into());
1658 builder.token(VALUE.into(), new_script);
1660 builder.finish_node();
1661 let new_script_green = builder.finish();
1662
1663 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1665
1666 let script_pos = self
1668 .0
1669 .children_with_tokens()
1670 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1671
1672 if let Some(pos) = script_pos {
1673 self.0
1675 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1676 }
1677 }
1679
1680 pub fn set_option(&mut self, option: crate::types::WatchOption) {
1686 let key = watch_option_to_key(&option);
1687 let value = watch_option_to_value(&option);
1688 self.set_opt(key, &value);
1689 }
1690
1691 pub fn set_opt(&mut self, key: &str, value: &str) {
1697 let opts_pos = self.0.children_with_tokens().position(
1699 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1700 );
1701
1702 if let Some(_opts_idx) = opts_pos {
1703 if let Some(mut ol) = self.option_list() {
1704 if let Some(mut opt) = ol.find_option(key) {
1706 opt.set_value(value);
1708 } else {
1710 ol.add_option(key, value);
1712 }
1714 }
1715 } else {
1716 let mut builder = GreenNodeBuilder::new();
1718 builder.start_node(OPTS_LIST.into());
1719 builder.token(KEY.into(), "opts");
1720 builder.token(EQUALS.into(), "=");
1721 builder.start_node(OPTION.into());
1722 builder.token(KEY.into(), key);
1723 builder.token(EQUALS.into(), "=");
1724 builder.token(VALUE.into(), value);
1725 builder.finish_node();
1726 builder.finish_node();
1727 let new_opts_green = builder.finish();
1728 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1729
1730 let url_pos = self
1732 .0
1733 .children_with_tokens()
1734 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1735
1736 if let Some(url_idx) = url_pos {
1737 let mut combined_builder = GreenNodeBuilder::new();
1740 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1742 combined_builder.finish_node();
1743 let temp_green = combined_builder.finish();
1744 let temp_root = SyntaxNode::new_root_mut(temp_green);
1745 let space_element = temp_root.children_with_tokens().next().unwrap();
1746
1747 self.0
1748 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1749 } else {
1750 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1751 }
1752 }
1753 }
1754
1755 pub fn del_opt(&mut self, option: crate::types::WatchOption) {
1762 let key = watch_option_to_key(&option);
1763 if let Some(mut ol) = self.option_list() {
1764 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1765
1766 if option_count == 1 && ol.has_option(key) {
1767 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1769
1770 if let Some(opts_idx) = opts_pos {
1771 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1773
1774 while self.0.children_with_tokens().next().is_some_and(|e| {
1776 matches!(
1777 e,
1778 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1779 )
1780 }) {
1781 self.0.splice_children(0..1, vec![]);
1782 }
1783 }
1784 } else {
1785 ol.remove_option(key);
1787 }
1788 }
1789 }
1790
1791 pub fn del_opt_str(&mut self, key: &str) {
1798 if let Some(mut ol) = self.option_list() {
1799 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1800
1801 if option_count == 1 && ol.has_option(key) {
1802 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1804
1805 if let Some(opts_idx) = opts_pos {
1806 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1808
1809 while self.0.children_with_tokens().next().is_some_and(|e| {
1811 matches!(
1812 e,
1813 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1814 )
1815 }) {
1816 self.0.splice_children(0..1, vec![]);
1817 }
1818 }
1819 } else {
1820 ol.remove_option(key);
1822 }
1823 }
1824 }
1825}
1826
1827impl std::fmt::Debug for OptionList {
1828 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1829 f.debug_struct("OptionList")
1830 .field("text", &self.0.text().to_string())
1831 .finish()
1832 }
1833}
1834
1835impl OptionList {
1836 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1838 self.0.children().filter_map(_Option::cast)
1839 }
1840
1841 pub fn find_option(&self, key: &str) -> Option<_Option> {
1843 self.options().find(|opt| opt.key().as_deref() == Some(key))
1844 }
1845
1846 pub fn has_option(&self, key: &str) -> bool {
1848 self.options().any(|it| it.key().as_deref() == Some(key))
1849 }
1850
1851 #[cfg(feature = "deb822")]
1854 pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
1855 self.options().filter_map(|opt| {
1856 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
1857 Some((key, value))
1858 } else {
1859 None
1860 }
1861 })
1862 }
1863
1864 pub fn get_option(&self, key: &str) -> Option<String> {
1866 for child in self.options() {
1867 if child.key().as_deref() == Some(key) {
1868 return child.value();
1869 }
1870 }
1871 None
1872 }
1873
1874 fn add_option(&mut self, key: &str, value: &str) {
1876 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1877
1878 let mut builder = GreenNodeBuilder::new();
1880 builder.start_node(ROOT.into()); if option_count > 0 {
1883 builder.start_node(OPTION_SEPARATOR.into());
1884 builder.token(COMMA.into(), ",");
1885 builder.finish_node();
1886 }
1887
1888 builder.start_node(OPTION.into());
1889 builder.token(KEY.into(), key);
1890 builder.token(EQUALS.into(), "=");
1891 builder.token(VALUE.into(), value);
1892 builder.finish_node();
1893
1894 builder.finish_node(); let combined_green = builder.finish();
1896
1897 let temp_root = SyntaxNode::new_root_mut(combined_green);
1899 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1900
1901 let insert_pos = self.0.children_with_tokens().count();
1902 self.0.splice_children(insert_pos..insert_pos, new_children);
1903 }
1904
1905 fn remove_option(&mut self, key: &str) -> bool {
1907 if let Some(mut opt) = self.find_option(key) {
1908 opt.remove();
1909 true
1910 } else {
1911 false
1912 }
1913 }
1914}
1915
1916impl _Option {
1917 pub fn key(&self) -> Option<String> {
1919 self.0.children_with_tokens().find_map(|it| match it {
1920 SyntaxElement::Token(token) => {
1921 if token.kind() == KEY {
1922 Some(token.text().to_string())
1923 } else {
1924 None
1925 }
1926 }
1927 _ => None,
1928 })
1929 }
1930
1931 pub fn value(&self) -> Option<String> {
1933 self.0
1934 .children_with_tokens()
1935 .filter_map(|it| match it {
1936 SyntaxElement::Token(token) => {
1937 if token.kind() == VALUE || token.kind() == KEY {
1938 Some(token.text().to_string())
1939 } else {
1940 None
1941 }
1942 }
1943 _ => None,
1944 })
1945 .nth(1)
1946 }
1947
1948 pub fn set_value(&mut self, new_value: &str) {
1950 let key = self.key().expect("Option must have a key");
1951
1952 let mut builder = GreenNodeBuilder::new();
1954 builder.start_node(OPTION.into());
1955 builder.token(KEY.into(), &key);
1956 builder.token(EQUALS.into(), "=");
1957 builder.token(VALUE.into(), new_value);
1958 builder.finish_node();
1959 let new_option_green = builder.finish();
1960 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1961
1962 if let Some(parent) = self.0.parent() {
1964 let idx = self.0.index();
1965 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1966 }
1967 }
1968
1969 pub fn remove(&mut self) {
1971 let next_sep = self
1973 .0
1974 .next_sibling()
1975 .filter(|n| n.kind() == OPTION_SEPARATOR);
1976 let prev_sep = self
1977 .0
1978 .prev_sibling()
1979 .filter(|n| n.kind() == OPTION_SEPARATOR);
1980
1981 if let Some(sep) = next_sep {
1983 sep.detach();
1984 } else if let Some(sep) = prev_sep {
1985 sep.detach();
1986 }
1987
1988 self.0.detach();
1990 }
1991}
1992
1993impl Url {
1994 pub fn url(&self) -> String {
1996 self.0
1997 .children_with_tokens()
1998 .find_map(|it| match it {
1999 SyntaxElement::Token(token) => {
2000 if token.kind() == VALUE {
2001 Some(token.text().to_string())
2002 } else {
2003 None
2004 }
2005 }
2006 _ => None,
2007 })
2008 .unwrap()
2009 }
2010}
2011
2012impl MatchingPattern {
2013 pub fn pattern(&self) -> String {
2015 self.0
2016 .children_with_tokens()
2017 .find_map(|it| match it {
2018 SyntaxElement::Token(token) => {
2019 if token.kind() == VALUE {
2020 Some(token.text().to_string())
2021 } else {
2022 None
2023 }
2024 }
2025 _ => None,
2026 })
2027 .unwrap()
2028 }
2029}
2030
2031impl VersionPolicyNode {
2032 pub fn policy(&self) -> String {
2034 self.0
2035 .children_with_tokens()
2036 .find_map(|it| match it {
2037 SyntaxElement::Token(token) => {
2038 if token.kind() == VALUE || token.kind() == KEY {
2040 Some(token.text().to_string())
2041 } else {
2042 None
2043 }
2044 }
2045 _ => None,
2046 })
2047 .unwrap()
2048 }
2049}
2050
2051impl ScriptNode {
2052 pub fn script(&self) -> String {
2054 self.0
2055 .children_with_tokens()
2056 .find_map(|it| match it {
2057 SyntaxElement::Token(token) => {
2058 if token.kind() == VALUE || token.kind() == KEY {
2060 Some(token.text().to_string())
2061 } else {
2062 None
2063 }
2064 }
2065 _ => None,
2066 })
2067 .unwrap()
2068 }
2069}
2070
2071#[cfg(test)]
2072mod tests {
2073 use super::*;
2074
2075 #[test]
2076 fn test_entry_node_structure() {
2077 let wf: super::WatchFile = r#"version=4
2079opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2080"#
2081 .parse()
2082 .unwrap();
2083
2084 let entry = wf.entries().next().unwrap();
2085
2086 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2088 assert_eq!(entry.url(), "https://example.com/releases");
2089
2090 assert_eq!(
2092 entry
2093 .0
2094 .children()
2095 .find(|n| n.kind() == MATCHING_PATTERN)
2096 .is_some(),
2097 true
2098 );
2099 assert_eq!(
2100 entry.matching_pattern(),
2101 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2102 );
2103
2104 assert_eq!(
2106 entry
2107 .0
2108 .children()
2109 .find(|n| n.kind() == VERSION_POLICY)
2110 .is_some(),
2111 true
2112 );
2113 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2114
2115 assert_eq!(
2117 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2118 true
2119 );
2120 assert_eq!(entry.script(), Some("uupdate".into()));
2121 }
2122
2123 #[test]
2124 fn test_entry_node_structure_partial() {
2125 let wf: super::WatchFile = r#"version=4
2127https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2128"#
2129 .parse()
2130 .unwrap();
2131
2132 let entry = wf.entries().next().unwrap();
2133
2134 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2136 assert_eq!(
2137 entry
2138 .0
2139 .children()
2140 .find(|n| n.kind() == MATCHING_PATTERN)
2141 .is_some(),
2142 true
2143 );
2144
2145 assert_eq!(
2147 entry
2148 .0
2149 .children()
2150 .find(|n| n.kind() == VERSION_POLICY)
2151 .is_some(),
2152 false
2153 );
2154 assert_eq!(
2155 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2156 false
2157 );
2158
2159 assert_eq!(entry.url(), "https://github.com/example/tags");
2161 assert_eq!(
2162 entry.matching_pattern(),
2163 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2164 );
2165 assert_eq!(entry.version(), Ok(None));
2166 assert_eq!(entry.script(), None);
2167 }
2168
2169 #[test]
2170 fn test_parse_v1() {
2171 const WATCHV1: &str = r#"version=4
2172opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2173 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2174"#;
2175 let parsed = parse(WATCHV1);
2176 let node = parsed.syntax();
2178 assert_eq!(
2179 format!("{:#?}", node),
2180 r#"ROOT@0..161
2181 VERSION@0..10
2182 KEY@0..7 "version"
2183 EQUALS@7..8 "="
2184 VALUE@8..9 "4"
2185 NEWLINE@9..10 "\n"
2186 ENTRY@10..161
2187 OPTS_LIST@10..86
2188 KEY@10..14 "opts"
2189 EQUALS@14..15 "="
2190 OPTION@15..19
2191 KEY@15..19 "bare"
2192 OPTION_SEPARATOR@19..20
2193 COMMA@19..20 ","
2194 OPTION@20..86
2195 KEY@20..34 "filenamemangle"
2196 EQUALS@34..35 "="
2197 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2198 WHITESPACE@86..87 " "
2199 CONTINUATION@87..89 "\\\n"
2200 WHITESPACE@89..91 " "
2201 URL@91..138
2202 VALUE@91..138 "https://github.com/sy ..."
2203 WHITESPACE@138..139 " "
2204 MATCHING_PATTERN@139..160
2205 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2206 NEWLINE@160..161 "\n"
2207"#
2208 );
2209
2210 let root = parsed.root();
2211 assert_eq!(root.version(), 4);
2212 let entries = root.entries().collect::<Vec<_>>();
2213 assert_eq!(entries.len(), 1);
2214 let entry = &entries[0];
2215 assert_eq!(
2216 entry.url(),
2217 "https://github.com/syncthing/syncthing-gtk/tags"
2218 );
2219 assert_eq!(
2220 entry.matching_pattern(),
2221 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2222 );
2223 assert_eq!(entry.version(), Ok(None));
2224 assert_eq!(entry.script(), None);
2225
2226 assert_eq!(node.text(), WATCHV1);
2227 }
2228
2229 #[test]
2230 fn test_parse_v2() {
2231 let parsed = parse(
2232 r#"version=4
2233https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2234# comment
2235"#,
2236 );
2237 assert_eq!(parsed.errors, Vec::<String>::new());
2238 let node = parsed.syntax();
2239 assert_eq!(
2240 format!("{:#?}", node),
2241 r###"ROOT@0..90
2242 VERSION@0..10
2243 KEY@0..7 "version"
2244 EQUALS@7..8 "="
2245 VALUE@8..9 "4"
2246 NEWLINE@9..10 "\n"
2247 ENTRY@10..80
2248 URL@10..57
2249 VALUE@10..57 "https://github.com/sy ..."
2250 WHITESPACE@57..58 " "
2251 MATCHING_PATTERN@58..79
2252 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2253 NEWLINE@79..80 "\n"
2254 COMMENT@80..89 "# comment"
2255 NEWLINE@89..90 "\n"
2256"###
2257 );
2258
2259 let root = parsed.root();
2260 assert_eq!(root.version(), 4);
2261 let entries = root.entries().collect::<Vec<_>>();
2262 assert_eq!(entries.len(), 1);
2263 let entry = &entries[0];
2264 assert_eq!(
2265 entry.url(),
2266 "https://github.com/syncthing/syncthing-gtk/tags"
2267 );
2268 assert_eq!(
2269 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2270 "https://github.com/syncthing/syncthing-gtk/tags"
2271 .parse()
2272 .unwrap()
2273 );
2274 }
2275
2276 #[test]
2277 fn test_parse_v3() {
2278 let parsed = parse(
2279 r#"version=4
2280https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2281# comment
2282"#,
2283 );
2284 assert_eq!(parsed.errors, Vec::<String>::new());
2285 let root = parsed.root();
2286 assert_eq!(root.version(), 4);
2287 let entries = root.entries().collect::<Vec<_>>();
2288 assert_eq!(entries.len(), 1);
2289 let entry = &entries[0];
2290 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2291 assert_eq!(
2292 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2293 "https://github.com/syncthing/syncthing-gtk/tags"
2294 .parse()
2295 .unwrap()
2296 );
2297 }
2298
2299 #[test]
2300 fn test_thread_safe_parsing() {
2301 let text = r#"version=4
2302https://github.com/example/example/tags example-(.*)\.tar\.gz
2303"#;
2304
2305 let parsed = parse_watch_file(text);
2306 assert!(parsed.is_ok());
2307 assert_eq!(parsed.errors().len(), 0);
2308
2309 let watchfile = parsed.tree();
2311 assert_eq!(watchfile.version(), 4);
2312 let entries: Vec<_> = watchfile.entries().collect();
2313 assert_eq!(entries.len(), 1);
2314 }
2315
2316 #[test]
2317 fn test_parse_clone_and_eq() {
2318 let text = r#"version=4
2319https://github.com/example/example/tags example-(.*)\.tar\.gz
2320"#;
2321
2322 let parsed1 = parse_watch_file(text);
2323 let parsed2 = parsed1.clone();
2324
2325 assert_eq!(parsed1, parsed2);
2327
2328 let watchfile1 = parsed1.tree();
2330 let watchfile2 = watchfile1.clone();
2331 assert_eq!(watchfile1, watchfile2);
2332 }
2333
2334 #[test]
2335 fn test_parse_v4() {
2336 let cl: super::WatchFile = r#"version=4
2337opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2338 https://github.com/example/example-cat/tags \
2339 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2340"#
2341 .parse()
2342 .unwrap();
2343 assert_eq!(cl.version(), 4);
2344 let entries = cl.entries().collect::<Vec<_>>();
2345 assert_eq!(entries.len(), 1);
2346 let entry = &entries[0];
2347 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2348 assert_eq!(
2349 entry.matching_pattern(),
2350 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2351 );
2352 assert!(entry.repack());
2353 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2354 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2355 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2356 assert_eq!(entry.script(), Some("uupdate".into()));
2357 assert_eq!(
2358 entry.format_url(|| "example-cat".to_string(), || String::new()),
2359 "https://github.com/example/example-cat/tags"
2360 .parse()
2361 .unwrap()
2362 );
2363 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2364 }
2365
2366 #[test]
2367 fn test_git_mode() {
2368 let text = r#"version=3
2369opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2370https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2371refs/tags/(.*) debian
2372"#;
2373 let parsed = parse(text);
2374 assert_eq!(parsed.errors, Vec::<String>::new());
2375 let cl = parsed.root();
2376 assert_eq!(cl.version(), 3);
2377 let entries = cl.entries().collect::<Vec<_>>();
2378 assert_eq!(entries.len(), 1);
2379 let entry = &entries[0];
2380 assert_eq!(
2381 entry.url(),
2382 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2383 );
2384 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2385 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2386 assert_eq!(entry.script(), None);
2387 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2388 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2389 assert_eq!(entry.mode(), Ok(Mode::Git));
2390 }
2391
2392 #[test]
2393 fn test_parse_quoted() {
2394 const WATCHV1: &str = r#"version=4
2395opts="bare, filenamemangle=blah" \
2396 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2397"#;
2398 let parsed = parse(WATCHV1);
2399 let node = parsed.syntax();
2401
2402 let root = parsed.root();
2403 assert_eq!(root.version(), 4);
2404 let entries = root.entries().collect::<Vec<_>>();
2405 assert_eq!(entries.len(), 1);
2406 let entry = &entries[0];
2407
2408 assert_eq!(
2409 entry.url(),
2410 "https://github.com/syncthing/syncthing-gtk/tags"
2411 );
2412 assert_eq!(
2413 entry.matching_pattern(),
2414 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2415 );
2416 assert_eq!(entry.version(), Ok(None));
2417 assert_eq!(entry.script(), None);
2418
2419 assert_eq!(node.text(), WATCHV1);
2420 }
2421
2422 #[test]
2423 fn test_set_url() {
2424 let wf: super::WatchFile = r#"version=4
2426https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2427"#
2428 .parse()
2429 .unwrap();
2430
2431 let mut entry = wf.entries().next().unwrap();
2432 assert_eq!(
2433 entry.url(),
2434 "https://github.com/syncthing/syncthing-gtk/tags"
2435 );
2436
2437 entry.set_url("https://newurl.example.org/path");
2438 assert_eq!(entry.url(), "https://newurl.example.org/path");
2439 assert_eq!(
2440 entry.matching_pattern(),
2441 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2442 );
2443
2444 assert_eq!(
2446 entry.to_string(),
2447 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2448 );
2449 }
2450
2451 #[test]
2452 fn test_set_url_with_options() {
2453 let wf: super::WatchFile = r#"version=4
2455opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2456"#
2457 .parse()
2458 .unwrap();
2459
2460 let mut entry = wf.entries().next().unwrap();
2461 assert_eq!(entry.url(), "https://foo.com/bar");
2462 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2463
2464 entry.set_url("https://example.com/baz");
2465 assert_eq!(entry.url(), "https://example.com/baz");
2466
2467 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2469 assert_eq!(
2470 entry.matching_pattern(),
2471 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2472 );
2473
2474 assert_eq!(
2476 entry.to_string(),
2477 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2478 );
2479 }
2480
2481 #[test]
2482 fn test_set_url_complex() {
2483 let wf: super::WatchFile = r#"version=4
2485opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2486 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2487"#
2488 .parse()
2489 .unwrap();
2490
2491 let mut entry = wf.entries().next().unwrap();
2492 assert_eq!(
2493 entry.url(),
2494 "https://github.com/syncthing/syncthing-gtk/tags"
2495 );
2496
2497 entry.set_url("https://gitlab.com/newproject/tags");
2498 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2499
2500 assert!(entry.bare());
2502 assert_eq!(
2503 entry.filenamemangle(),
2504 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2505 );
2506 assert_eq!(
2507 entry.matching_pattern(),
2508 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2509 );
2510
2511 assert_eq!(
2513 entry.to_string(),
2514 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2515 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2516"#
2517 );
2518 }
2519
2520 #[test]
2521 fn test_set_url_with_all_fields() {
2522 let wf: super::WatchFile = r#"version=4
2524opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2525 https://github.com/example/example-cat/tags \
2526 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2527"#
2528 .parse()
2529 .unwrap();
2530
2531 let mut entry = wf.entries().next().unwrap();
2532 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2533 assert_eq!(
2534 entry.matching_pattern(),
2535 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2536 );
2537 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2538 assert_eq!(entry.script(), Some("uupdate".into()));
2539
2540 entry.set_url("https://gitlab.example.org/project/releases");
2541 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2542
2543 assert!(entry.repack());
2545 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2546 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2547 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2548 assert_eq!(
2549 entry.matching_pattern(),
2550 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2551 );
2552 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2553 assert_eq!(entry.script(), Some("uupdate".into()));
2554
2555 assert_eq!(
2557 entry.to_string(),
2558 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2559 https://gitlab.example.org/project/releases \
2560 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2561"#
2562 );
2563 }
2564
2565 #[test]
2566 fn test_set_url_quoted_options() {
2567 let wf: super::WatchFile = r#"version=4
2569opts="bare, filenamemangle=blah" \
2570 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2571"#
2572 .parse()
2573 .unwrap();
2574
2575 let mut entry = wf.entries().next().unwrap();
2576 assert_eq!(
2577 entry.url(),
2578 "https://github.com/syncthing/syncthing-gtk/tags"
2579 );
2580
2581 entry.set_url("https://example.org/new/path");
2582 assert_eq!(entry.url(), "https://example.org/new/path");
2583
2584 assert_eq!(
2586 entry.to_string(),
2587 r#"opts="bare, filenamemangle=blah" \
2588 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2589"#
2590 );
2591 }
2592
2593 #[test]
2594 fn test_set_opt_update_existing() {
2595 let wf: super::WatchFile = r#"version=4
2597opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2598"#
2599 .parse()
2600 .unwrap();
2601
2602 let mut entry = wf.entries().next().unwrap();
2603 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2604 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2605
2606 entry.set_opt("foo", "updated");
2607 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2608 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2609
2610 assert_eq!(
2612 entry.to_string(),
2613 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2614 );
2615 }
2616
2617 #[test]
2618 fn test_set_opt_add_new() {
2619 let wf: super::WatchFile = r#"version=4
2621opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2622"#
2623 .parse()
2624 .unwrap();
2625
2626 let mut entry = wf.entries().next().unwrap();
2627 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2628 assert_eq!(entry.get_option("bar"), None);
2629
2630 entry.set_opt("bar", "baz");
2631 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2632 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2633
2634 assert_eq!(
2636 entry.to_string(),
2637 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2638 );
2639 }
2640
2641 #[test]
2642 fn test_set_opt_create_options_list() {
2643 let wf: super::WatchFile = r#"version=4
2645https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2646"#
2647 .parse()
2648 .unwrap();
2649
2650 let mut entry = wf.entries().next().unwrap();
2651 assert_eq!(entry.option_list(), None);
2652
2653 entry.set_opt("compression", "xz");
2654 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2655
2656 assert_eq!(
2658 entry.to_string(),
2659 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2660 );
2661 }
2662
2663 #[test]
2664 fn test_del_opt_remove_single() {
2665 let wf: super::WatchFile = r#"version=4
2667opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2668"#
2669 .parse()
2670 .unwrap();
2671
2672 let mut entry = wf.entries().next().unwrap();
2673 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2674 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2675 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2676
2677 entry.del_opt_str("bar");
2678 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2679 assert_eq!(entry.get_option("bar"), None);
2680 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2681
2682 assert_eq!(
2684 entry.to_string(),
2685 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2686 );
2687 }
2688
2689 #[test]
2690 fn test_del_opt_remove_first() {
2691 let wf: super::WatchFile = r#"version=4
2693opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2694"#
2695 .parse()
2696 .unwrap();
2697
2698 let mut entry = wf.entries().next().unwrap();
2699 entry.del_opt_str("foo");
2700 assert_eq!(entry.get_option("foo"), None);
2701 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2702
2703 assert_eq!(
2705 entry.to_string(),
2706 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2707 );
2708 }
2709
2710 #[test]
2711 fn test_del_opt_remove_last() {
2712 let wf: super::WatchFile = r#"version=4
2714opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2715"#
2716 .parse()
2717 .unwrap();
2718
2719 let mut entry = wf.entries().next().unwrap();
2720 entry.del_opt_str("bar");
2721 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2722 assert_eq!(entry.get_option("bar"), None);
2723
2724 assert_eq!(
2726 entry.to_string(),
2727 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2728 );
2729 }
2730
2731 #[test]
2732 fn test_del_opt_remove_only_option() {
2733 let wf: super::WatchFile = r#"version=4
2735opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2736"#
2737 .parse()
2738 .unwrap();
2739
2740 let mut entry = wf.entries().next().unwrap();
2741 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2742
2743 entry.del_opt_str("foo");
2744 assert_eq!(entry.get_option("foo"), None);
2745 assert_eq!(entry.option_list(), None);
2746
2747 assert_eq!(
2749 entry.to_string(),
2750 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2751 );
2752 }
2753
2754 #[test]
2755 fn test_del_opt_nonexistent() {
2756 let wf: super::WatchFile = r#"version=4
2758opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2759"#
2760 .parse()
2761 .unwrap();
2762
2763 let mut entry = wf.entries().next().unwrap();
2764 let original = entry.to_string();
2765
2766 entry.del_opt_str("nonexistent");
2767 assert_eq!(entry.to_string(), original);
2768 }
2769
2770 #[test]
2771 fn test_set_opt_multiple_operations() {
2772 let wf: super::WatchFile = r#"version=4
2774https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2775"#
2776 .parse()
2777 .unwrap();
2778
2779 let mut entry = wf.entries().next().unwrap();
2780
2781 entry.set_opt("compression", "xz");
2782 entry.set_opt("repack", "");
2783 entry.set_opt("dversionmangle", "s/\\+ds//");
2784
2785 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2786 assert_eq!(
2787 entry.get_option("dversionmangle"),
2788 Some("s/\\+ds//".to_string())
2789 );
2790 }
2791
2792 #[test]
2793 fn test_set_matching_pattern() {
2794 let wf: super::WatchFile = r#"version=4
2796https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2797"#
2798 .parse()
2799 .unwrap();
2800
2801 let mut entry = wf.entries().next().unwrap();
2802 assert_eq!(
2803 entry.matching_pattern(),
2804 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2805 );
2806
2807 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2808 assert_eq!(
2809 entry.matching_pattern(),
2810 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2811 );
2812
2813 assert_eq!(entry.url(), "https://github.com/example/tags");
2815
2816 assert_eq!(
2818 entry.to_string(),
2819 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2820 );
2821 }
2822
2823 #[test]
2824 fn test_set_matching_pattern_with_all_fields() {
2825 let wf: super::WatchFile = r#"version=4
2827opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2828"#
2829 .parse()
2830 .unwrap();
2831
2832 let mut entry = wf.entries().next().unwrap();
2833 assert_eq!(
2834 entry.matching_pattern(),
2835 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2836 );
2837
2838 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2839 assert_eq!(
2840 entry.matching_pattern(),
2841 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2842 );
2843
2844 assert_eq!(entry.url(), "https://example.com/releases");
2846 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2847 assert_eq!(entry.script(), Some("uupdate".into()));
2848 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2849
2850 assert_eq!(
2852 entry.to_string(),
2853 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2854 );
2855 }
2856
2857 #[test]
2858 fn test_set_version_policy() {
2859 let wf: super::WatchFile = r#"version=4
2861https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2862"#
2863 .parse()
2864 .unwrap();
2865
2866 let mut entry = wf.entries().next().unwrap();
2867 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2868
2869 entry.set_version_policy("previous");
2870 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2871
2872 assert_eq!(entry.url(), "https://example.com/releases");
2874 assert_eq!(
2875 entry.matching_pattern(),
2876 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2877 );
2878 assert_eq!(entry.script(), Some("uupdate".into()));
2879
2880 assert_eq!(
2882 entry.to_string(),
2883 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2884 );
2885 }
2886
2887 #[test]
2888 fn test_set_version_policy_with_options() {
2889 let wf: super::WatchFile = r#"version=4
2891opts=repack,compression=xz \
2892 https://github.com/example/example-cat/tags \
2893 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2894"#
2895 .parse()
2896 .unwrap();
2897
2898 let mut entry = wf.entries().next().unwrap();
2899 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2900
2901 entry.set_version_policy("ignore");
2902 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2903
2904 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2906 assert_eq!(
2907 entry.matching_pattern(),
2908 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2909 );
2910 assert_eq!(entry.script(), Some("uupdate".into()));
2911 assert!(entry.repack());
2912
2913 assert_eq!(
2915 entry.to_string(),
2916 r#"opts=repack,compression=xz \
2917 https://github.com/example/example-cat/tags \
2918 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2919"#
2920 );
2921 }
2922
2923 #[test]
2924 fn test_set_script() {
2925 let wf: super::WatchFile = r#"version=4
2927https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2928"#
2929 .parse()
2930 .unwrap();
2931
2932 let mut entry = wf.entries().next().unwrap();
2933 assert_eq!(entry.script(), Some("uupdate".into()));
2934
2935 entry.set_script("uscan");
2936 assert_eq!(entry.script(), Some("uscan".into()));
2937
2938 assert_eq!(entry.url(), "https://example.com/releases");
2940 assert_eq!(
2941 entry.matching_pattern(),
2942 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2943 );
2944 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2945
2946 assert_eq!(
2948 entry.to_string(),
2949 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2950 );
2951 }
2952
2953 #[test]
2954 fn test_set_script_with_options() {
2955 let wf: super::WatchFile = r#"version=4
2957opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2958"#
2959 .parse()
2960 .unwrap();
2961
2962 let mut entry = wf.entries().next().unwrap();
2963 assert_eq!(entry.script(), Some("uupdate".into()));
2964
2965 entry.set_script("custom-script.sh");
2966 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2967
2968 assert_eq!(entry.url(), "https://example.com/releases");
2970 assert_eq!(
2971 entry.matching_pattern(),
2972 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2973 );
2974 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2975 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2976
2977 assert_eq!(
2979 entry.to_string(),
2980 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2981 );
2982 }
2983
2984 #[test]
2985 fn test_apply_dversionmangle() {
2986 let wf: super::WatchFile = r#"version=4
2988opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2989"#
2990 .parse()
2991 .unwrap();
2992 let entry = wf.entries().next().unwrap();
2993 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2994 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2995
2996 let wf: super::WatchFile = r#"version=4
2998opts=versionmangle=s/^v// https://example.com/ .*
2999"#
3000 .parse()
3001 .unwrap();
3002 let entry = wf.entries().next().unwrap();
3003 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
3004
3005 let wf: super::WatchFile = r#"version=4
3007opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
3008"#
3009 .parse()
3010 .unwrap();
3011 let entry = wf.entries().next().unwrap();
3012 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
3013
3014 let wf: super::WatchFile = r#"version=4
3016https://example.com/ .*
3017"#
3018 .parse()
3019 .unwrap();
3020 let entry = wf.entries().next().unwrap();
3021 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
3022 }
3023
3024 #[test]
3025 fn test_apply_oversionmangle() {
3026 let wf: super::WatchFile = r#"version=4
3028opts=oversionmangle=s/$/-1/ https://example.com/ .*
3029"#
3030 .parse()
3031 .unwrap();
3032 let entry = wf.entries().next().unwrap();
3033 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
3034 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
3035
3036 let wf: super::WatchFile = r#"version=4
3038opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
3039"#
3040 .parse()
3041 .unwrap();
3042 let entry = wf.entries().next().unwrap();
3043 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
3044
3045 let wf: super::WatchFile = r#"version=4
3047https://example.com/ .*
3048"#
3049 .parse()
3050 .unwrap();
3051 let entry = wf.entries().next().unwrap();
3052 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
3053 }
3054
3055 #[test]
3056 fn test_apply_dirversionmangle() {
3057 let wf: super::WatchFile = r#"version=4
3059opts=dirversionmangle=s/^v// https://example.com/ .*
3060"#
3061 .parse()
3062 .unwrap();
3063 let entry = wf.entries().next().unwrap();
3064 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3065 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
3066
3067 let wf: super::WatchFile = r#"version=4
3069opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
3070"#
3071 .parse()
3072 .unwrap();
3073 let entry = wf.entries().next().unwrap();
3074 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3075
3076 let wf: super::WatchFile = r#"version=4
3078https://example.com/ .*
3079"#
3080 .parse()
3081 .unwrap();
3082 let entry = wf.entries().next().unwrap();
3083 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
3084 }
3085
3086 #[test]
3087 fn test_apply_filenamemangle() {
3088 let wf: super::WatchFile = r#"version=4
3090opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
3091"#
3092 .parse()
3093 .unwrap();
3094 let entry = wf.entries().next().unwrap();
3095 assert_eq!(
3096 entry
3097 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
3098 .unwrap(),
3099 "mypackage-1.0.tar.gz"
3100 );
3101 assert_eq!(
3102 entry
3103 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
3104 .unwrap(),
3105 "mypackage-2.5.3.tar.gz"
3106 );
3107
3108 let wf: super::WatchFile = r#"version=4
3110opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
3111"#
3112 .parse()
3113 .unwrap();
3114 let entry = wf.entries().next().unwrap();
3115 assert_eq!(
3116 entry
3117 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
3118 .unwrap(),
3119 "file.tar.gz"
3120 );
3121
3122 let wf: super::WatchFile = r#"version=4
3124https://example.com/ .*
3125"#
3126 .parse()
3127 .unwrap();
3128 let entry = wf.entries().next().unwrap();
3129 assert_eq!(
3130 entry
3131 .apply_filenamemangle("https://example.com/file.tar.gz")
3132 .unwrap(),
3133 "https://example.com/file.tar.gz"
3134 );
3135 }
3136
3137 #[test]
3138 fn test_apply_pagemangle() {
3139 let wf: super::WatchFile = r#"version=4
3141opts=pagemangle=s/&/&/g https://example.com/ .*
3142"#
3143 .parse()
3144 .unwrap();
3145 let entry = wf.entries().next().unwrap();
3146 assert_eq!(
3147 entry.apply_pagemangle(b"foo & bar").unwrap(),
3148 b"foo & bar"
3149 );
3150 assert_eq!(
3151 entry
3152 .apply_pagemangle(b"& foo & bar &")
3153 .unwrap(),
3154 b"& foo & bar &"
3155 );
3156
3157 let wf: super::WatchFile = r#"version=4
3159opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3160"#
3161 .parse()
3162 .unwrap();
3163 let entry = wf.entries().next().unwrap();
3164 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3165
3166 let wf: super::WatchFile = r#"version=4
3168https://example.com/ .*
3169"#
3170 .parse()
3171 .unwrap();
3172 let entry = wf.entries().next().unwrap();
3173 assert_eq!(
3174 entry.apply_pagemangle(b"foo & bar").unwrap(),
3175 b"foo & bar"
3176 );
3177 }
3178
3179 #[test]
3180 fn test_apply_downloadurlmangle() {
3181 let wf: super::WatchFile = r#"version=4
3183opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3184"#
3185 .parse()
3186 .unwrap();
3187 let entry = wf.entries().next().unwrap();
3188 assert_eq!(
3189 entry
3190 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3191 .unwrap(),
3192 "https://example.com/download/file.tar.gz"
3193 );
3194
3195 let wf: super::WatchFile = r#"version=4
3197opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3198"#
3199 .parse()
3200 .unwrap();
3201 let entry = wf.entries().next().unwrap();
3202 assert_eq!(
3203 entry
3204 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3205 .unwrap(),
3206 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3207 );
3208
3209 let wf: super::WatchFile = r#"version=4
3211https://example.com/ .*
3212"#
3213 .parse()
3214 .unwrap();
3215 let entry = wf.entries().next().unwrap();
3216 assert_eq!(
3217 entry
3218 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3219 .unwrap(),
3220 "https://example.com/archive/file.tar.gz"
3221 );
3222 }
3223
3224 #[test]
3225 fn test_entry_builder_minimal() {
3226 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3228 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3229 .build();
3230
3231 assert_eq!(entry.url(), "https://github.com/example/tags");
3232 assert_eq!(
3233 entry.matching_pattern().as_deref(),
3234 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3235 );
3236 assert_eq!(entry.version(), Ok(None));
3237 assert_eq!(entry.script(), None);
3238 assert!(entry.opts().is_empty());
3239 }
3240
3241 #[test]
3242 fn test_entry_builder_url_only() {
3243 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3245
3246 assert_eq!(entry.url(), "https://example.com/releases");
3247 assert_eq!(entry.matching_pattern(), None);
3248 assert_eq!(entry.version(), Ok(None));
3249 assert_eq!(entry.script(), None);
3250 assert!(entry.opts().is_empty());
3251 }
3252
3253 #[test]
3254 fn test_entry_builder_with_all_fields() {
3255 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3257 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3258 .version_policy("debian")
3259 .script("uupdate")
3260 .opt("compression", "xz")
3261 .flag("repack")
3262 .build();
3263
3264 assert_eq!(entry.url(), "https://github.com/example/tags");
3265 assert_eq!(
3266 entry.matching_pattern().as_deref(),
3267 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3268 );
3269 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3270 assert_eq!(entry.script(), Some("uupdate".into()));
3271 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3272 assert!(entry.has_option("repack"));
3273 assert!(entry.repack());
3274 }
3275
3276 #[test]
3277 fn test_entry_builder_multiple_options() {
3278 let entry = super::EntryBuilder::new("https://example.com/tags")
3280 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3281 .opt("compression", "xz")
3282 .opt("dversionmangle", "s/\\+ds//")
3283 .opt("repacksuffix", "+ds")
3284 .build();
3285
3286 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3287 assert_eq!(
3288 entry.get_option("dversionmangle"),
3289 Some("s/\\+ds//".to_string())
3290 );
3291 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3292 }
3293
3294 #[test]
3295 fn test_entry_builder_via_entry() {
3296 let entry = super::Entry::builder("https://github.com/example/tags")
3298 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3299 .version_policy("debian")
3300 .build();
3301
3302 assert_eq!(entry.url(), "https://github.com/example/tags");
3303 assert_eq!(
3304 entry.matching_pattern().as_deref(),
3305 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3306 );
3307 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3308 }
3309
3310 #[test]
3311 fn test_watchfile_add_entry_to_empty() {
3312 let mut wf = super::WatchFile::new(Some(4));
3314
3315 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3316 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3317 .build();
3318
3319 wf.add_entry(entry);
3320
3321 assert_eq!(wf.version(), 4);
3322 assert_eq!(wf.entries().count(), 1);
3323
3324 let added_entry = wf.entries().next().unwrap();
3325 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3326 assert_eq!(
3327 added_entry.matching_pattern().as_deref(),
3328 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3329 );
3330 }
3331
3332 #[test]
3333 fn test_watchfile_add_multiple_entries() {
3334 let mut wf = super::WatchFile::new(Some(4));
3336
3337 wf.add_entry(
3338 super::EntryBuilder::new("https://github.com/example1/tags")
3339 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3340 .build(),
3341 );
3342
3343 wf.add_entry(
3344 super::EntryBuilder::new("https://github.com/example2/releases")
3345 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3346 .opt("compression", "xz")
3347 .build(),
3348 );
3349
3350 assert_eq!(wf.entries().count(), 2);
3351
3352 let entries: Vec<_> = wf.entries().collect();
3353 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3354 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3355 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3356 }
3357
3358 #[test]
3359 fn test_watchfile_add_entry_to_existing() {
3360 let mut wf: super::WatchFile = r#"version=4
3362https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3363"#
3364 .parse()
3365 .unwrap();
3366
3367 assert_eq!(wf.entries().count(), 1);
3368
3369 wf.add_entry(
3370 super::EntryBuilder::new("https://github.com/example/new")
3371 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3372 .opt("compression", "xz")
3373 .version_policy("debian")
3374 .build(),
3375 );
3376
3377 assert_eq!(wf.entries().count(), 2);
3378
3379 let entries: Vec<_> = wf.entries().collect();
3380 assert_eq!(entries[0].url(), "https://example.com/old");
3381 assert_eq!(entries[1].url(), "https://github.com/example/new");
3382 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3383 }
3384
3385 #[test]
3386 fn test_entry_builder_formatting() {
3387 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3389 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3390 .opt("compression", "xz")
3391 .flag("repack")
3392 .version_policy("debian")
3393 .script("uupdate")
3394 .build();
3395
3396 let entry_str = entry.to_string();
3397
3398 assert!(entry_str.starts_with("opts="));
3400 assert!(entry_str.contains("https://github.com/example/tags"));
3402 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3404 assert!(entry_str.contains("debian"));
3406 assert!(entry_str.contains("uupdate"));
3408 assert!(entry_str.ends_with('\n'));
3410 }
3411
3412 #[test]
3413 fn test_watchfile_add_entry_preserves_format() {
3414 let mut wf = super::WatchFile::new(Some(4));
3416
3417 wf.add_entry(
3418 super::EntryBuilder::new("https://github.com/example/tags")
3419 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3420 .build(),
3421 );
3422
3423 let wf_str = wf.to_string();
3424
3425 assert!(wf_str.starts_with("version=4\n"));
3427 assert!(wf_str.contains("https://github.com/example/tags"));
3429
3430 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3432 assert_eq!(reparsed.version(), 4);
3433 assert_eq!(reparsed.entries().count(), 1);
3434 }
3435
3436 #[test]
3437 fn test_line_col() {
3438 let text = r#"version=4
3439opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3440"#;
3441 let wf = text.parse::<super::WatchFile>().unwrap();
3442
3443 let version_node = wf.version_node().unwrap();
3445 assert_eq!(version_node.line(), 0);
3446 assert_eq!(version_node.column(), 0);
3447 assert_eq!(version_node.line_col(), (0, 0));
3448
3449 let entries: Vec<_> = wf.entries().collect();
3451 assert_eq!(entries.len(), 1);
3452
3453 assert_eq!(entries[0].line(), 1);
3455 assert_eq!(entries[0].column(), 0);
3456 assert_eq!(entries[0].line_col(), (1, 0));
3457
3458 let option_list = entries[0].option_list().unwrap();
3460 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3463 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3466 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3469 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3472 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3476 assert_eq!(options.len(), 1);
3477 assert_eq!(options[0].key(), Some("compression".to_string()));
3478 assert_eq!(options[0].value(), Some("xz".to_string()));
3479 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3483 assert_eq!(compression_opt.line(), 1);
3484 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3486 }
3487
3488 #[test]
3489 fn test_parse_str_relaxed() {
3490 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3491 r#"version=4
3492ERRORS IN THIS LINE
3493opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3494"#,
3495 );
3496 assert_eq!(wf.version(), 4);
3497 assert_eq!(wf.entries().count(), 2);
3498
3499 let entries = wf.entries().collect::<Vec<_>>();
3500
3501 let entry = &entries[0];
3502 assert_eq!(entry.url(), "ERRORS");
3503
3504 let entry = &entries[1];
3505 assert_eq!(entry.url(), "https://example.com/releases");
3506 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3507 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3508 }
3509
3510 #[test]
3511 fn test_parse_entry_with_comment_before() {
3512 let input = concat!(
3516 "version=4\n",
3517 "# try also https://pypi.debian.net/tomoscan/watch\n",
3518 "opts=uversionmangle=s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/ \\\n",
3519 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))\n"
3520 );
3521 let wf: super::WatchFile = input.parse().unwrap();
3522 assert_eq!(wf.to_string(), input);
3524 assert_eq!(wf.entries().count(), 1);
3525 let entry = wf.entries().next().unwrap();
3526 assert_eq!(
3527 entry.url(),
3528 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))"
3529 );
3530 assert_eq!(
3531 entry.get_option("uversionmangle"),
3532 Some("s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/".to_string())
3533 );
3534 }
3535
3536 #[test]
3537 fn test_parse_multiple_comments_before_entry() {
3538 let input = concat!(
3541 "version=4\n",
3542 "# first comment\n",
3543 "# second comment\n",
3544 "# third comment\n",
3545 "https://example.com/foo foo-(.*).tar.gz\n",
3546 );
3547 let wf: super::WatchFile = input.parse().unwrap();
3548 assert_eq!(wf.to_string(), input);
3549 assert_eq!(wf.entries().count(), 1);
3550 assert_eq!(
3551 wf.entries().next().unwrap().url(),
3552 "https://example.com/foo"
3553 );
3554 }
3555
3556 #[test]
3557 fn test_parse_blank_lines_between_entries() {
3558 let input = concat!(
3560 "version=4\n",
3561 "https://example.com/foo .*/foo-(\\d+)\\.tar\\.gz\n",
3562 "\n",
3563 "https://example.com/bar .*/bar-(\\d+)\\.tar\\.gz\n",
3564 );
3565 let wf: super::WatchFile = input.parse().unwrap();
3566 assert_eq!(wf.to_string(), input);
3567 assert_eq!(wf.entries().count(), 2);
3568 }
3569
3570 #[test]
3571 fn test_parse_trailing_unparseable_tokens_produce_error() {
3572 let input = "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n=garbage\n";
3575 let result = input.parse::<super::WatchFile>();
3576 assert!(result.is_err(), "expected parse error for trailing garbage");
3577 let wf = super::WatchFile::from_str_relaxed(input);
3579 assert_eq!(wf.to_string(), input);
3580 }
3581
3582 #[test]
3583 fn test_parse_roundtrip_full_file() {
3584 let inputs = [
3586 "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n",
3587 "version=4\n# a comment\nhttps://example.com/foo foo-(.*).tar.gz\n",
3588 concat!(
3589 "version=4\n",
3590 "opts=uversionmangle=s/rc/~rc/ \\\n",
3591 " https://example.com/foo foo-(.*).tar.gz\n",
3592 ),
3593 concat!(
3594 "version=4\n",
3595 "# comment before entry\n",
3596 "opts=uversionmangle=s/rc/~rc/ \\\n",
3597 "https://example.com/foo foo-(.*).tar.gz\n",
3598 "# comment between entries\n",
3599 "https://example.com/bar bar-(.*).tar.gz\n",
3600 ),
3601 ];
3602 for input in &inputs {
3603 let wf: super::WatchFile = input.parse().unwrap();
3604 assert_eq!(
3605 wf.to_string(),
3606 *input,
3607 "round-trip failed for input: {:?}",
3608 input
3609 );
3610 }
3611 }
3612}