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 #[cfg(feature = "deb822")]
1028 pub(crate) fn syntax(&self) -> &SyntaxNode {
1029 &self.0
1030 }
1031
1032 pub fn builder(url: impl Into<String>) -> EntryBuilder {
1046 EntryBuilder::new(url)
1047 }
1048
1049 pub fn option_list(&self) -> Option<OptionList> {
1051 self.0.children().find_map(OptionList::cast)
1052 }
1053
1054 pub fn get_option(&self, key: &str) -> Option<String> {
1056 self.option_list().and_then(|ol| ol.get_option(key))
1057 }
1058
1059 pub fn has_option(&self, key: &str) -> bool {
1061 self.option_list().is_some_and(|ol| ol.has_option(key))
1062 }
1063
1064 pub fn component(&self) -> Option<String> {
1066 self.get_option("component")
1067 }
1068
1069 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
1071 self.try_ctype().map_err(|_| ())
1072 }
1073
1074 pub fn try_ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
1076 self.get_option("ctype").map(|s| s.parse()).transpose()
1077 }
1078
1079 pub fn compression(&self) -> Result<Option<Compression>, ()> {
1081 self.try_compression().map_err(|_| ())
1082 }
1083
1084 pub fn try_compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
1086 self.get_option("compression")
1087 .map(|s| s.parse())
1088 .transpose()
1089 }
1090
1091 pub fn repack(&self) -> bool {
1093 self.has_option("repack")
1094 }
1095
1096 pub fn repacksuffix(&self) -> Option<String> {
1098 self.get_option("repacksuffix")
1099 }
1100
1101 pub fn mode(&self) -> Result<Mode, ()> {
1103 self.try_mode().map_err(|_| ())
1104 }
1105
1106 pub fn try_mode(&self) -> Result<Mode, crate::types::ParseError> {
1108 Ok(self
1109 .get_option("mode")
1110 .map(|s| s.parse())
1111 .transpose()?
1112 .unwrap_or_default())
1113 }
1114
1115 pub fn pretty(&self) -> Result<Pretty, ()> {
1117 self.try_pretty().map_err(|_| ())
1118 }
1119
1120 pub fn try_pretty(&self) -> Result<Pretty, crate::types::ParseError> {
1122 Ok(self
1123 .get_option("pretty")
1124 .map(|s| s.parse())
1125 .transpose()?
1126 .unwrap_or_default())
1127 }
1128
1129 pub fn date(&self) -> String {
1132 self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
1133 }
1134
1135 pub fn gitexport(&self) -> Result<GitExport, ()> {
1137 self.try_gitexport().map_err(|_| ())
1138 }
1139
1140 pub fn try_gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
1142 Ok(self
1143 .get_option("gitexport")
1144 .map(|s| s.parse())
1145 .transpose()?
1146 .unwrap_or_default())
1147 }
1148
1149 pub fn gitmode(&self) -> Result<GitMode, ()> {
1151 self.try_gitmode().map_err(|_| ())
1152 }
1153
1154 pub fn try_gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
1156 Ok(self
1157 .get_option("gitmode")
1158 .map(|s| s.parse())
1159 .transpose()?
1160 .unwrap_or_default())
1161 }
1162
1163 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
1165 self.try_pgpmode().map_err(|_| ())
1166 }
1167
1168 pub fn try_pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
1170 Ok(self
1171 .get_option("pgpmode")
1172 .map(|s| s.parse())
1173 .transpose()?
1174 .unwrap_or_default())
1175 }
1176
1177 pub fn searchmode(&self) -> Result<SearchMode, ()> {
1179 self.try_searchmode().map_err(|_| ())
1180 }
1181
1182 pub fn try_searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
1184 Ok(self
1185 .get_option("searchmode")
1186 .map(|s| s.parse())
1187 .transpose()?
1188 .unwrap_or_default())
1189 }
1190
1191 pub fn decompress(&self) -> bool {
1193 self.has_option("decompress")
1194 }
1195
1196 pub fn bare(&self) -> bool {
1199 self.has_option("bare")
1200 }
1201
1202 pub fn user_agent(&self) -> Option<String> {
1204 self.get_option("user-agent")
1205 }
1206
1207 pub fn passive(&self) -> Option<bool> {
1209 if self.has_option("passive") || self.has_option("pasv") {
1210 Some(true)
1211 } else if self.has_option("active") || self.has_option("nopasv") {
1212 Some(false)
1213 } else {
1214 None
1215 }
1216 }
1217
1218 pub fn unzipoptions(&self) -> Option<String> {
1221 self.get_option("unzipopt")
1222 }
1223
1224 pub fn dversionmangle(&self) -> Option<String> {
1226 self.get_option("dversionmangle")
1227 .or_else(|| self.get_option("versionmangle"))
1228 }
1229
1230 pub fn dirversionmangle(&self) -> Option<String> {
1234 self.get_option("dirversionmangle")
1235 }
1236
1237 pub fn pagemangle(&self) -> Option<String> {
1239 self.get_option("pagemangle")
1240 }
1241
1242 pub fn uversionmangle(&self) -> Option<String> {
1246 self.get_option("uversionmangle")
1247 .or_else(|| self.get_option("versionmangle"))
1248 }
1249
1250 pub fn versionmangle(&self) -> Option<String> {
1252 self.get_option("versionmangle")
1253 }
1254
1255 pub fn hrefdecode(&self) -> bool {
1260 self.get_option("hrefdecode").is_some()
1261 }
1262
1263 pub fn downloadurlmangle(&self) -> Option<String> {
1266 self.get_option("downloadurlmangle")
1267 }
1268
1269 pub fn filenamemangle(&self) -> Option<String> {
1277 self.get_option("filenamemangle")
1278 }
1279
1280 pub fn pgpsigurlmangle(&self) -> Option<String> {
1282 self.get_option("pgpsigurlmangle")
1283 }
1284
1285 pub fn oversionmangle(&self) -> Option<String> {
1288 self.get_option("oversionmangle")
1289 }
1290
1291 pub fn apply_uversionmangle(
1304 &self,
1305 version: &str,
1306 ) -> Result<String, crate::mangle::MangleError> {
1307 if let Some(vm) = self.uversionmangle() {
1308 crate::mangle::apply_mangle(&vm, version)
1309 } else {
1310 Ok(version.to_string())
1311 }
1312 }
1313
1314 pub fn apply_dversionmangle(
1327 &self,
1328 version: &str,
1329 ) -> Result<String, crate::mangle::MangleError> {
1330 if let Some(vm) = self.dversionmangle() {
1331 crate::mangle::apply_mangle(&vm, version)
1332 } else {
1333 Ok(version.to_string())
1334 }
1335 }
1336
1337 pub fn apply_oversionmangle(
1350 &self,
1351 version: &str,
1352 ) -> Result<String, crate::mangle::MangleError> {
1353 if let Some(vm) = self.oversionmangle() {
1354 crate::mangle::apply_mangle(&vm, version)
1355 } else {
1356 Ok(version.to_string())
1357 }
1358 }
1359
1360 pub fn apply_dirversionmangle(
1373 &self,
1374 version: &str,
1375 ) -> Result<String, crate::mangle::MangleError> {
1376 if let Some(vm) = self.dirversionmangle() {
1377 crate::mangle::apply_mangle(&vm, version)
1378 } else {
1379 Ok(version.to_string())
1380 }
1381 }
1382
1383 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1399 if let Some(vm) = self.filenamemangle() {
1400 crate::mangle::apply_mangle(&vm, url)
1401 } else {
1402 Ok(url.to_string())
1403 }
1404 }
1405
1406 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1422 if let Some(vm) = self.pagemangle() {
1423 let page_str = String::from_utf8_lossy(page);
1424 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1425 Ok(mangled.into_bytes())
1426 } else {
1427 Ok(page.to_vec())
1428 }
1429 }
1430
1431 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1447 if let Some(vm) = self.downloadurlmangle() {
1448 crate::mangle::apply_mangle(&vm, url)
1449 } else {
1450 Ok(url.to_string())
1451 }
1452 }
1453
1454 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1456 let mut options = std::collections::HashMap::new();
1457
1458 if let Some(ol) = self.option_list() {
1459 for opt in ol.options() {
1460 let key = opt.key();
1461 let value = opt.value();
1462 if let (Some(key), Some(value)) = (key, value) {
1463 options.insert(key.to_string(), value.to_string());
1464 }
1465 }
1466 }
1467
1468 options
1469 }
1470
1471 fn items(&self) -> impl Iterator<Item = String> + '_ {
1472 self.0.children_with_tokens().filter_map(|it| match it {
1473 SyntaxElement::Token(token) => {
1474 if token.kind() == VALUE || token.kind() == KEY {
1475 Some(token.text().to_string())
1476 } else {
1477 None
1478 }
1479 }
1480 SyntaxElement::Node(node) => {
1481 match node.kind() {
1483 URL => Url::cast(node).map(|n| n.url()),
1484 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1485 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1486 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1487 _ => None,
1488 }
1489 }
1490 })
1491 }
1492
1493 pub fn url_node(&self) -> Option<Url> {
1495 self.0.children().find_map(Url::cast)
1496 }
1497
1498 pub fn url(&self) -> String {
1500 self.url_node().map(|it| it.url()).unwrap_or_else(|| {
1501 self.items().next().unwrap()
1503 })
1504 }
1505
1506 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1508 self.0.children().find_map(MatchingPattern::cast)
1509 }
1510
1511 pub fn matching_pattern(&self) -> Option<String> {
1513 self.matching_pattern_node()
1514 .map(|it| it.pattern())
1515 .or_else(|| {
1516 self.items().nth(1)
1518 })
1519 }
1520
1521 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1523 self.0.children().find_map(VersionPolicyNode::cast)
1524 }
1525
1526 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1528 self.version_node()
1529 .map(|it| it.policy().parse())
1530 .transpose()
1531 .map_err(|e: crate::types::ParseError| e.to_string())
1532 .or_else(|_e| {
1533 self.items()
1535 .nth(2)
1536 .map(|it| it.parse())
1537 .transpose()
1538 .map_err(|e: crate::types::ParseError| e.to_string())
1539 })
1540 }
1541
1542 pub fn script_node(&self) -> Option<ScriptNode> {
1544 self.0.children().find_map(ScriptNode::cast)
1545 }
1546
1547 pub fn script(&self) -> Option<String> {
1549 self.script_node().map(|it| it.script()).or_else(|| {
1550 self.items().nth(3)
1552 })
1553 }
1554
1555 pub fn format_url(
1557 &self,
1558 package: impl FnOnce() -> String,
1559 component: impl FnOnce() -> String,
1560 ) -> url::Url {
1561 crate::subst::subst(self.url().as_str(), package, component)
1562 .parse()
1563 .unwrap()
1564 }
1565
1566 pub fn set_url(&mut self, new_url: &str) {
1568 let mut builder = GreenNodeBuilder::new();
1570 builder.start_node(URL.into());
1571 builder.token(VALUE.into(), new_url);
1572 builder.finish_node();
1573 let new_url_green = builder.finish();
1574
1575 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1577
1578 let url_pos = self
1580 .0
1581 .children_with_tokens()
1582 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1583
1584 if let Some(pos) = url_pos {
1585 self.0
1587 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1588 }
1589 }
1590
1591 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1597 let mut builder = GreenNodeBuilder::new();
1599 builder.start_node(MATCHING_PATTERN.into());
1600 builder.token(VALUE.into(), new_pattern);
1601 builder.finish_node();
1602 let new_pattern_green = builder.finish();
1603
1604 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1606
1607 let pattern_pos = self.0.children_with_tokens().position(
1609 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1610 );
1611
1612 if let Some(pos) = pattern_pos {
1613 self.0
1615 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1616 }
1617 }
1619
1620 pub fn set_version_policy(&mut self, new_policy: &str) {
1626 let mut builder = GreenNodeBuilder::new();
1628 builder.start_node(VERSION_POLICY.into());
1629 builder.token(VALUE.into(), new_policy);
1631 builder.finish_node();
1632 let new_policy_green = builder.finish();
1633
1634 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1636
1637 let policy_pos = self.0.children_with_tokens().position(
1639 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1640 );
1641
1642 if let Some(pos) = policy_pos {
1643 self.0
1645 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1646 }
1647 }
1649
1650 pub fn set_script(&mut self, new_script: &str) {
1656 let mut builder = GreenNodeBuilder::new();
1658 builder.start_node(SCRIPT.into());
1659 builder.token(VALUE.into(), new_script);
1661 builder.finish_node();
1662 let new_script_green = builder.finish();
1663
1664 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1666
1667 let script_pos = self
1669 .0
1670 .children_with_tokens()
1671 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1672
1673 if let Some(pos) = script_pos {
1674 self.0
1676 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1677 }
1678 }
1680
1681 pub fn set_option(&mut self, option: crate::types::WatchOption) {
1687 let key = watch_option_to_key(&option);
1688 let value = watch_option_to_value(&option);
1689 self.set_opt(key, &value);
1690 }
1691
1692 pub fn set_opt(&mut self, key: &str, value: &str) {
1698 let opts_pos = self.0.children_with_tokens().position(
1700 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1701 );
1702
1703 if let Some(_opts_idx) = opts_pos {
1704 if let Some(mut ol) = self.option_list() {
1705 if let Some(mut opt) = ol.find_option(key) {
1707 opt.set_value(value);
1709 } else {
1711 ol.add_option(key, value);
1713 }
1715 }
1716 } else {
1717 let mut builder = GreenNodeBuilder::new();
1719 builder.start_node(OPTS_LIST.into());
1720 builder.token(KEY.into(), "opts");
1721 builder.token(EQUALS.into(), "=");
1722 builder.start_node(OPTION.into());
1723 builder.token(KEY.into(), key);
1724 builder.token(EQUALS.into(), "=");
1725 builder.token(VALUE.into(), value);
1726 builder.finish_node();
1727 builder.finish_node();
1728 let new_opts_green = builder.finish();
1729 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1730
1731 let url_pos = self
1733 .0
1734 .children_with_tokens()
1735 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1736
1737 if let Some(url_idx) = url_pos {
1738 let mut combined_builder = GreenNodeBuilder::new();
1741 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1743 combined_builder.finish_node();
1744 let temp_green = combined_builder.finish();
1745 let temp_root = SyntaxNode::new_root_mut(temp_green);
1746 let space_element = temp_root.children_with_tokens().next().unwrap();
1747
1748 self.0
1749 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1750 } else {
1751 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1752 }
1753 }
1754 }
1755
1756 pub fn del_opt(&mut self, option: crate::types::WatchOption) {
1763 let key = watch_option_to_key(&option);
1764 if let Some(mut ol) = self.option_list() {
1765 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1766
1767 if option_count == 1 && ol.has_option(key) {
1768 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1770
1771 if let Some(opts_idx) = opts_pos {
1772 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1774
1775 while self.0.children_with_tokens().next().is_some_and(|e| {
1777 matches!(
1778 e,
1779 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1780 )
1781 }) {
1782 self.0.splice_children(0..1, vec![]);
1783 }
1784 }
1785 } else {
1786 ol.remove_option(key);
1788 }
1789 }
1790 }
1791
1792 pub fn del_opt_str(&mut self, key: &str) {
1799 if let Some(mut ol) = self.option_list() {
1800 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1801
1802 if option_count == 1 && ol.has_option(key) {
1803 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1805
1806 if let Some(opts_idx) = opts_pos {
1807 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1809
1810 while self.0.children_with_tokens().next().is_some_and(|e| {
1812 matches!(
1813 e,
1814 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1815 )
1816 }) {
1817 self.0.splice_children(0..1, vec![]);
1818 }
1819 }
1820 } else {
1821 ol.remove_option(key);
1823 }
1824 }
1825 }
1826}
1827
1828impl std::fmt::Debug for OptionList {
1829 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1830 f.debug_struct("OptionList")
1831 .field("text", &self.0.text().to_string())
1832 .finish()
1833 }
1834}
1835
1836impl OptionList {
1837 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1839 self.0.children().filter_map(_Option::cast)
1840 }
1841
1842 pub fn find_option(&self, key: &str) -> Option<_Option> {
1844 self.options().find(|opt| opt.key().as_deref() == Some(key))
1845 }
1846
1847 pub fn has_option(&self, key: &str) -> bool {
1849 self.options().any(|it| it.key().as_deref() == Some(key))
1850 }
1851
1852 #[cfg(feature = "deb822")]
1855 pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
1856 self.options().filter_map(|opt| {
1857 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
1858 Some((key, value))
1859 } else {
1860 None
1861 }
1862 })
1863 }
1864
1865 pub fn get_option(&self, key: &str) -> Option<String> {
1867 for child in self.options() {
1868 if child.key().as_deref() == Some(key) {
1869 return child.value();
1870 }
1871 }
1872 None
1873 }
1874
1875 fn add_option(&mut self, key: &str, value: &str) {
1877 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1878
1879 let mut builder = GreenNodeBuilder::new();
1881 builder.start_node(ROOT.into()); if option_count > 0 {
1884 builder.start_node(OPTION_SEPARATOR.into());
1885 builder.token(COMMA.into(), ",");
1886 builder.finish_node();
1887 }
1888
1889 builder.start_node(OPTION.into());
1890 builder.token(KEY.into(), key);
1891 builder.token(EQUALS.into(), "=");
1892 builder.token(VALUE.into(), value);
1893 builder.finish_node();
1894
1895 builder.finish_node(); let combined_green = builder.finish();
1897
1898 let temp_root = SyntaxNode::new_root_mut(combined_green);
1900 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1901
1902 let insert_pos = self.0.children_with_tokens().count();
1903 self.0.splice_children(insert_pos..insert_pos, new_children);
1904 }
1905
1906 fn remove_option(&mut self, key: &str) -> bool {
1908 if let Some(mut opt) = self.find_option(key) {
1909 opt.remove();
1910 true
1911 } else {
1912 false
1913 }
1914 }
1915}
1916
1917impl _Option {
1918 pub fn key(&self) -> Option<String> {
1920 self.0.children_with_tokens().find_map(|it| match it {
1921 SyntaxElement::Token(token) => {
1922 if token.kind() == KEY {
1923 Some(token.text().to_string())
1924 } else {
1925 None
1926 }
1927 }
1928 _ => None,
1929 })
1930 }
1931
1932 pub fn value(&self) -> Option<String> {
1934 self.0
1935 .children_with_tokens()
1936 .filter_map(|it| match it {
1937 SyntaxElement::Token(token) => {
1938 if token.kind() == VALUE || token.kind() == KEY {
1939 Some(token.text().to_string())
1940 } else {
1941 None
1942 }
1943 }
1944 _ => None,
1945 })
1946 .nth(1)
1947 }
1948
1949 pub fn set_value(&mut self, new_value: &str) {
1951 let key = self.key().expect("Option must have a key");
1952
1953 let mut builder = GreenNodeBuilder::new();
1955 builder.start_node(OPTION.into());
1956 builder.token(KEY.into(), &key);
1957 builder.token(EQUALS.into(), "=");
1958 builder.token(VALUE.into(), new_value);
1959 builder.finish_node();
1960 let new_option_green = builder.finish();
1961 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1962
1963 if let Some(parent) = self.0.parent() {
1965 let idx = self.0.index();
1966 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1967 }
1968 }
1969
1970 pub fn remove(&mut self) {
1972 let next_sep = self
1974 .0
1975 .next_sibling()
1976 .filter(|n| n.kind() == OPTION_SEPARATOR);
1977 let prev_sep = self
1978 .0
1979 .prev_sibling()
1980 .filter(|n| n.kind() == OPTION_SEPARATOR);
1981
1982 if let Some(sep) = next_sep {
1984 sep.detach();
1985 } else if let Some(sep) = prev_sep {
1986 sep.detach();
1987 }
1988
1989 self.0.detach();
1991 }
1992}
1993
1994impl Url {
1995 pub fn url(&self) -> String {
1997 self.0
1998 .children_with_tokens()
1999 .find_map(|it| match it {
2000 SyntaxElement::Token(token) => {
2001 if token.kind() == VALUE {
2002 Some(token.text().to_string())
2003 } else {
2004 None
2005 }
2006 }
2007 _ => None,
2008 })
2009 .unwrap()
2010 }
2011}
2012
2013impl MatchingPattern {
2014 pub fn pattern(&self) -> String {
2016 self.0
2017 .children_with_tokens()
2018 .find_map(|it| match it {
2019 SyntaxElement::Token(token) => {
2020 if token.kind() == VALUE {
2021 Some(token.text().to_string())
2022 } else {
2023 None
2024 }
2025 }
2026 _ => None,
2027 })
2028 .unwrap()
2029 }
2030}
2031
2032impl VersionPolicyNode {
2033 pub fn policy(&self) -> String {
2035 self.0
2036 .children_with_tokens()
2037 .find_map(|it| match it {
2038 SyntaxElement::Token(token) => {
2039 if token.kind() == VALUE || token.kind() == KEY {
2041 Some(token.text().to_string())
2042 } else {
2043 None
2044 }
2045 }
2046 _ => None,
2047 })
2048 .unwrap()
2049 }
2050}
2051
2052impl ScriptNode {
2053 pub fn script(&self) -> String {
2055 self.0
2056 .children_with_tokens()
2057 .find_map(|it| match it {
2058 SyntaxElement::Token(token) => {
2059 if token.kind() == VALUE || token.kind() == KEY {
2061 Some(token.text().to_string())
2062 } else {
2063 None
2064 }
2065 }
2066 _ => None,
2067 })
2068 .unwrap()
2069 }
2070}
2071
2072#[cfg(test)]
2073mod tests {
2074 use super::*;
2075
2076 #[test]
2077 fn test_entry_node_structure() {
2078 let wf: super::WatchFile = r#"version=4
2080opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2081"#
2082 .parse()
2083 .unwrap();
2084
2085 let entry = wf.entries().next().unwrap();
2086
2087 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2089 assert_eq!(entry.url(), "https://example.com/releases");
2090
2091 assert_eq!(
2093 entry
2094 .0
2095 .children()
2096 .find(|n| n.kind() == MATCHING_PATTERN)
2097 .is_some(),
2098 true
2099 );
2100 assert_eq!(
2101 entry.matching_pattern(),
2102 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2103 );
2104
2105 assert_eq!(
2107 entry
2108 .0
2109 .children()
2110 .find(|n| n.kind() == VERSION_POLICY)
2111 .is_some(),
2112 true
2113 );
2114 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2115
2116 assert_eq!(
2118 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2119 true
2120 );
2121 assert_eq!(entry.script(), Some("uupdate".into()));
2122 }
2123
2124 #[test]
2125 fn test_entry_node_structure_partial() {
2126 let wf: super::WatchFile = r#"version=4
2128https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2129"#
2130 .parse()
2131 .unwrap();
2132
2133 let entry = wf.entries().next().unwrap();
2134
2135 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2137 assert_eq!(
2138 entry
2139 .0
2140 .children()
2141 .find(|n| n.kind() == MATCHING_PATTERN)
2142 .is_some(),
2143 true
2144 );
2145
2146 assert_eq!(
2148 entry
2149 .0
2150 .children()
2151 .find(|n| n.kind() == VERSION_POLICY)
2152 .is_some(),
2153 false
2154 );
2155 assert_eq!(
2156 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2157 false
2158 );
2159
2160 assert_eq!(entry.url(), "https://github.com/example/tags");
2162 assert_eq!(
2163 entry.matching_pattern(),
2164 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2165 );
2166 assert_eq!(entry.version(), Ok(None));
2167 assert_eq!(entry.script(), None);
2168 }
2169
2170 #[test]
2171 fn test_parse_v1() {
2172 const WATCHV1: &str = r#"version=4
2173opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2174 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2175"#;
2176 let parsed = parse(WATCHV1);
2177 let node = parsed.syntax();
2179 assert_eq!(
2180 format!("{:#?}", node),
2181 r#"ROOT@0..161
2182 VERSION@0..10
2183 KEY@0..7 "version"
2184 EQUALS@7..8 "="
2185 VALUE@8..9 "4"
2186 NEWLINE@9..10 "\n"
2187 ENTRY@10..161
2188 OPTS_LIST@10..86
2189 KEY@10..14 "opts"
2190 EQUALS@14..15 "="
2191 OPTION@15..19
2192 KEY@15..19 "bare"
2193 OPTION_SEPARATOR@19..20
2194 COMMA@19..20 ","
2195 OPTION@20..86
2196 KEY@20..34 "filenamemangle"
2197 EQUALS@34..35 "="
2198 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2199 WHITESPACE@86..87 " "
2200 CONTINUATION@87..89 "\\\n"
2201 WHITESPACE@89..91 " "
2202 URL@91..138
2203 VALUE@91..138 "https://github.com/sy ..."
2204 WHITESPACE@138..139 " "
2205 MATCHING_PATTERN@139..160
2206 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2207 NEWLINE@160..161 "\n"
2208"#
2209 );
2210
2211 let root = parsed.root();
2212 assert_eq!(root.version(), 4);
2213 let entries = root.entries().collect::<Vec<_>>();
2214 assert_eq!(entries.len(), 1);
2215 let entry = &entries[0];
2216 assert_eq!(
2217 entry.url(),
2218 "https://github.com/syncthing/syncthing-gtk/tags"
2219 );
2220 assert_eq!(
2221 entry.matching_pattern(),
2222 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2223 );
2224 assert_eq!(entry.version(), Ok(None));
2225 assert_eq!(entry.script(), None);
2226
2227 assert_eq!(node.text(), WATCHV1);
2228 }
2229
2230 #[test]
2231 fn test_parse_v2() {
2232 let parsed = parse(
2233 r#"version=4
2234https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2235# comment
2236"#,
2237 );
2238 assert_eq!(parsed.errors, Vec::<String>::new());
2239 let node = parsed.syntax();
2240 assert_eq!(
2241 format!("{:#?}", node),
2242 r###"ROOT@0..90
2243 VERSION@0..10
2244 KEY@0..7 "version"
2245 EQUALS@7..8 "="
2246 VALUE@8..9 "4"
2247 NEWLINE@9..10 "\n"
2248 ENTRY@10..80
2249 URL@10..57
2250 VALUE@10..57 "https://github.com/sy ..."
2251 WHITESPACE@57..58 " "
2252 MATCHING_PATTERN@58..79
2253 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2254 NEWLINE@79..80 "\n"
2255 COMMENT@80..89 "# comment"
2256 NEWLINE@89..90 "\n"
2257"###
2258 );
2259
2260 let root = parsed.root();
2261 assert_eq!(root.version(), 4);
2262 let entries = root.entries().collect::<Vec<_>>();
2263 assert_eq!(entries.len(), 1);
2264 let entry = &entries[0];
2265 assert_eq!(
2266 entry.url(),
2267 "https://github.com/syncthing/syncthing-gtk/tags"
2268 );
2269 assert_eq!(
2270 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2271 "https://github.com/syncthing/syncthing-gtk/tags"
2272 .parse()
2273 .unwrap()
2274 );
2275 }
2276
2277 #[test]
2278 fn test_parse_v3() {
2279 let parsed = parse(
2280 r#"version=4
2281https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2282# comment
2283"#,
2284 );
2285 assert_eq!(parsed.errors, Vec::<String>::new());
2286 let root = parsed.root();
2287 assert_eq!(root.version(), 4);
2288 let entries = root.entries().collect::<Vec<_>>();
2289 assert_eq!(entries.len(), 1);
2290 let entry = &entries[0];
2291 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2292 assert_eq!(
2293 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2294 "https://github.com/syncthing/syncthing-gtk/tags"
2295 .parse()
2296 .unwrap()
2297 );
2298 }
2299
2300 #[test]
2301 fn test_thread_safe_parsing() {
2302 let text = r#"version=4
2303https://github.com/example/example/tags example-(.*)\.tar\.gz
2304"#;
2305
2306 let parsed = parse_watch_file(text);
2307 assert!(parsed.is_ok());
2308 assert_eq!(parsed.errors().len(), 0);
2309
2310 let watchfile = parsed.tree();
2312 assert_eq!(watchfile.version(), 4);
2313 let entries: Vec<_> = watchfile.entries().collect();
2314 assert_eq!(entries.len(), 1);
2315 }
2316
2317 #[test]
2318 fn test_parse_clone_and_eq() {
2319 let text = r#"version=4
2320https://github.com/example/example/tags example-(.*)\.tar\.gz
2321"#;
2322
2323 let parsed1 = parse_watch_file(text);
2324 let parsed2 = parsed1.clone();
2325
2326 assert_eq!(parsed1, parsed2);
2328
2329 let watchfile1 = parsed1.tree();
2331 let watchfile2 = watchfile1.clone();
2332 assert_eq!(watchfile1, watchfile2);
2333 }
2334
2335 #[test]
2336 fn test_parse_v4() {
2337 let cl: super::WatchFile = r#"version=4
2338opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2339 https://github.com/example/example-cat/tags \
2340 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2341"#
2342 .parse()
2343 .unwrap();
2344 assert_eq!(cl.version(), 4);
2345 let entries = cl.entries().collect::<Vec<_>>();
2346 assert_eq!(entries.len(), 1);
2347 let entry = &entries[0];
2348 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2349 assert_eq!(
2350 entry.matching_pattern(),
2351 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2352 );
2353 assert!(entry.repack());
2354 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2355 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2356 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2357 assert_eq!(entry.script(), Some("uupdate".into()));
2358 assert_eq!(
2359 entry.format_url(|| "example-cat".to_string(), || String::new()),
2360 "https://github.com/example/example-cat/tags"
2361 .parse()
2362 .unwrap()
2363 );
2364 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2365 }
2366
2367 #[test]
2368 fn test_git_mode() {
2369 let text = r#"version=3
2370opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2371https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2372refs/tags/(.*) debian
2373"#;
2374 let parsed = parse(text);
2375 assert_eq!(parsed.errors, Vec::<String>::new());
2376 let cl = parsed.root();
2377 assert_eq!(cl.version(), 3);
2378 let entries = cl.entries().collect::<Vec<_>>();
2379 assert_eq!(entries.len(), 1);
2380 let entry = &entries[0];
2381 assert_eq!(
2382 entry.url(),
2383 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2384 );
2385 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2386 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2387 assert_eq!(entry.script(), None);
2388 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2389 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2390 assert_eq!(entry.mode(), Ok(Mode::Git));
2391 }
2392
2393 #[test]
2394 fn test_parse_quoted() {
2395 const WATCHV1: &str = r#"version=4
2396opts="bare, filenamemangle=blah" \
2397 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2398"#;
2399 let parsed = parse(WATCHV1);
2400 let node = parsed.syntax();
2402
2403 let root = parsed.root();
2404 assert_eq!(root.version(), 4);
2405 let entries = root.entries().collect::<Vec<_>>();
2406 assert_eq!(entries.len(), 1);
2407 let entry = &entries[0];
2408
2409 assert_eq!(
2410 entry.url(),
2411 "https://github.com/syncthing/syncthing-gtk/tags"
2412 );
2413 assert_eq!(
2414 entry.matching_pattern(),
2415 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2416 );
2417 assert_eq!(entry.version(), Ok(None));
2418 assert_eq!(entry.script(), None);
2419
2420 assert_eq!(node.text(), WATCHV1);
2421 }
2422
2423 #[test]
2424 fn test_set_url() {
2425 let wf: super::WatchFile = r#"version=4
2427https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2428"#
2429 .parse()
2430 .unwrap();
2431
2432 let mut entry = wf.entries().next().unwrap();
2433 assert_eq!(
2434 entry.url(),
2435 "https://github.com/syncthing/syncthing-gtk/tags"
2436 );
2437
2438 entry.set_url("https://newurl.example.org/path");
2439 assert_eq!(entry.url(), "https://newurl.example.org/path");
2440 assert_eq!(
2441 entry.matching_pattern(),
2442 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2443 );
2444
2445 assert_eq!(
2447 entry.to_string(),
2448 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2449 );
2450 }
2451
2452 #[test]
2453 fn test_set_url_with_options() {
2454 let wf: super::WatchFile = r#"version=4
2456opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2457"#
2458 .parse()
2459 .unwrap();
2460
2461 let mut entry = wf.entries().next().unwrap();
2462 assert_eq!(entry.url(), "https://foo.com/bar");
2463 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2464
2465 entry.set_url("https://example.com/baz");
2466 assert_eq!(entry.url(), "https://example.com/baz");
2467
2468 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2470 assert_eq!(
2471 entry.matching_pattern(),
2472 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2473 );
2474
2475 assert_eq!(
2477 entry.to_string(),
2478 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2479 );
2480 }
2481
2482 #[test]
2483 fn test_set_url_complex() {
2484 let wf: super::WatchFile = r#"version=4
2486opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2487 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2488"#
2489 .parse()
2490 .unwrap();
2491
2492 let mut entry = wf.entries().next().unwrap();
2493 assert_eq!(
2494 entry.url(),
2495 "https://github.com/syncthing/syncthing-gtk/tags"
2496 );
2497
2498 entry.set_url("https://gitlab.com/newproject/tags");
2499 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2500
2501 assert!(entry.bare());
2503 assert_eq!(
2504 entry.filenamemangle(),
2505 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2506 );
2507 assert_eq!(
2508 entry.matching_pattern(),
2509 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2510 );
2511
2512 assert_eq!(
2514 entry.to_string(),
2515 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2516 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2517"#
2518 );
2519 }
2520
2521 #[test]
2522 fn test_set_url_with_all_fields() {
2523 let wf: super::WatchFile = r#"version=4
2525opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2526 https://github.com/example/example-cat/tags \
2527 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2528"#
2529 .parse()
2530 .unwrap();
2531
2532 let mut entry = wf.entries().next().unwrap();
2533 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2534 assert_eq!(
2535 entry.matching_pattern(),
2536 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2537 );
2538 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2539 assert_eq!(entry.script(), Some("uupdate".into()));
2540
2541 entry.set_url("https://gitlab.example.org/project/releases");
2542 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2543
2544 assert!(entry.repack());
2546 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2547 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2548 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2549 assert_eq!(
2550 entry.matching_pattern(),
2551 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2552 );
2553 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2554 assert_eq!(entry.script(), Some("uupdate".into()));
2555
2556 assert_eq!(
2558 entry.to_string(),
2559 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2560 https://gitlab.example.org/project/releases \
2561 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2562"#
2563 );
2564 }
2565
2566 #[test]
2567 fn test_set_url_quoted_options() {
2568 let wf: super::WatchFile = r#"version=4
2570opts="bare, filenamemangle=blah" \
2571 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2572"#
2573 .parse()
2574 .unwrap();
2575
2576 let mut entry = wf.entries().next().unwrap();
2577 assert_eq!(
2578 entry.url(),
2579 "https://github.com/syncthing/syncthing-gtk/tags"
2580 );
2581
2582 entry.set_url("https://example.org/new/path");
2583 assert_eq!(entry.url(), "https://example.org/new/path");
2584
2585 assert_eq!(
2587 entry.to_string(),
2588 r#"opts="bare, filenamemangle=blah" \
2589 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2590"#
2591 );
2592 }
2593
2594 #[test]
2595 fn test_set_opt_update_existing() {
2596 let wf: super::WatchFile = r#"version=4
2598opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2599"#
2600 .parse()
2601 .unwrap();
2602
2603 let mut entry = wf.entries().next().unwrap();
2604 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2605 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2606
2607 entry.set_opt("foo", "updated");
2608 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2609 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2610
2611 assert_eq!(
2613 entry.to_string(),
2614 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2615 );
2616 }
2617
2618 #[test]
2619 fn test_set_opt_add_new() {
2620 let wf: super::WatchFile = r#"version=4
2622opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2623"#
2624 .parse()
2625 .unwrap();
2626
2627 let mut entry = wf.entries().next().unwrap();
2628 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2629 assert_eq!(entry.get_option("bar"), None);
2630
2631 entry.set_opt("bar", "baz");
2632 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2633 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2634
2635 assert_eq!(
2637 entry.to_string(),
2638 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2639 );
2640 }
2641
2642 #[test]
2643 fn test_set_opt_create_options_list() {
2644 let wf: super::WatchFile = r#"version=4
2646https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2647"#
2648 .parse()
2649 .unwrap();
2650
2651 let mut entry = wf.entries().next().unwrap();
2652 assert_eq!(entry.option_list(), None);
2653
2654 entry.set_opt("compression", "xz");
2655 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2656
2657 assert_eq!(
2659 entry.to_string(),
2660 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2661 );
2662 }
2663
2664 #[test]
2665 fn test_del_opt_remove_single() {
2666 let wf: super::WatchFile = r#"version=4
2668opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2669"#
2670 .parse()
2671 .unwrap();
2672
2673 let mut entry = wf.entries().next().unwrap();
2674 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2675 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2676 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2677
2678 entry.del_opt_str("bar");
2679 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2680 assert_eq!(entry.get_option("bar"), None);
2681 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2682
2683 assert_eq!(
2685 entry.to_string(),
2686 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2687 );
2688 }
2689
2690 #[test]
2691 fn test_del_opt_remove_first() {
2692 let wf: super::WatchFile = r#"version=4
2694opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2695"#
2696 .parse()
2697 .unwrap();
2698
2699 let mut entry = wf.entries().next().unwrap();
2700 entry.del_opt_str("foo");
2701 assert_eq!(entry.get_option("foo"), None);
2702 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2703
2704 assert_eq!(
2706 entry.to_string(),
2707 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2708 );
2709 }
2710
2711 #[test]
2712 fn test_del_opt_remove_last() {
2713 let wf: super::WatchFile = r#"version=4
2715opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2716"#
2717 .parse()
2718 .unwrap();
2719
2720 let mut entry = wf.entries().next().unwrap();
2721 entry.del_opt_str("bar");
2722 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2723 assert_eq!(entry.get_option("bar"), None);
2724
2725 assert_eq!(
2727 entry.to_string(),
2728 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2729 );
2730 }
2731
2732 #[test]
2733 fn test_del_opt_remove_only_option() {
2734 let wf: super::WatchFile = r#"version=4
2736opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2737"#
2738 .parse()
2739 .unwrap();
2740
2741 let mut entry = wf.entries().next().unwrap();
2742 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2743
2744 entry.del_opt_str("foo");
2745 assert_eq!(entry.get_option("foo"), None);
2746 assert_eq!(entry.option_list(), None);
2747
2748 assert_eq!(
2750 entry.to_string(),
2751 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2752 );
2753 }
2754
2755 #[test]
2756 fn test_del_opt_nonexistent() {
2757 let wf: super::WatchFile = r#"version=4
2759opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2760"#
2761 .parse()
2762 .unwrap();
2763
2764 let mut entry = wf.entries().next().unwrap();
2765 let original = entry.to_string();
2766
2767 entry.del_opt_str("nonexistent");
2768 assert_eq!(entry.to_string(), original);
2769 }
2770
2771 #[test]
2772 fn test_set_opt_multiple_operations() {
2773 let wf: super::WatchFile = r#"version=4
2775https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2776"#
2777 .parse()
2778 .unwrap();
2779
2780 let mut entry = wf.entries().next().unwrap();
2781
2782 entry.set_opt("compression", "xz");
2783 entry.set_opt("repack", "");
2784 entry.set_opt("dversionmangle", "s/\\+ds//");
2785
2786 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2787 assert_eq!(
2788 entry.get_option("dversionmangle"),
2789 Some("s/\\+ds//".to_string())
2790 );
2791 }
2792
2793 #[test]
2794 fn test_set_matching_pattern() {
2795 let wf: super::WatchFile = r#"version=4
2797https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2798"#
2799 .parse()
2800 .unwrap();
2801
2802 let mut entry = wf.entries().next().unwrap();
2803 assert_eq!(
2804 entry.matching_pattern(),
2805 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2806 );
2807
2808 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2809 assert_eq!(
2810 entry.matching_pattern(),
2811 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2812 );
2813
2814 assert_eq!(entry.url(), "https://github.com/example/tags");
2816
2817 assert_eq!(
2819 entry.to_string(),
2820 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2821 );
2822 }
2823
2824 #[test]
2825 fn test_set_matching_pattern_with_all_fields() {
2826 let wf: super::WatchFile = r#"version=4
2828opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2829"#
2830 .parse()
2831 .unwrap();
2832
2833 let mut entry = wf.entries().next().unwrap();
2834 assert_eq!(
2835 entry.matching_pattern(),
2836 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2837 );
2838
2839 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2840 assert_eq!(
2841 entry.matching_pattern(),
2842 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2843 );
2844
2845 assert_eq!(entry.url(), "https://example.com/releases");
2847 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2848 assert_eq!(entry.script(), Some("uupdate".into()));
2849 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2850
2851 assert_eq!(
2853 entry.to_string(),
2854 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2855 );
2856 }
2857
2858 #[test]
2859 fn test_set_version_policy() {
2860 let wf: super::WatchFile = r#"version=4
2862https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2863"#
2864 .parse()
2865 .unwrap();
2866
2867 let mut entry = wf.entries().next().unwrap();
2868 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2869
2870 entry.set_version_policy("previous");
2871 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2872
2873 assert_eq!(entry.url(), "https://example.com/releases");
2875 assert_eq!(
2876 entry.matching_pattern(),
2877 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2878 );
2879 assert_eq!(entry.script(), Some("uupdate".into()));
2880
2881 assert_eq!(
2883 entry.to_string(),
2884 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2885 );
2886 }
2887
2888 #[test]
2889 fn test_set_version_policy_with_options() {
2890 let wf: super::WatchFile = r#"version=4
2892opts=repack,compression=xz \
2893 https://github.com/example/example-cat/tags \
2894 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2895"#
2896 .parse()
2897 .unwrap();
2898
2899 let mut entry = wf.entries().next().unwrap();
2900 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2901
2902 entry.set_version_policy("ignore");
2903 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2904
2905 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2907 assert_eq!(
2908 entry.matching_pattern(),
2909 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2910 );
2911 assert_eq!(entry.script(), Some("uupdate".into()));
2912 assert!(entry.repack());
2913
2914 assert_eq!(
2916 entry.to_string(),
2917 r#"opts=repack,compression=xz \
2918 https://github.com/example/example-cat/tags \
2919 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2920"#
2921 );
2922 }
2923
2924 #[test]
2925 fn test_set_script() {
2926 let wf: super::WatchFile = r#"version=4
2928https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2929"#
2930 .parse()
2931 .unwrap();
2932
2933 let mut entry = wf.entries().next().unwrap();
2934 assert_eq!(entry.script(), Some("uupdate".into()));
2935
2936 entry.set_script("uscan");
2937 assert_eq!(entry.script(), Some("uscan".into()));
2938
2939 assert_eq!(entry.url(), "https://example.com/releases");
2941 assert_eq!(
2942 entry.matching_pattern(),
2943 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2944 );
2945 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2946
2947 assert_eq!(
2949 entry.to_string(),
2950 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2951 );
2952 }
2953
2954 #[test]
2955 fn test_set_script_with_options() {
2956 let wf: super::WatchFile = r#"version=4
2958opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2959"#
2960 .parse()
2961 .unwrap();
2962
2963 let mut entry = wf.entries().next().unwrap();
2964 assert_eq!(entry.script(), Some("uupdate".into()));
2965
2966 entry.set_script("custom-script.sh");
2967 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2968
2969 assert_eq!(entry.url(), "https://example.com/releases");
2971 assert_eq!(
2972 entry.matching_pattern(),
2973 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2974 );
2975 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2976 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2977
2978 assert_eq!(
2980 entry.to_string(),
2981 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2982 );
2983 }
2984
2985 #[test]
2986 fn test_apply_dversionmangle() {
2987 let wf: super::WatchFile = r#"version=4
2989opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2990"#
2991 .parse()
2992 .unwrap();
2993 let entry = wf.entries().next().unwrap();
2994 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2995 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2996
2997 let wf: super::WatchFile = r#"version=4
2999opts=versionmangle=s/^v// https://example.com/ .*
3000"#
3001 .parse()
3002 .unwrap();
3003 let entry = wf.entries().next().unwrap();
3004 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
3005
3006 let wf: super::WatchFile = r#"version=4
3008opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
3009"#
3010 .parse()
3011 .unwrap();
3012 let entry = wf.entries().next().unwrap();
3013 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
3014
3015 let wf: super::WatchFile = r#"version=4
3017https://example.com/ .*
3018"#
3019 .parse()
3020 .unwrap();
3021 let entry = wf.entries().next().unwrap();
3022 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
3023 }
3024
3025 #[test]
3026 fn test_apply_oversionmangle() {
3027 let wf: super::WatchFile = r#"version=4
3029opts=oversionmangle=s/$/-1/ https://example.com/ .*
3030"#
3031 .parse()
3032 .unwrap();
3033 let entry = wf.entries().next().unwrap();
3034 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
3035 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
3036
3037 let wf: super::WatchFile = r#"version=4
3039opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
3040"#
3041 .parse()
3042 .unwrap();
3043 let entry = wf.entries().next().unwrap();
3044 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
3045
3046 let wf: super::WatchFile = r#"version=4
3048https://example.com/ .*
3049"#
3050 .parse()
3051 .unwrap();
3052 let entry = wf.entries().next().unwrap();
3053 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
3054 }
3055
3056 #[test]
3057 fn test_apply_dirversionmangle() {
3058 let wf: super::WatchFile = r#"version=4
3060opts=dirversionmangle=s/^v// https://example.com/ .*
3061"#
3062 .parse()
3063 .unwrap();
3064 let entry = wf.entries().next().unwrap();
3065 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3066 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
3067
3068 let wf: super::WatchFile = r#"version=4
3070opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
3071"#
3072 .parse()
3073 .unwrap();
3074 let entry = wf.entries().next().unwrap();
3075 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3076
3077 let wf: super::WatchFile = r#"version=4
3079https://example.com/ .*
3080"#
3081 .parse()
3082 .unwrap();
3083 let entry = wf.entries().next().unwrap();
3084 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
3085 }
3086
3087 #[test]
3088 fn test_apply_filenamemangle() {
3089 let wf: super::WatchFile = r#"version=4
3091opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
3092"#
3093 .parse()
3094 .unwrap();
3095 let entry = wf.entries().next().unwrap();
3096 assert_eq!(
3097 entry
3098 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
3099 .unwrap(),
3100 "mypackage-1.0.tar.gz"
3101 );
3102 assert_eq!(
3103 entry
3104 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
3105 .unwrap(),
3106 "mypackage-2.5.3.tar.gz"
3107 );
3108
3109 let wf: super::WatchFile = r#"version=4
3111opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
3112"#
3113 .parse()
3114 .unwrap();
3115 let entry = wf.entries().next().unwrap();
3116 assert_eq!(
3117 entry
3118 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
3119 .unwrap(),
3120 "file.tar.gz"
3121 );
3122
3123 let wf: super::WatchFile = r#"version=4
3125https://example.com/ .*
3126"#
3127 .parse()
3128 .unwrap();
3129 let entry = wf.entries().next().unwrap();
3130 assert_eq!(
3131 entry
3132 .apply_filenamemangle("https://example.com/file.tar.gz")
3133 .unwrap(),
3134 "https://example.com/file.tar.gz"
3135 );
3136 }
3137
3138 #[test]
3139 fn test_apply_pagemangle() {
3140 let wf: super::WatchFile = r#"version=4
3142opts=pagemangle=s/&/&/g https://example.com/ .*
3143"#
3144 .parse()
3145 .unwrap();
3146 let entry = wf.entries().next().unwrap();
3147 assert_eq!(
3148 entry.apply_pagemangle(b"foo & bar").unwrap(),
3149 b"foo & bar"
3150 );
3151 assert_eq!(
3152 entry
3153 .apply_pagemangle(b"& foo & bar &")
3154 .unwrap(),
3155 b"& foo & bar &"
3156 );
3157
3158 let wf: super::WatchFile = r#"version=4
3160opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3161"#
3162 .parse()
3163 .unwrap();
3164 let entry = wf.entries().next().unwrap();
3165 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3166
3167 let wf: super::WatchFile = r#"version=4
3169https://example.com/ .*
3170"#
3171 .parse()
3172 .unwrap();
3173 let entry = wf.entries().next().unwrap();
3174 assert_eq!(
3175 entry.apply_pagemangle(b"foo & bar").unwrap(),
3176 b"foo & bar"
3177 );
3178 }
3179
3180 #[test]
3181 fn test_apply_downloadurlmangle() {
3182 let wf: super::WatchFile = r#"version=4
3184opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3185"#
3186 .parse()
3187 .unwrap();
3188 let entry = wf.entries().next().unwrap();
3189 assert_eq!(
3190 entry
3191 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3192 .unwrap(),
3193 "https://example.com/download/file.tar.gz"
3194 );
3195
3196 let wf: super::WatchFile = r#"version=4
3198opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3199"#
3200 .parse()
3201 .unwrap();
3202 let entry = wf.entries().next().unwrap();
3203 assert_eq!(
3204 entry
3205 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3206 .unwrap(),
3207 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3208 );
3209
3210 let wf: super::WatchFile = r#"version=4
3212https://example.com/ .*
3213"#
3214 .parse()
3215 .unwrap();
3216 let entry = wf.entries().next().unwrap();
3217 assert_eq!(
3218 entry
3219 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3220 .unwrap(),
3221 "https://example.com/archive/file.tar.gz"
3222 );
3223 }
3224
3225 #[test]
3226 fn test_entry_builder_minimal() {
3227 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3229 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3230 .build();
3231
3232 assert_eq!(entry.url(), "https://github.com/example/tags");
3233 assert_eq!(
3234 entry.matching_pattern().as_deref(),
3235 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3236 );
3237 assert_eq!(entry.version(), Ok(None));
3238 assert_eq!(entry.script(), None);
3239 assert!(entry.opts().is_empty());
3240 }
3241
3242 #[test]
3243 fn test_entry_builder_url_only() {
3244 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3246
3247 assert_eq!(entry.url(), "https://example.com/releases");
3248 assert_eq!(entry.matching_pattern(), None);
3249 assert_eq!(entry.version(), Ok(None));
3250 assert_eq!(entry.script(), None);
3251 assert!(entry.opts().is_empty());
3252 }
3253
3254 #[test]
3255 fn test_entry_builder_with_all_fields() {
3256 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3258 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3259 .version_policy("debian")
3260 .script("uupdate")
3261 .opt("compression", "xz")
3262 .flag("repack")
3263 .build();
3264
3265 assert_eq!(entry.url(), "https://github.com/example/tags");
3266 assert_eq!(
3267 entry.matching_pattern().as_deref(),
3268 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3269 );
3270 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3271 assert_eq!(entry.script(), Some("uupdate".into()));
3272 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3273 assert!(entry.has_option("repack"));
3274 assert!(entry.repack());
3275 }
3276
3277 #[test]
3278 fn test_entry_builder_multiple_options() {
3279 let entry = super::EntryBuilder::new("https://example.com/tags")
3281 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3282 .opt("compression", "xz")
3283 .opt("dversionmangle", "s/\\+ds//")
3284 .opt("repacksuffix", "+ds")
3285 .build();
3286
3287 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3288 assert_eq!(
3289 entry.get_option("dversionmangle"),
3290 Some("s/\\+ds//".to_string())
3291 );
3292 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3293 }
3294
3295 #[test]
3296 fn test_entry_builder_via_entry() {
3297 let entry = super::Entry::builder("https://github.com/example/tags")
3299 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3300 .version_policy("debian")
3301 .build();
3302
3303 assert_eq!(entry.url(), "https://github.com/example/tags");
3304 assert_eq!(
3305 entry.matching_pattern().as_deref(),
3306 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3307 );
3308 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3309 }
3310
3311 #[test]
3312 fn test_watchfile_add_entry_to_empty() {
3313 let mut wf = super::WatchFile::new(Some(4));
3315
3316 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3317 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3318 .build();
3319
3320 wf.add_entry(entry);
3321
3322 assert_eq!(wf.version(), 4);
3323 assert_eq!(wf.entries().count(), 1);
3324
3325 let added_entry = wf.entries().next().unwrap();
3326 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3327 assert_eq!(
3328 added_entry.matching_pattern().as_deref(),
3329 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3330 );
3331 }
3332
3333 #[test]
3334 fn test_watchfile_add_multiple_entries() {
3335 let mut wf = super::WatchFile::new(Some(4));
3337
3338 wf.add_entry(
3339 super::EntryBuilder::new("https://github.com/example1/tags")
3340 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3341 .build(),
3342 );
3343
3344 wf.add_entry(
3345 super::EntryBuilder::new("https://github.com/example2/releases")
3346 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3347 .opt("compression", "xz")
3348 .build(),
3349 );
3350
3351 assert_eq!(wf.entries().count(), 2);
3352
3353 let entries: Vec<_> = wf.entries().collect();
3354 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3355 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3356 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3357 }
3358
3359 #[test]
3360 fn test_watchfile_add_entry_to_existing() {
3361 let mut wf: super::WatchFile = r#"version=4
3363https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3364"#
3365 .parse()
3366 .unwrap();
3367
3368 assert_eq!(wf.entries().count(), 1);
3369
3370 wf.add_entry(
3371 super::EntryBuilder::new("https://github.com/example/new")
3372 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3373 .opt("compression", "xz")
3374 .version_policy("debian")
3375 .build(),
3376 );
3377
3378 assert_eq!(wf.entries().count(), 2);
3379
3380 let entries: Vec<_> = wf.entries().collect();
3381 assert_eq!(entries[0].url(), "https://example.com/old");
3382 assert_eq!(entries[1].url(), "https://github.com/example/new");
3383 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3384 }
3385
3386 #[test]
3387 fn test_entry_builder_formatting() {
3388 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3390 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3391 .opt("compression", "xz")
3392 .flag("repack")
3393 .version_policy("debian")
3394 .script("uupdate")
3395 .build();
3396
3397 let entry_str = entry.to_string();
3398
3399 assert!(entry_str.starts_with("opts="));
3401 assert!(entry_str.contains("https://github.com/example/tags"));
3403 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3405 assert!(entry_str.contains("debian"));
3407 assert!(entry_str.contains("uupdate"));
3409 assert!(entry_str.ends_with('\n'));
3411 }
3412
3413 #[test]
3414 fn test_watchfile_add_entry_preserves_format() {
3415 let mut wf = super::WatchFile::new(Some(4));
3417
3418 wf.add_entry(
3419 super::EntryBuilder::new("https://github.com/example/tags")
3420 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3421 .build(),
3422 );
3423
3424 let wf_str = wf.to_string();
3425
3426 assert!(wf_str.starts_with("version=4\n"));
3428 assert!(wf_str.contains("https://github.com/example/tags"));
3430
3431 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3433 assert_eq!(reparsed.version(), 4);
3434 assert_eq!(reparsed.entries().count(), 1);
3435 }
3436
3437 #[test]
3438 fn test_line_col() {
3439 let text = r#"version=4
3440opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3441"#;
3442 let wf = text.parse::<super::WatchFile>().unwrap();
3443
3444 let version_node = wf.version_node().unwrap();
3446 assert_eq!(version_node.line(), 0);
3447 assert_eq!(version_node.column(), 0);
3448 assert_eq!(version_node.line_col(), (0, 0));
3449
3450 let entries: Vec<_> = wf.entries().collect();
3452 assert_eq!(entries.len(), 1);
3453
3454 assert_eq!(entries[0].line(), 1);
3456 assert_eq!(entries[0].column(), 0);
3457 assert_eq!(entries[0].line_col(), (1, 0));
3458
3459 let option_list = entries[0].option_list().unwrap();
3461 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3464 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3467 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3470 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3473 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3477 assert_eq!(options.len(), 1);
3478 assert_eq!(options[0].key(), Some("compression".to_string()));
3479 assert_eq!(options[0].value(), Some("xz".to_string()));
3480 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3484 assert_eq!(compression_opt.line(), 1);
3485 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3487 }
3488
3489 #[test]
3490 fn test_parse_str_relaxed() {
3491 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3492 r#"version=4
3493ERRORS IN THIS LINE
3494opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3495"#,
3496 );
3497 assert_eq!(wf.version(), 4);
3498 assert_eq!(wf.entries().count(), 2);
3499
3500 let entries = wf.entries().collect::<Vec<_>>();
3501
3502 let entry = &entries[0];
3503 assert_eq!(entry.url(), "ERRORS");
3504
3505 let entry = &entries[1];
3506 assert_eq!(entry.url(), "https://example.com/releases");
3507 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3508 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3509 }
3510
3511 #[test]
3512 fn test_parse_entry_with_comment_before() {
3513 let input = concat!(
3517 "version=4\n",
3518 "# try also https://pypi.debian.net/tomoscan/watch\n",
3519 "opts=uversionmangle=s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/ \\\n",
3520 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))\n"
3521 );
3522 let wf: super::WatchFile = input.parse().unwrap();
3523 assert_eq!(wf.to_string(), input);
3525 assert_eq!(wf.entries().count(), 1);
3526 let entry = wf.entries().next().unwrap();
3527 assert_eq!(
3528 entry.url(),
3529 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))"
3530 );
3531 assert_eq!(
3532 entry.get_option("uversionmangle"),
3533 Some("s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/".to_string())
3534 );
3535 }
3536
3537 #[test]
3538 fn test_parse_multiple_comments_before_entry() {
3539 let input = concat!(
3542 "version=4\n",
3543 "# first comment\n",
3544 "# second comment\n",
3545 "# third comment\n",
3546 "https://example.com/foo foo-(.*).tar.gz\n",
3547 );
3548 let wf: super::WatchFile = input.parse().unwrap();
3549 assert_eq!(wf.to_string(), input);
3550 assert_eq!(wf.entries().count(), 1);
3551 assert_eq!(
3552 wf.entries().next().unwrap().url(),
3553 "https://example.com/foo"
3554 );
3555 }
3556
3557 #[test]
3558 fn test_parse_blank_lines_between_entries() {
3559 let input = concat!(
3561 "version=4\n",
3562 "https://example.com/foo .*/foo-(\\d+)\\.tar\\.gz\n",
3563 "\n",
3564 "https://example.com/bar .*/bar-(\\d+)\\.tar\\.gz\n",
3565 );
3566 let wf: super::WatchFile = input.parse().unwrap();
3567 assert_eq!(wf.to_string(), input);
3568 assert_eq!(wf.entries().count(), 2);
3569 }
3570
3571 #[test]
3572 fn test_parse_trailing_unparseable_tokens_produce_error() {
3573 let input = "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n=garbage\n";
3576 let result = input.parse::<super::WatchFile>();
3577 assert!(result.is_err(), "expected parse error for trailing garbage");
3578 let wf = super::WatchFile::from_str_relaxed(input);
3580 assert_eq!(wf.to_string(), input);
3581 }
3582
3583 #[test]
3584 fn test_parse_roundtrip_full_file() {
3585 let inputs = [
3587 "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n",
3588 "version=4\n# a comment\nhttps://example.com/foo foo-(.*).tar.gz\n",
3589 concat!(
3590 "version=4\n",
3591 "opts=uversionmangle=s/rc/~rc/ \\\n",
3592 " https://example.com/foo foo-(.*).tar.gz\n",
3593 ),
3594 concat!(
3595 "version=4\n",
3596 "# comment before entry\n",
3597 "opts=uversionmangle=s/rc/~rc/ \\\n",
3598 "https://example.com/foo foo-(.*).tar.gz\n",
3599 "# comment between entries\n",
3600 "https://example.com/bar bar-(.*).tar.gz\n",
3601 ),
3602 ];
3603 for input in &inputs {
3604 let wf: super::WatchFile = input.parse().unwrap();
3605 assert_eq!(
3606 wf.to_string(),
3607 *input,
3608 "round-trip failed for input: {:?}",
3609 input
3610 );
3611 }
3612 }
3613}