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(crate) 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 #[cfg(feature = "deb822")]
630 pub(crate) fn syntax(&self) -> &SyntaxNode {
631 &self.0
632 }
633
634 pub fn new(version: Option<u32>) -> WatchFile {
636 let mut builder = GreenNodeBuilder::new();
637
638 builder.start_node(ROOT.into());
639 if let Some(version) = version {
640 builder.start_node(VERSION.into());
641 builder.token(KEY.into(), "version");
642 builder.token(EQUALS.into(), "=");
643 builder.token(VALUE.into(), version.to_string().as_str());
644 builder.token(NEWLINE.into(), "\n");
645 builder.finish_node();
646 }
647 builder.finish_node();
648 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
649 }
650
651 pub fn version_node(&self) -> Option<Version> {
653 self.0.children().find_map(Version::cast)
654 }
655
656 pub fn version(&self) -> u32 {
658 self.version_node()
659 .map(|it| it.version())
660 .unwrap_or(DEFAULT_VERSION)
661 }
662
663 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
665 self.0.children().filter_map(Entry::cast)
666 }
667
668 pub fn set_version(&mut self, new_version: u32) {
670 let mut builder = GreenNodeBuilder::new();
672 builder.start_node(VERSION.into());
673 builder.token(KEY.into(), "version");
674 builder.token(EQUALS.into(), "=");
675 builder.token(VALUE.into(), new_version.to_string().as_str());
676 builder.token(NEWLINE.into(), "\n");
677 builder.finish_node();
678 let new_version_green = builder.finish();
679
680 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
682
683 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
685
686 if let Some(pos) = version_pos {
687 self.0
689 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
690 } else {
691 self.0.splice_children(0..0, vec![new_version_node.into()]);
693 }
694 }
695
696 #[cfg(feature = "discover")]
716 pub async fn uscan(
717 &self,
718 package: impl Fn() -> String + Send + Sync,
719 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
720 let mut all_releases = Vec::new();
721
722 for entry in self.entries() {
723 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
724 let releases = parsed_entry.discover(|| package()).await?;
725 all_releases.push(releases);
726 }
727
728 Ok(all_releases)
729 }
730
731 #[cfg(all(feature = "discover", feature = "blocking"))]
749 pub fn uscan_blocking(
750 &self,
751 package: impl Fn() -> String,
752 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
753 let mut all_releases = Vec::new();
754
755 for entry in self.entries() {
756 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
757 let releases = parsed_entry.discover_blocking(|| package())?;
758 all_releases.push(releases);
759 }
760
761 Ok(all_releases)
762 }
763
764 pub fn add_entry(&mut self, entry: Entry) -> Entry {
791 let insert_pos = self.0.children_with_tokens().count();
793
794 let entry_green = entry.0.green().into_owned();
796 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
797
798 self.0
800 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
801
802 Entry::cast(
804 self.0
805 .children()
806 .nth(insert_pos)
807 .expect("Entry was just inserted"),
808 )
809 .expect("Inserted node should be an Entry")
810 }
811
812 pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
814 let mut buf_reader = std::io::BufReader::new(reader);
815 let mut content = String::new();
816 buf_reader
817 .read_to_string(&mut content)
818 .map_err(|e| ParseError(vec![e.to_string()]))?;
819 content.parse()
820 }
821
822 pub fn from_reader_relaxed<R: std::io::Read>(mut r: R) -> Result<Self, std::io::Error> {
824 let mut content = String::new();
825 r.read_to_string(&mut content)?;
826 let parsed = parse(&content);
827 Ok(parsed.root())
828 }
829
830 pub fn from_str_relaxed(s: &str) -> Self {
832 let parsed = parse(s);
833 parsed.root()
834 }
835}
836
837impl FromStr for WatchFile {
838 type Err = ParseError;
839
840 fn from_str(s: &str) -> Result<Self, Self::Err> {
841 let parsed = parse(s);
842 if parsed.errors.is_empty() {
843 Ok(parsed.root())
844 } else {
845 Err(ParseError(parsed.errors))
846 }
847 }
848}
849
850pub fn parse_watch_file(text: &str) -> Parse<WatchFile> {
853 let parsed = parse(text);
854 Parse::new(parsed.green_node, parsed.errors)
855}
856
857impl Version {
858 pub fn version(&self) -> u32 {
860 self.0
861 .children_with_tokens()
862 .find_map(|it| match it {
863 SyntaxElement::Token(token) => {
864 if token.kind() == VALUE {
865 token.text().parse().ok()
866 } else {
867 None
868 }
869 }
870 _ => None,
871 })
872 .unwrap_or(DEFAULT_VERSION)
873 }
874}
875
876#[derive(Debug, Clone, Default)]
900pub struct EntryBuilder {
901 url: Option<String>,
902 matching_pattern: Option<String>,
903 version_policy: Option<String>,
904 script: Option<String>,
905 opts: std::collections::HashMap<String, String>,
906}
907
908impl EntryBuilder {
909 pub fn new(url: impl Into<String>) -> Self {
911 EntryBuilder {
912 url: Some(url.into()),
913 matching_pattern: None,
914 version_policy: None,
915 script: None,
916 opts: std::collections::HashMap::new(),
917 }
918 }
919
920 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
922 self.matching_pattern = Some(pattern.into());
923 self
924 }
925
926 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
928 self.version_policy = Some(policy.into());
929 self
930 }
931
932 pub fn script(mut self, script: impl Into<String>) -> Self {
934 self.script = Some(script.into());
935 self
936 }
937
938 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
940 self.opts.insert(key.into(), value.into());
941 self
942 }
943
944 pub fn flag(mut self, key: impl Into<String>) -> Self {
948 self.opts.insert(key.into(), String::new());
949 self
950 }
951
952 pub fn build(self) -> Entry {
958 let url = self.url.expect("URL is required for entry");
959
960 let mut builder = GreenNodeBuilder::new();
961
962 builder.start_node(ENTRY.into());
963
964 if !self.opts.is_empty() {
966 builder.start_node(OPTS_LIST.into());
967 builder.token(KEY.into(), "opts");
968 builder.token(EQUALS.into(), "=");
969
970 let mut first = true;
971 for (key, value) in self.opts.iter() {
972 if !first {
973 builder.token(COMMA.into(), ",");
974 }
975 first = false;
976
977 builder.start_node(OPTION.into());
978 builder.token(KEY.into(), key);
979 if !value.is_empty() {
980 builder.token(EQUALS.into(), "=");
981 builder.token(VALUE.into(), value);
982 }
983 builder.finish_node();
984 }
985
986 builder.finish_node();
987 builder.token(WHITESPACE.into(), " ");
988 }
989
990 builder.start_node(URL.into());
992 builder.token(VALUE.into(), &url);
993 builder.finish_node();
994
995 if let Some(pattern) = self.matching_pattern {
997 builder.token(WHITESPACE.into(), " ");
998 builder.start_node(MATCHING_PATTERN.into());
999 builder.token(VALUE.into(), &pattern);
1000 builder.finish_node();
1001 }
1002
1003 if let Some(policy) = self.version_policy {
1005 builder.token(WHITESPACE.into(), " ");
1006 builder.start_node(VERSION_POLICY.into());
1007 builder.token(VALUE.into(), &policy);
1008 builder.finish_node();
1009 }
1010
1011 if let Some(script_val) = self.script {
1013 builder.token(WHITESPACE.into(), " ");
1014 builder.start_node(SCRIPT.into());
1015 builder.token(VALUE.into(), &script_val);
1016 builder.finish_node();
1017 }
1018
1019 builder.token(NEWLINE.into(), "\n");
1020 builder.finish_node();
1021
1022 Entry(SyntaxNode::new_root_mut(builder.finish()))
1023 }
1024}
1025
1026impl Entry {
1027 #[cfg(feature = "deb822")]
1029 pub(crate) fn syntax(&self) -> &SyntaxNode {
1030 &self.0
1031 }
1032
1033 pub fn builder(url: impl Into<String>) -> EntryBuilder {
1047 EntryBuilder::new(url)
1048 }
1049
1050 pub fn option_list(&self) -> Option<OptionList> {
1052 self.0.children().find_map(OptionList::cast)
1053 }
1054
1055 pub fn get_option(&self, key: &str) -> Option<String> {
1057 self.option_list().and_then(|ol| ol.get_option(key))
1058 }
1059
1060 pub fn has_option(&self, key: &str) -> bool {
1062 self.option_list().is_some_and(|ol| ol.has_option(key))
1063 }
1064
1065 pub fn component(&self) -> Option<String> {
1067 self.get_option("component")
1068 }
1069
1070 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
1072 self.try_ctype().map_err(|_| ())
1073 }
1074
1075 pub fn try_ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
1077 self.get_option("ctype").map(|s| s.parse()).transpose()
1078 }
1079
1080 pub fn compression(&self) -> Result<Option<Compression>, ()> {
1082 self.try_compression().map_err(|_| ())
1083 }
1084
1085 pub fn try_compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
1087 self.get_option("compression")
1088 .map(|s| s.parse())
1089 .transpose()
1090 }
1091
1092 pub fn repack(&self) -> bool {
1094 self.has_option("repack")
1095 }
1096
1097 pub fn repacksuffix(&self) -> Option<String> {
1099 self.get_option("repacksuffix")
1100 }
1101
1102 pub fn mode(&self) -> Result<Mode, ()> {
1104 self.try_mode().map_err(|_| ())
1105 }
1106
1107 pub fn try_mode(&self) -> Result<Mode, crate::types::ParseError> {
1109 Ok(self
1110 .get_option("mode")
1111 .map(|s| s.parse())
1112 .transpose()?
1113 .unwrap_or_default())
1114 }
1115
1116 pub fn pretty(&self) -> Result<Pretty, ()> {
1118 self.try_pretty().map_err(|_| ())
1119 }
1120
1121 pub fn try_pretty(&self) -> Result<Pretty, crate::types::ParseError> {
1123 Ok(self
1124 .get_option("pretty")
1125 .map(|s| s.parse())
1126 .transpose()?
1127 .unwrap_or_default())
1128 }
1129
1130 pub fn date(&self) -> String {
1133 self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
1134 }
1135
1136 pub fn gitexport(&self) -> Result<GitExport, ()> {
1138 self.try_gitexport().map_err(|_| ())
1139 }
1140
1141 pub fn try_gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
1143 Ok(self
1144 .get_option("gitexport")
1145 .map(|s| s.parse())
1146 .transpose()?
1147 .unwrap_or_default())
1148 }
1149
1150 pub fn gitmode(&self) -> Result<GitMode, ()> {
1152 self.try_gitmode().map_err(|_| ())
1153 }
1154
1155 pub fn try_gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
1157 Ok(self
1158 .get_option("gitmode")
1159 .map(|s| s.parse())
1160 .transpose()?
1161 .unwrap_or_default())
1162 }
1163
1164 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
1166 self.try_pgpmode().map_err(|_| ())
1167 }
1168
1169 pub fn try_pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
1171 Ok(self
1172 .get_option("pgpmode")
1173 .map(|s| s.parse())
1174 .transpose()?
1175 .unwrap_or_default())
1176 }
1177
1178 pub fn searchmode(&self) -> Result<SearchMode, ()> {
1180 self.try_searchmode().map_err(|_| ())
1181 }
1182
1183 pub fn try_searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
1185 Ok(self
1186 .get_option("searchmode")
1187 .map(|s| s.parse())
1188 .transpose()?
1189 .unwrap_or_default())
1190 }
1191
1192 pub fn decompress(&self) -> bool {
1194 self.has_option("decompress")
1195 }
1196
1197 pub fn bare(&self) -> bool {
1200 self.has_option("bare")
1201 }
1202
1203 pub fn user_agent(&self) -> Option<String> {
1205 self.get_option("user-agent")
1206 }
1207
1208 pub fn passive(&self) -> Option<bool> {
1210 if self.has_option("passive") || self.has_option("pasv") {
1211 Some(true)
1212 } else if self.has_option("active") || self.has_option("nopasv") {
1213 Some(false)
1214 } else {
1215 None
1216 }
1217 }
1218
1219 pub fn unzipoptions(&self) -> Option<String> {
1222 self.get_option("unzipopt")
1223 }
1224
1225 pub fn dversionmangle(&self) -> Option<String> {
1227 self.get_option("dversionmangle")
1228 .or_else(|| self.get_option("versionmangle"))
1229 }
1230
1231 pub fn dirversionmangle(&self) -> Option<String> {
1235 self.get_option("dirversionmangle")
1236 }
1237
1238 pub fn pagemangle(&self) -> Option<String> {
1240 self.get_option("pagemangle")
1241 }
1242
1243 pub fn uversionmangle(&self) -> Option<String> {
1247 self.get_option("uversionmangle")
1248 .or_else(|| self.get_option("versionmangle"))
1249 }
1250
1251 pub fn versionmangle(&self) -> Option<String> {
1253 self.get_option("versionmangle")
1254 }
1255
1256 pub fn hrefdecode(&self) -> bool {
1261 self.get_option("hrefdecode").is_some()
1262 }
1263
1264 pub fn downloadurlmangle(&self) -> Option<String> {
1267 self.get_option("downloadurlmangle")
1268 }
1269
1270 pub fn filenamemangle(&self) -> Option<String> {
1278 self.get_option("filenamemangle")
1279 }
1280
1281 pub fn pgpsigurlmangle(&self) -> Option<String> {
1283 self.get_option("pgpsigurlmangle")
1284 }
1285
1286 pub fn oversionmangle(&self) -> Option<String> {
1289 self.get_option("oversionmangle")
1290 }
1291
1292 pub fn apply_uversionmangle(
1305 &self,
1306 version: &str,
1307 ) -> Result<String, crate::mangle::MangleError> {
1308 if let Some(vm) = self.uversionmangle() {
1309 crate::mangle::apply_mangle(&vm, version)
1310 } else {
1311 Ok(version.to_string())
1312 }
1313 }
1314
1315 pub fn apply_dversionmangle(
1328 &self,
1329 version: &str,
1330 ) -> Result<String, crate::mangle::MangleError> {
1331 if let Some(vm) = self.dversionmangle() {
1332 crate::mangle::apply_mangle(&vm, version)
1333 } else {
1334 Ok(version.to_string())
1335 }
1336 }
1337
1338 pub fn apply_oversionmangle(
1351 &self,
1352 version: &str,
1353 ) -> Result<String, crate::mangle::MangleError> {
1354 if let Some(vm) = self.oversionmangle() {
1355 crate::mangle::apply_mangle(&vm, version)
1356 } else {
1357 Ok(version.to_string())
1358 }
1359 }
1360
1361 pub fn apply_dirversionmangle(
1374 &self,
1375 version: &str,
1376 ) -> Result<String, crate::mangle::MangleError> {
1377 if let Some(vm) = self.dirversionmangle() {
1378 crate::mangle::apply_mangle(&vm, version)
1379 } else {
1380 Ok(version.to_string())
1381 }
1382 }
1383
1384 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1400 if let Some(vm) = self.filenamemangle() {
1401 crate::mangle::apply_mangle(&vm, url)
1402 } else {
1403 Ok(url.to_string())
1404 }
1405 }
1406
1407 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1423 if let Some(vm) = self.pagemangle() {
1424 let page_str = String::from_utf8_lossy(page);
1425 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1426 Ok(mangled.into_bytes())
1427 } else {
1428 Ok(page.to_vec())
1429 }
1430 }
1431
1432 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1448 if let Some(vm) = self.downloadurlmangle() {
1449 crate::mangle::apply_mangle(&vm, url)
1450 } else {
1451 Ok(url.to_string())
1452 }
1453 }
1454
1455 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1457 let mut options = std::collections::HashMap::new();
1458
1459 if let Some(ol) = self.option_list() {
1460 for opt in ol.options() {
1461 let key = opt.key();
1462 let value = opt.value();
1463 if let (Some(key), Some(value)) = (key, value) {
1464 options.insert(key.to_string(), value.to_string());
1465 }
1466 }
1467 }
1468
1469 options
1470 }
1471
1472 fn items(&self) -> impl Iterator<Item = String> + '_ {
1473 self.0.children_with_tokens().filter_map(|it| match it {
1474 SyntaxElement::Token(token) => {
1475 if token.kind() == VALUE || token.kind() == KEY {
1476 Some(token.text().to_string())
1477 } else {
1478 None
1479 }
1480 }
1481 SyntaxElement::Node(node) => {
1482 match node.kind() {
1484 URL => Url::cast(node).map(|n| n.url()),
1485 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1486 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1487 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1488 _ => None,
1489 }
1490 }
1491 })
1492 }
1493
1494 pub fn url_node(&self) -> Option<Url> {
1496 self.0.children().find_map(Url::cast)
1497 }
1498
1499 pub fn url(&self) -> String {
1501 self.url_node().map(|it| it.url()).unwrap_or_else(|| {
1502 self.items().next().unwrap()
1504 })
1505 }
1506
1507 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1509 self.0.children().find_map(MatchingPattern::cast)
1510 }
1511
1512 pub fn matching_pattern(&self) -> Option<String> {
1514 self.matching_pattern_node()
1515 .map(|it| it.pattern())
1516 .or_else(|| {
1517 self.items().nth(1)
1519 })
1520 }
1521
1522 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1524 self.0.children().find_map(VersionPolicyNode::cast)
1525 }
1526
1527 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1529 self.version_node()
1530 .map(|it| it.policy().parse())
1531 .transpose()
1532 .map_err(|e: crate::types::ParseError| e.to_string())
1533 .or_else(|_e| {
1534 self.items()
1536 .nth(2)
1537 .map(|it| it.parse())
1538 .transpose()
1539 .map_err(|e: crate::types::ParseError| e.to_string())
1540 })
1541 }
1542
1543 pub fn script_node(&self) -> Option<ScriptNode> {
1545 self.0.children().find_map(ScriptNode::cast)
1546 }
1547
1548 pub fn script(&self) -> Option<String> {
1550 self.script_node().map(|it| it.script()).or_else(|| {
1551 self.items().nth(3)
1553 })
1554 }
1555
1556 pub fn format_url(
1558 &self,
1559 package: impl FnOnce() -> String,
1560 component: impl FnOnce() -> String,
1561 ) -> url::Url {
1562 crate::subst::subst(self.url().as_str(), package, component)
1563 .parse()
1564 .unwrap()
1565 }
1566
1567 pub fn set_url(&mut self, new_url: &str) {
1569 let mut builder = GreenNodeBuilder::new();
1571 builder.start_node(URL.into());
1572 builder.token(VALUE.into(), new_url);
1573 builder.finish_node();
1574 let new_url_green = builder.finish();
1575
1576 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1578
1579 let url_pos = self
1581 .0
1582 .children_with_tokens()
1583 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1584
1585 if let Some(pos) = url_pos {
1586 self.0
1588 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1589 }
1590 }
1591
1592 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1598 let mut builder = GreenNodeBuilder::new();
1600 builder.start_node(MATCHING_PATTERN.into());
1601 builder.token(VALUE.into(), new_pattern);
1602 builder.finish_node();
1603 let new_pattern_green = builder.finish();
1604
1605 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1607
1608 let pattern_pos = self.0.children_with_tokens().position(
1610 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1611 );
1612
1613 if let Some(pos) = pattern_pos {
1614 self.0
1616 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1617 }
1618 }
1620
1621 pub fn set_version_policy(&mut self, new_policy: &str) {
1627 let mut builder = GreenNodeBuilder::new();
1629 builder.start_node(VERSION_POLICY.into());
1630 builder.token(VALUE.into(), new_policy);
1632 builder.finish_node();
1633 let new_policy_green = builder.finish();
1634
1635 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1637
1638 let policy_pos = self.0.children_with_tokens().position(
1640 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1641 );
1642
1643 if let Some(pos) = policy_pos {
1644 self.0
1646 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1647 }
1648 }
1650
1651 pub fn set_script(&mut self, new_script: &str) {
1657 let mut builder = GreenNodeBuilder::new();
1659 builder.start_node(SCRIPT.into());
1660 builder.token(VALUE.into(), new_script);
1662 builder.finish_node();
1663 let new_script_green = builder.finish();
1664
1665 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1667
1668 let script_pos = self
1670 .0
1671 .children_with_tokens()
1672 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1673
1674 if let Some(pos) = script_pos {
1675 self.0
1677 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1678 }
1679 }
1681
1682 pub fn set_option(&mut self, option: crate::types::WatchOption) {
1688 let key = watch_option_to_key(&option);
1689 let value = watch_option_to_value(&option);
1690 self.set_opt(key, &value);
1691 }
1692
1693 pub fn set_opt(&mut self, key: &str, value: &str) {
1699 let opts_pos = self.0.children_with_tokens().position(
1701 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1702 );
1703
1704 if let Some(_opts_idx) = opts_pos {
1705 if let Some(mut ol) = self.option_list() {
1706 if let Some(mut opt) = ol.find_option(key) {
1708 opt.set_value(value);
1710 } else {
1712 ol.add_option(key, value);
1714 }
1716 }
1717 } else {
1718 let mut builder = GreenNodeBuilder::new();
1720 builder.start_node(OPTS_LIST.into());
1721 builder.token(KEY.into(), "opts");
1722 builder.token(EQUALS.into(), "=");
1723 builder.start_node(OPTION.into());
1724 builder.token(KEY.into(), key);
1725 builder.token(EQUALS.into(), "=");
1726 builder.token(VALUE.into(), value);
1727 builder.finish_node();
1728 builder.finish_node();
1729 let new_opts_green = builder.finish();
1730 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1731
1732 let url_pos = self
1734 .0
1735 .children_with_tokens()
1736 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1737
1738 if let Some(url_idx) = url_pos {
1739 let mut combined_builder = GreenNodeBuilder::new();
1742 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1744 combined_builder.finish_node();
1745 let temp_green = combined_builder.finish();
1746 let temp_root = SyntaxNode::new_root_mut(temp_green);
1747 let space_element = temp_root.children_with_tokens().next().unwrap();
1748
1749 self.0
1750 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1751 } else {
1752 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1753 }
1754 }
1755 }
1756
1757 pub fn del_opt(&mut self, option: crate::types::WatchOption) {
1764 let key = watch_option_to_key(&option);
1765 if let Some(mut ol) = self.option_list() {
1766 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1767
1768 if option_count == 1 && ol.has_option(key) {
1769 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1771
1772 if let Some(opts_idx) = opts_pos {
1773 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1775
1776 while self.0.children_with_tokens().next().is_some_and(|e| {
1778 matches!(
1779 e,
1780 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1781 )
1782 }) {
1783 self.0.splice_children(0..1, vec![]);
1784 }
1785 }
1786 } else {
1787 ol.remove_option(key);
1789 }
1790 }
1791 }
1792
1793 pub fn del_opt_str(&mut self, key: &str) {
1800 if let Some(mut ol) = self.option_list() {
1801 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1802
1803 if option_count == 1 && ol.has_option(key) {
1804 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1806
1807 if let Some(opts_idx) = opts_pos {
1808 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1810
1811 while self.0.children_with_tokens().next().is_some_and(|e| {
1813 matches!(
1814 e,
1815 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1816 )
1817 }) {
1818 self.0.splice_children(0..1, vec![]);
1819 }
1820 }
1821 } else {
1822 ol.remove_option(key);
1824 }
1825 }
1826 }
1827}
1828
1829impl std::fmt::Debug for OptionList {
1830 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1831 f.debug_struct("OptionList")
1832 .field("text", &self.0.text().to_string())
1833 .finish()
1834 }
1835}
1836
1837impl OptionList {
1838 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1840 self.0.children().filter_map(_Option::cast)
1841 }
1842
1843 pub fn find_option(&self, key: &str) -> Option<_Option> {
1845 self.options().find(|opt| opt.key().as_deref() == Some(key))
1846 }
1847
1848 pub fn has_option(&self, key: &str) -> bool {
1850 self.options().any(|it| it.key().as_deref() == Some(key))
1851 }
1852
1853 #[cfg(feature = "deb822")]
1856 pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
1857 self.options().filter_map(|opt| {
1858 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
1859 Some((key, value))
1860 } else {
1861 None
1862 }
1863 })
1864 }
1865
1866 pub fn get_option(&self, key: &str) -> Option<String> {
1868 for child in self.options() {
1869 if child.key().as_deref() == Some(key) {
1870 return child.value();
1871 }
1872 }
1873 None
1874 }
1875
1876 fn add_option(&mut self, key: &str, value: &str) {
1878 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1879
1880 let mut builder = GreenNodeBuilder::new();
1882 builder.start_node(ROOT.into()); if option_count > 0 {
1885 builder.start_node(OPTION_SEPARATOR.into());
1886 builder.token(COMMA.into(), ",");
1887 builder.finish_node();
1888 }
1889
1890 builder.start_node(OPTION.into());
1891 builder.token(KEY.into(), key);
1892 builder.token(EQUALS.into(), "=");
1893 builder.token(VALUE.into(), value);
1894 builder.finish_node();
1895
1896 builder.finish_node(); let combined_green = builder.finish();
1898
1899 let temp_root = SyntaxNode::new_root_mut(combined_green);
1901 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1902
1903 let insert_pos = self.0.children_with_tokens().count();
1904 self.0.splice_children(insert_pos..insert_pos, new_children);
1905 }
1906
1907 fn remove_option(&mut self, key: &str) -> bool {
1909 if let Some(mut opt) = self.find_option(key) {
1910 opt.remove();
1911 true
1912 } else {
1913 false
1914 }
1915 }
1916}
1917
1918impl _Option {
1919 pub fn key(&self) -> Option<String> {
1921 self.0.children_with_tokens().find_map(|it| match it {
1922 SyntaxElement::Token(token) => {
1923 if token.kind() == KEY {
1924 Some(token.text().to_string())
1925 } else {
1926 None
1927 }
1928 }
1929 _ => None,
1930 })
1931 }
1932
1933 pub fn value(&self) -> Option<String> {
1935 self.0
1936 .children_with_tokens()
1937 .filter_map(|it| match it {
1938 SyntaxElement::Token(token) => {
1939 if token.kind() == VALUE || token.kind() == KEY {
1940 Some(token.text().to_string())
1941 } else {
1942 None
1943 }
1944 }
1945 _ => None,
1946 })
1947 .nth(1)
1948 }
1949
1950 pub fn set_value(&mut self, new_value: &str) {
1952 let key = self.key().expect("Option must have a key");
1953
1954 let mut builder = GreenNodeBuilder::new();
1956 builder.start_node(OPTION.into());
1957 builder.token(KEY.into(), &key);
1958 builder.token(EQUALS.into(), "=");
1959 builder.token(VALUE.into(), new_value);
1960 builder.finish_node();
1961 let new_option_green = builder.finish();
1962 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1963
1964 if let Some(parent) = self.0.parent() {
1966 let idx = self.0.index();
1967 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1968 }
1969 }
1970
1971 pub fn remove(&mut self) {
1973 let next_sep = self
1975 .0
1976 .next_sibling()
1977 .filter(|n| n.kind() == OPTION_SEPARATOR);
1978 let prev_sep = self
1979 .0
1980 .prev_sibling()
1981 .filter(|n| n.kind() == OPTION_SEPARATOR);
1982
1983 if let Some(sep) = next_sep {
1985 sep.detach();
1986 } else if let Some(sep) = prev_sep {
1987 sep.detach();
1988 }
1989
1990 self.0.detach();
1992 }
1993}
1994
1995impl Url {
1996 pub fn url(&self) -> String {
1998 self.0
1999 .children_with_tokens()
2000 .find_map(|it| match it {
2001 SyntaxElement::Token(token) => {
2002 if token.kind() == VALUE {
2003 Some(token.text().to_string())
2004 } else {
2005 None
2006 }
2007 }
2008 _ => None,
2009 })
2010 .unwrap()
2011 }
2012}
2013
2014impl MatchingPattern {
2015 pub fn pattern(&self) -> String {
2017 self.0
2018 .children_with_tokens()
2019 .find_map(|it| match it {
2020 SyntaxElement::Token(token) => {
2021 if token.kind() == VALUE {
2022 Some(token.text().to_string())
2023 } else {
2024 None
2025 }
2026 }
2027 _ => None,
2028 })
2029 .unwrap()
2030 }
2031}
2032
2033impl VersionPolicyNode {
2034 pub fn policy(&self) -> String {
2036 self.0
2037 .children_with_tokens()
2038 .find_map(|it| match it {
2039 SyntaxElement::Token(token) => {
2040 if token.kind() == VALUE || token.kind() == KEY {
2042 Some(token.text().to_string())
2043 } else {
2044 None
2045 }
2046 }
2047 _ => None,
2048 })
2049 .unwrap()
2050 }
2051}
2052
2053impl ScriptNode {
2054 pub fn script(&self) -> String {
2056 self.0
2057 .children_with_tokens()
2058 .find_map(|it| match it {
2059 SyntaxElement::Token(token) => {
2060 if token.kind() == VALUE || token.kind() == KEY {
2062 Some(token.text().to_string())
2063 } else {
2064 None
2065 }
2066 }
2067 _ => None,
2068 })
2069 .unwrap()
2070 }
2071}
2072
2073#[cfg(test)]
2074mod tests {
2075 use super::*;
2076
2077 #[test]
2078 fn test_entry_node_structure() {
2079 let wf: super::WatchFile = r#"version=4
2081opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2082"#
2083 .parse()
2084 .unwrap();
2085
2086 let entry = wf.entries().next().unwrap();
2087
2088 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2090 assert_eq!(entry.url(), "https://example.com/releases");
2091
2092 assert_eq!(
2094 entry
2095 .0
2096 .children()
2097 .find(|n| n.kind() == MATCHING_PATTERN)
2098 .is_some(),
2099 true
2100 );
2101 assert_eq!(
2102 entry.matching_pattern(),
2103 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2104 );
2105
2106 assert_eq!(
2108 entry
2109 .0
2110 .children()
2111 .find(|n| n.kind() == VERSION_POLICY)
2112 .is_some(),
2113 true
2114 );
2115 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2116
2117 assert_eq!(
2119 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2120 true
2121 );
2122 assert_eq!(entry.script(), Some("uupdate".into()));
2123 }
2124
2125 #[test]
2126 fn test_entry_node_structure_partial() {
2127 let wf: super::WatchFile = r#"version=4
2129https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2130"#
2131 .parse()
2132 .unwrap();
2133
2134 let entry = wf.entries().next().unwrap();
2135
2136 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2138 assert_eq!(
2139 entry
2140 .0
2141 .children()
2142 .find(|n| n.kind() == MATCHING_PATTERN)
2143 .is_some(),
2144 true
2145 );
2146
2147 assert_eq!(
2149 entry
2150 .0
2151 .children()
2152 .find(|n| n.kind() == VERSION_POLICY)
2153 .is_some(),
2154 false
2155 );
2156 assert_eq!(
2157 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2158 false
2159 );
2160
2161 assert_eq!(entry.url(), "https://github.com/example/tags");
2163 assert_eq!(
2164 entry.matching_pattern(),
2165 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2166 );
2167 assert_eq!(entry.version(), Ok(None));
2168 assert_eq!(entry.script(), None);
2169 }
2170
2171 #[test]
2172 fn test_parse_v1() {
2173 const WATCHV1: &str = r#"version=4
2174opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2175 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2176"#;
2177 let parsed = parse(WATCHV1);
2178 let node = parsed.syntax();
2180 assert_eq!(
2181 format!("{:#?}", node),
2182 r#"ROOT@0..161
2183 VERSION@0..10
2184 KEY@0..7 "version"
2185 EQUALS@7..8 "="
2186 VALUE@8..9 "4"
2187 NEWLINE@9..10 "\n"
2188 ENTRY@10..161
2189 OPTS_LIST@10..86
2190 KEY@10..14 "opts"
2191 EQUALS@14..15 "="
2192 OPTION@15..19
2193 KEY@15..19 "bare"
2194 OPTION_SEPARATOR@19..20
2195 COMMA@19..20 ","
2196 OPTION@20..86
2197 KEY@20..34 "filenamemangle"
2198 EQUALS@34..35 "="
2199 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2200 WHITESPACE@86..87 " "
2201 CONTINUATION@87..89 "\\\n"
2202 WHITESPACE@89..91 " "
2203 URL@91..138
2204 VALUE@91..138 "https://github.com/sy ..."
2205 WHITESPACE@138..139 " "
2206 MATCHING_PATTERN@139..160
2207 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2208 NEWLINE@160..161 "\n"
2209"#
2210 );
2211
2212 let root = parsed.root();
2213 assert_eq!(root.version(), 4);
2214 let entries = root.entries().collect::<Vec<_>>();
2215 assert_eq!(entries.len(), 1);
2216 let entry = &entries[0];
2217 assert_eq!(
2218 entry.url(),
2219 "https://github.com/syncthing/syncthing-gtk/tags"
2220 );
2221 assert_eq!(
2222 entry.matching_pattern(),
2223 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2224 );
2225 assert_eq!(entry.version(), Ok(None));
2226 assert_eq!(entry.script(), None);
2227
2228 assert_eq!(node.text(), WATCHV1);
2229 }
2230
2231 #[test]
2232 fn test_parse_v2() {
2233 let parsed = parse(
2234 r#"version=4
2235https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2236# comment
2237"#,
2238 );
2239 assert_eq!(parsed.errors, Vec::<String>::new());
2240 let node = parsed.syntax();
2241 assert_eq!(
2242 format!("{:#?}", node),
2243 r###"ROOT@0..90
2244 VERSION@0..10
2245 KEY@0..7 "version"
2246 EQUALS@7..8 "="
2247 VALUE@8..9 "4"
2248 NEWLINE@9..10 "\n"
2249 ENTRY@10..80
2250 URL@10..57
2251 VALUE@10..57 "https://github.com/sy ..."
2252 WHITESPACE@57..58 " "
2253 MATCHING_PATTERN@58..79
2254 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2255 NEWLINE@79..80 "\n"
2256 COMMENT@80..89 "# comment"
2257 NEWLINE@89..90 "\n"
2258"###
2259 );
2260
2261 let root = parsed.root();
2262 assert_eq!(root.version(), 4);
2263 let entries = root.entries().collect::<Vec<_>>();
2264 assert_eq!(entries.len(), 1);
2265 let entry = &entries[0];
2266 assert_eq!(
2267 entry.url(),
2268 "https://github.com/syncthing/syncthing-gtk/tags"
2269 );
2270 assert_eq!(
2271 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2272 "https://github.com/syncthing/syncthing-gtk/tags"
2273 .parse()
2274 .unwrap()
2275 );
2276 }
2277
2278 #[test]
2279 fn test_parse_v3() {
2280 let parsed = parse(
2281 r#"version=4
2282https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2283# comment
2284"#,
2285 );
2286 assert_eq!(parsed.errors, Vec::<String>::new());
2287 let root = parsed.root();
2288 assert_eq!(root.version(), 4);
2289 let entries = root.entries().collect::<Vec<_>>();
2290 assert_eq!(entries.len(), 1);
2291 let entry = &entries[0];
2292 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2293 assert_eq!(
2294 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2295 "https://github.com/syncthing/syncthing-gtk/tags"
2296 .parse()
2297 .unwrap()
2298 );
2299 }
2300
2301 #[test]
2302 fn test_thread_safe_parsing() {
2303 let text = r#"version=4
2304https://github.com/example/example/tags example-(.*)\.tar\.gz
2305"#;
2306
2307 let parsed = parse_watch_file(text);
2308 assert!(parsed.is_ok());
2309 assert_eq!(parsed.errors().len(), 0);
2310
2311 let watchfile = parsed.tree();
2313 assert_eq!(watchfile.version(), 4);
2314 let entries: Vec<_> = watchfile.entries().collect();
2315 assert_eq!(entries.len(), 1);
2316 }
2317
2318 #[test]
2319 fn test_parse_clone_and_eq() {
2320 let text = r#"version=4
2321https://github.com/example/example/tags example-(.*)\.tar\.gz
2322"#;
2323
2324 let parsed1 = parse_watch_file(text);
2325 let parsed2 = parsed1.clone();
2326
2327 assert_eq!(parsed1, parsed2);
2329
2330 let watchfile1 = parsed1.tree();
2332 let watchfile2 = watchfile1.clone();
2333 assert_eq!(watchfile1, watchfile2);
2334 }
2335
2336 #[test]
2337 fn test_parse_v4() {
2338 let cl: super::WatchFile = r#"version=4
2339opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2340 https://github.com/example/example-cat/tags \
2341 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2342"#
2343 .parse()
2344 .unwrap();
2345 assert_eq!(cl.version(), 4);
2346 let entries = cl.entries().collect::<Vec<_>>();
2347 assert_eq!(entries.len(), 1);
2348 let entry = &entries[0];
2349 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2350 assert_eq!(
2351 entry.matching_pattern(),
2352 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2353 );
2354 assert!(entry.repack());
2355 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2356 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2357 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2358 assert_eq!(entry.script(), Some("uupdate".into()));
2359 assert_eq!(
2360 entry.format_url(|| "example-cat".to_string(), || String::new()),
2361 "https://github.com/example/example-cat/tags"
2362 .parse()
2363 .unwrap()
2364 );
2365 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2366 }
2367
2368 #[test]
2369 fn test_git_mode() {
2370 let text = r#"version=3
2371opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2372https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2373refs/tags/(.*) debian
2374"#;
2375 let parsed = parse(text);
2376 assert_eq!(parsed.errors, Vec::<String>::new());
2377 let cl = parsed.root();
2378 assert_eq!(cl.version(), 3);
2379 let entries = cl.entries().collect::<Vec<_>>();
2380 assert_eq!(entries.len(), 1);
2381 let entry = &entries[0];
2382 assert_eq!(
2383 entry.url(),
2384 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2385 );
2386 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2387 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2388 assert_eq!(entry.script(), None);
2389 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2390 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2391 assert_eq!(entry.mode(), Ok(Mode::Git));
2392 }
2393
2394 #[test]
2395 fn test_parse_quoted() {
2396 const WATCHV1: &str = r#"version=4
2397opts="bare, filenamemangle=blah" \
2398 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2399"#;
2400 let parsed = parse(WATCHV1);
2401 let node = parsed.syntax();
2403
2404 let root = parsed.root();
2405 assert_eq!(root.version(), 4);
2406 let entries = root.entries().collect::<Vec<_>>();
2407 assert_eq!(entries.len(), 1);
2408 let entry = &entries[0];
2409
2410 assert_eq!(
2411 entry.url(),
2412 "https://github.com/syncthing/syncthing-gtk/tags"
2413 );
2414 assert_eq!(
2415 entry.matching_pattern(),
2416 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2417 );
2418 assert_eq!(entry.version(), Ok(None));
2419 assert_eq!(entry.script(), None);
2420
2421 assert_eq!(node.text(), WATCHV1);
2422 }
2423
2424 #[test]
2425 fn test_set_url() {
2426 let wf: super::WatchFile = r#"version=4
2428https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2429"#
2430 .parse()
2431 .unwrap();
2432
2433 let mut entry = wf.entries().next().unwrap();
2434 assert_eq!(
2435 entry.url(),
2436 "https://github.com/syncthing/syncthing-gtk/tags"
2437 );
2438
2439 entry.set_url("https://newurl.example.org/path");
2440 assert_eq!(entry.url(), "https://newurl.example.org/path");
2441 assert_eq!(
2442 entry.matching_pattern(),
2443 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2444 );
2445
2446 assert_eq!(
2448 entry.to_string(),
2449 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2450 );
2451 }
2452
2453 #[test]
2454 fn test_set_url_with_options() {
2455 let wf: super::WatchFile = r#"version=4
2457opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2458"#
2459 .parse()
2460 .unwrap();
2461
2462 let mut entry = wf.entries().next().unwrap();
2463 assert_eq!(entry.url(), "https://foo.com/bar");
2464 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2465
2466 entry.set_url("https://example.com/baz");
2467 assert_eq!(entry.url(), "https://example.com/baz");
2468
2469 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2471 assert_eq!(
2472 entry.matching_pattern(),
2473 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2474 );
2475
2476 assert_eq!(
2478 entry.to_string(),
2479 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2480 );
2481 }
2482
2483 #[test]
2484 fn test_set_url_complex() {
2485 let wf: super::WatchFile = r#"version=4
2487opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2488 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2489"#
2490 .parse()
2491 .unwrap();
2492
2493 let mut entry = wf.entries().next().unwrap();
2494 assert_eq!(
2495 entry.url(),
2496 "https://github.com/syncthing/syncthing-gtk/tags"
2497 );
2498
2499 entry.set_url("https://gitlab.com/newproject/tags");
2500 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2501
2502 assert!(entry.bare());
2504 assert_eq!(
2505 entry.filenamemangle(),
2506 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2507 );
2508 assert_eq!(
2509 entry.matching_pattern(),
2510 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2511 );
2512
2513 assert_eq!(
2515 entry.to_string(),
2516 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2517 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2518"#
2519 );
2520 }
2521
2522 #[test]
2523 fn test_set_url_with_all_fields() {
2524 let wf: super::WatchFile = r#"version=4
2526opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2527 https://github.com/example/example-cat/tags \
2528 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2529"#
2530 .parse()
2531 .unwrap();
2532
2533 let mut entry = wf.entries().next().unwrap();
2534 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2535 assert_eq!(
2536 entry.matching_pattern(),
2537 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2538 );
2539 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2540 assert_eq!(entry.script(), Some("uupdate".into()));
2541
2542 entry.set_url("https://gitlab.example.org/project/releases");
2543 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2544
2545 assert!(entry.repack());
2547 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2548 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2549 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2550 assert_eq!(
2551 entry.matching_pattern(),
2552 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2553 );
2554 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2555 assert_eq!(entry.script(), Some("uupdate".into()));
2556
2557 assert_eq!(
2559 entry.to_string(),
2560 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2561 https://gitlab.example.org/project/releases \
2562 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2563"#
2564 );
2565 }
2566
2567 #[test]
2568 fn test_set_url_quoted_options() {
2569 let wf: super::WatchFile = r#"version=4
2571opts="bare, filenamemangle=blah" \
2572 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2573"#
2574 .parse()
2575 .unwrap();
2576
2577 let mut entry = wf.entries().next().unwrap();
2578 assert_eq!(
2579 entry.url(),
2580 "https://github.com/syncthing/syncthing-gtk/tags"
2581 );
2582
2583 entry.set_url("https://example.org/new/path");
2584 assert_eq!(entry.url(), "https://example.org/new/path");
2585
2586 assert_eq!(
2588 entry.to_string(),
2589 r#"opts="bare, filenamemangle=blah" \
2590 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2591"#
2592 );
2593 }
2594
2595 #[test]
2596 fn test_set_opt_update_existing() {
2597 let wf: super::WatchFile = r#"version=4
2599opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2600"#
2601 .parse()
2602 .unwrap();
2603
2604 let mut entry = wf.entries().next().unwrap();
2605 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2606 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2607
2608 entry.set_opt("foo", "updated");
2609 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2610 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2611
2612 assert_eq!(
2614 entry.to_string(),
2615 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2616 );
2617 }
2618
2619 #[test]
2620 fn test_set_opt_add_new() {
2621 let wf: super::WatchFile = r#"version=4
2623opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2624"#
2625 .parse()
2626 .unwrap();
2627
2628 let mut entry = wf.entries().next().unwrap();
2629 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2630 assert_eq!(entry.get_option("bar"), None);
2631
2632 entry.set_opt("bar", "baz");
2633 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2634 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2635
2636 assert_eq!(
2638 entry.to_string(),
2639 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2640 );
2641 }
2642
2643 #[test]
2644 fn test_set_opt_create_options_list() {
2645 let wf: super::WatchFile = r#"version=4
2647https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2648"#
2649 .parse()
2650 .unwrap();
2651
2652 let mut entry = wf.entries().next().unwrap();
2653 assert_eq!(entry.option_list(), None);
2654
2655 entry.set_opt("compression", "xz");
2656 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2657
2658 assert_eq!(
2660 entry.to_string(),
2661 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2662 );
2663 }
2664
2665 #[test]
2666 fn test_del_opt_remove_single() {
2667 let wf: super::WatchFile = r#"version=4
2669opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2670"#
2671 .parse()
2672 .unwrap();
2673
2674 let mut entry = wf.entries().next().unwrap();
2675 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2676 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2677 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2678
2679 entry.del_opt_str("bar");
2680 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2681 assert_eq!(entry.get_option("bar"), None);
2682 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2683
2684 assert_eq!(
2686 entry.to_string(),
2687 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2688 );
2689 }
2690
2691 #[test]
2692 fn test_del_opt_remove_first() {
2693 let wf: super::WatchFile = r#"version=4
2695opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2696"#
2697 .parse()
2698 .unwrap();
2699
2700 let mut entry = wf.entries().next().unwrap();
2701 entry.del_opt_str("foo");
2702 assert_eq!(entry.get_option("foo"), None);
2703 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2704
2705 assert_eq!(
2707 entry.to_string(),
2708 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2709 );
2710 }
2711
2712 #[test]
2713 fn test_del_opt_remove_last() {
2714 let wf: super::WatchFile = r#"version=4
2716opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2717"#
2718 .parse()
2719 .unwrap();
2720
2721 let mut entry = wf.entries().next().unwrap();
2722 entry.del_opt_str("bar");
2723 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2724 assert_eq!(entry.get_option("bar"), None);
2725
2726 assert_eq!(
2728 entry.to_string(),
2729 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2730 );
2731 }
2732
2733 #[test]
2734 fn test_del_opt_remove_only_option() {
2735 let wf: super::WatchFile = r#"version=4
2737opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2738"#
2739 .parse()
2740 .unwrap();
2741
2742 let mut entry = wf.entries().next().unwrap();
2743 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2744
2745 entry.del_opt_str("foo");
2746 assert_eq!(entry.get_option("foo"), None);
2747 assert_eq!(entry.option_list(), None);
2748
2749 assert_eq!(
2751 entry.to_string(),
2752 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2753 );
2754 }
2755
2756 #[test]
2757 fn test_del_opt_nonexistent() {
2758 let wf: super::WatchFile = r#"version=4
2760opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2761"#
2762 .parse()
2763 .unwrap();
2764
2765 let mut entry = wf.entries().next().unwrap();
2766 let original = entry.to_string();
2767
2768 entry.del_opt_str("nonexistent");
2769 assert_eq!(entry.to_string(), original);
2770 }
2771
2772 #[test]
2773 fn test_set_opt_multiple_operations() {
2774 let wf: super::WatchFile = r#"version=4
2776https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2777"#
2778 .parse()
2779 .unwrap();
2780
2781 let mut entry = wf.entries().next().unwrap();
2782
2783 entry.set_opt("compression", "xz");
2784 entry.set_opt("repack", "");
2785 entry.set_opt("dversionmangle", "s/\\+ds//");
2786
2787 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2788 assert_eq!(
2789 entry.get_option("dversionmangle"),
2790 Some("s/\\+ds//".to_string())
2791 );
2792 }
2793
2794 #[test]
2795 fn test_set_matching_pattern() {
2796 let wf: super::WatchFile = r#"version=4
2798https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2799"#
2800 .parse()
2801 .unwrap();
2802
2803 let mut entry = wf.entries().next().unwrap();
2804 assert_eq!(
2805 entry.matching_pattern(),
2806 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2807 );
2808
2809 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2810 assert_eq!(
2811 entry.matching_pattern(),
2812 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2813 );
2814
2815 assert_eq!(entry.url(), "https://github.com/example/tags");
2817
2818 assert_eq!(
2820 entry.to_string(),
2821 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2822 );
2823 }
2824
2825 #[test]
2826 fn test_set_matching_pattern_with_all_fields() {
2827 let wf: super::WatchFile = r#"version=4
2829opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2830"#
2831 .parse()
2832 .unwrap();
2833
2834 let mut entry = wf.entries().next().unwrap();
2835 assert_eq!(
2836 entry.matching_pattern(),
2837 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2838 );
2839
2840 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2841 assert_eq!(
2842 entry.matching_pattern(),
2843 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2844 );
2845
2846 assert_eq!(entry.url(), "https://example.com/releases");
2848 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2849 assert_eq!(entry.script(), Some("uupdate".into()));
2850 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2851
2852 assert_eq!(
2854 entry.to_string(),
2855 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2856 );
2857 }
2858
2859 #[test]
2860 fn test_set_version_policy() {
2861 let wf: super::WatchFile = r#"version=4
2863https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2864"#
2865 .parse()
2866 .unwrap();
2867
2868 let mut entry = wf.entries().next().unwrap();
2869 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2870
2871 entry.set_version_policy("previous");
2872 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2873
2874 assert_eq!(entry.url(), "https://example.com/releases");
2876 assert_eq!(
2877 entry.matching_pattern(),
2878 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2879 );
2880 assert_eq!(entry.script(), Some("uupdate".into()));
2881
2882 assert_eq!(
2884 entry.to_string(),
2885 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2886 );
2887 }
2888
2889 #[test]
2890 fn test_set_version_policy_with_options() {
2891 let wf: super::WatchFile = r#"version=4
2893opts=repack,compression=xz \
2894 https://github.com/example/example-cat/tags \
2895 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2896"#
2897 .parse()
2898 .unwrap();
2899
2900 let mut entry = wf.entries().next().unwrap();
2901 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2902
2903 entry.set_version_policy("ignore");
2904 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2905
2906 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2908 assert_eq!(
2909 entry.matching_pattern(),
2910 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2911 );
2912 assert_eq!(entry.script(), Some("uupdate".into()));
2913 assert!(entry.repack());
2914
2915 assert_eq!(
2917 entry.to_string(),
2918 r#"opts=repack,compression=xz \
2919 https://github.com/example/example-cat/tags \
2920 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2921"#
2922 );
2923 }
2924
2925 #[test]
2926 fn test_set_script() {
2927 let wf: super::WatchFile = r#"version=4
2929https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2930"#
2931 .parse()
2932 .unwrap();
2933
2934 let mut entry = wf.entries().next().unwrap();
2935 assert_eq!(entry.script(), Some("uupdate".into()));
2936
2937 entry.set_script("uscan");
2938 assert_eq!(entry.script(), Some("uscan".into()));
2939
2940 assert_eq!(entry.url(), "https://example.com/releases");
2942 assert_eq!(
2943 entry.matching_pattern(),
2944 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2945 );
2946 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2947
2948 assert_eq!(
2950 entry.to_string(),
2951 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2952 );
2953 }
2954
2955 #[test]
2956 fn test_set_script_with_options() {
2957 let wf: super::WatchFile = r#"version=4
2959opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2960"#
2961 .parse()
2962 .unwrap();
2963
2964 let mut entry = wf.entries().next().unwrap();
2965 assert_eq!(entry.script(), Some("uupdate".into()));
2966
2967 entry.set_script("custom-script.sh");
2968 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2969
2970 assert_eq!(entry.url(), "https://example.com/releases");
2972 assert_eq!(
2973 entry.matching_pattern(),
2974 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2975 );
2976 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2977 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2978
2979 assert_eq!(
2981 entry.to_string(),
2982 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2983 );
2984 }
2985
2986 #[test]
2987 fn test_apply_dversionmangle() {
2988 let wf: super::WatchFile = r#"version=4
2990opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2991"#
2992 .parse()
2993 .unwrap();
2994 let entry = wf.entries().next().unwrap();
2995 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2996 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2997
2998 let wf: super::WatchFile = r#"version=4
3000opts=versionmangle=s/^v// https://example.com/ .*
3001"#
3002 .parse()
3003 .unwrap();
3004 let entry = wf.entries().next().unwrap();
3005 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
3006
3007 let wf: super::WatchFile = r#"version=4
3009opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
3010"#
3011 .parse()
3012 .unwrap();
3013 let entry = wf.entries().next().unwrap();
3014 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
3015
3016 let wf: super::WatchFile = r#"version=4
3018https://example.com/ .*
3019"#
3020 .parse()
3021 .unwrap();
3022 let entry = wf.entries().next().unwrap();
3023 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
3024 }
3025
3026 #[test]
3027 fn test_apply_oversionmangle() {
3028 let wf: super::WatchFile = r#"version=4
3030opts=oversionmangle=s/$/-1/ https://example.com/ .*
3031"#
3032 .parse()
3033 .unwrap();
3034 let entry = wf.entries().next().unwrap();
3035 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
3036 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
3037
3038 let wf: super::WatchFile = r#"version=4
3040opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
3041"#
3042 .parse()
3043 .unwrap();
3044 let entry = wf.entries().next().unwrap();
3045 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
3046
3047 let wf: super::WatchFile = r#"version=4
3049https://example.com/ .*
3050"#
3051 .parse()
3052 .unwrap();
3053 let entry = wf.entries().next().unwrap();
3054 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
3055 }
3056
3057 #[test]
3058 fn test_apply_dirversionmangle() {
3059 let wf: super::WatchFile = r#"version=4
3061opts=dirversionmangle=s/^v// https://example.com/ .*
3062"#
3063 .parse()
3064 .unwrap();
3065 let entry = wf.entries().next().unwrap();
3066 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3067 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
3068
3069 let wf: super::WatchFile = r#"version=4
3071opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
3072"#
3073 .parse()
3074 .unwrap();
3075 let entry = wf.entries().next().unwrap();
3076 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3077
3078 let wf: super::WatchFile = r#"version=4
3080https://example.com/ .*
3081"#
3082 .parse()
3083 .unwrap();
3084 let entry = wf.entries().next().unwrap();
3085 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
3086 }
3087
3088 #[test]
3089 fn test_apply_filenamemangle() {
3090 let wf: super::WatchFile = r#"version=4
3092opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
3093"#
3094 .parse()
3095 .unwrap();
3096 let entry = wf.entries().next().unwrap();
3097 assert_eq!(
3098 entry
3099 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
3100 .unwrap(),
3101 "mypackage-1.0.tar.gz"
3102 );
3103 assert_eq!(
3104 entry
3105 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
3106 .unwrap(),
3107 "mypackage-2.5.3.tar.gz"
3108 );
3109
3110 let wf: super::WatchFile = r#"version=4
3112opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
3113"#
3114 .parse()
3115 .unwrap();
3116 let entry = wf.entries().next().unwrap();
3117 assert_eq!(
3118 entry
3119 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
3120 .unwrap(),
3121 "file.tar.gz"
3122 );
3123
3124 let wf: super::WatchFile = r#"version=4
3126https://example.com/ .*
3127"#
3128 .parse()
3129 .unwrap();
3130 let entry = wf.entries().next().unwrap();
3131 assert_eq!(
3132 entry
3133 .apply_filenamemangle("https://example.com/file.tar.gz")
3134 .unwrap(),
3135 "https://example.com/file.tar.gz"
3136 );
3137 }
3138
3139 #[test]
3140 fn test_apply_pagemangle() {
3141 let wf: super::WatchFile = r#"version=4
3143opts=pagemangle=s/&/&/g https://example.com/ .*
3144"#
3145 .parse()
3146 .unwrap();
3147 let entry = wf.entries().next().unwrap();
3148 assert_eq!(
3149 entry.apply_pagemangle(b"foo & bar").unwrap(),
3150 b"foo & bar"
3151 );
3152 assert_eq!(
3153 entry
3154 .apply_pagemangle(b"& foo & bar &")
3155 .unwrap(),
3156 b"& foo & bar &"
3157 );
3158
3159 let wf: super::WatchFile = r#"version=4
3161opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3162"#
3163 .parse()
3164 .unwrap();
3165 let entry = wf.entries().next().unwrap();
3166 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3167
3168 let wf: super::WatchFile = r#"version=4
3170https://example.com/ .*
3171"#
3172 .parse()
3173 .unwrap();
3174 let entry = wf.entries().next().unwrap();
3175 assert_eq!(
3176 entry.apply_pagemangle(b"foo & bar").unwrap(),
3177 b"foo & bar"
3178 );
3179 }
3180
3181 #[test]
3182 fn test_apply_downloadurlmangle() {
3183 let wf: super::WatchFile = r#"version=4
3185opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3186"#
3187 .parse()
3188 .unwrap();
3189 let entry = wf.entries().next().unwrap();
3190 assert_eq!(
3191 entry
3192 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3193 .unwrap(),
3194 "https://example.com/download/file.tar.gz"
3195 );
3196
3197 let wf: super::WatchFile = r#"version=4
3199opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3200"#
3201 .parse()
3202 .unwrap();
3203 let entry = wf.entries().next().unwrap();
3204 assert_eq!(
3205 entry
3206 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3207 .unwrap(),
3208 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3209 );
3210
3211 let wf: super::WatchFile = r#"version=4
3213https://example.com/ .*
3214"#
3215 .parse()
3216 .unwrap();
3217 let entry = wf.entries().next().unwrap();
3218 assert_eq!(
3219 entry
3220 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3221 .unwrap(),
3222 "https://example.com/archive/file.tar.gz"
3223 );
3224 }
3225
3226 #[test]
3227 fn test_entry_builder_minimal() {
3228 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3230 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3231 .build();
3232
3233 assert_eq!(entry.url(), "https://github.com/example/tags");
3234 assert_eq!(
3235 entry.matching_pattern().as_deref(),
3236 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3237 );
3238 assert_eq!(entry.version(), Ok(None));
3239 assert_eq!(entry.script(), None);
3240 assert!(entry.opts().is_empty());
3241 }
3242
3243 #[test]
3244 fn test_entry_builder_url_only() {
3245 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3247
3248 assert_eq!(entry.url(), "https://example.com/releases");
3249 assert_eq!(entry.matching_pattern(), None);
3250 assert_eq!(entry.version(), Ok(None));
3251 assert_eq!(entry.script(), None);
3252 assert!(entry.opts().is_empty());
3253 }
3254
3255 #[test]
3256 fn test_entry_builder_with_all_fields() {
3257 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3259 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3260 .version_policy("debian")
3261 .script("uupdate")
3262 .opt("compression", "xz")
3263 .flag("repack")
3264 .build();
3265
3266 assert_eq!(entry.url(), "https://github.com/example/tags");
3267 assert_eq!(
3268 entry.matching_pattern().as_deref(),
3269 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3270 );
3271 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3272 assert_eq!(entry.script(), Some("uupdate".into()));
3273 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3274 assert!(entry.has_option("repack"));
3275 assert!(entry.repack());
3276 }
3277
3278 #[test]
3279 fn test_entry_builder_multiple_options() {
3280 let entry = super::EntryBuilder::new("https://example.com/tags")
3282 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3283 .opt("compression", "xz")
3284 .opt("dversionmangle", "s/\\+ds//")
3285 .opt("repacksuffix", "+ds")
3286 .build();
3287
3288 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3289 assert_eq!(
3290 entry.get_option("dversionmangle"),
3291 Some("s/\\+ds//".to_string())
3292 );
3293 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3294 }
3295
3296 #[test]
3297 fn test_entry_builder_via_entry() {
3298 let entry = super::Entry::builder("https://github.com/example/tags")
3300 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3301 .version_policy("debian")
3302 .build();
3303
3304 assert_eq!(entry.url(), "https://github.com/example/tags");
3305 assert_eq!(
3306 entry.matching_pattern().as_deref(),
3307 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3308 );
3309 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3310 }
3311
3312 #[test]
3313 fn test_watchfile_add_entry_to_empty() {
3314 let mut wf = super::WatchFile::new(Some(4));
3316
3317 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3318 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3319 .build();
3320
3321 wf.add_entry(entry);
3322
3323 assert_eq!(wf.version(), 4);
3324 assert_eq!(wf.entries().count(), 1);
3325
3326 let added_entry = wf.entries().next().unwrap();
3327 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3328 assert_eq!(
3329 added_entry.matching_pattern().as_deref(),
3330 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3331 );
3332 }
3333
3334 #[test]
3335 fn test_watchfile_add_multiple_entries() {
3336 let mut wf = super::WatchFile::new(Some(4));
3338
3339 wf.add_entry(
3340 super::EntryBuilder::new("https://github.com/example1/tags")
3341 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3342 .build(),
3343 );
3344
3345 wf.add_entry(
3346 super::EntryBuilder::new("https://github.com/example2/releases")
3347 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3348 .opt("compression", "xz")
3349 .build(),
3350 );
3351
3352 assert_eq!(wf.entries().count(), 2);
3353
3354 let entries: Vec<_> = wf.entries().collect();
3355 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3356 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3357 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3358 }
3359
3360 #[test]
3361 fn test_watchfile_add_entry_to_existing() {
3362 let mut wf: super::WatchFile = r#"version=4
3364https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3365"#
3366 .parse()
3367 .unwrap();
3368
3369 assert_eq!(wf.entries().count(), 1);
3370
3371 wf.add_entry(
3372 super::EntryBuilder::new("https://github.com/example/new")
3373 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3374 .opt("compression", "xz")
3375 .version_policy("debian")
3376 .build(),
3377 );
3378
3379 assert_eq!(wf.entries().count(), 2);
3380
3381 let entries: Vec<_> = wf.entries().collect();
3382 assert_eq!(entries[0].url(), "https://example.com/old");
3383 assert_eq!(entries[1].url(), "https://github.com/example/new");
3384 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3385 }
3386
3387 #[test]
3388 fn test_entry_builder_formatting() {
3389 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3391 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3392 .opt("compression", "xz")
3393 .flag("repack")
3394 .version_policy("debian")
3395 .script("uupdate")
3396 .build();
3397
3398 let entry_str = entry.to_string();
3399
3400 assert!(entry_str.starts_with("opts="));
3402 assert!(entry_str.contains("https://github.com/example/tags"));
3404 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3406 assert!(entry_str.contains("debian"));
3408 assert!(entry_str.contains("uupdate"));
3410 assert!(entry_str.ends_with('\n'));
3412 }
3413
3414 #[test]
3415 fn test_watchfile_add_entry_preserves_format() {
3416 let mut wf = super::WatchFile::new(Some(4));
3418
3419 wf.add_entry(
3420 super::EntryBuilder::new("https://github.com/example/tags")
3421 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3422 .build(),
3423 );
3424
3425 let wf_str = wf.to_string();
3426
3427 assert!(wf_str.starts_with("version=4\n"));
3429 assert!(wf_str.contains("https://github.com/example/tags"));
3431
3432 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3434 assert_eq!(reparsed.version(), 4);
3435 assert_eq!(reparsed.entries().count(), 1);
3436 }
3437
3438 #[test]
3439 fn test_line_col() {
3440 let text = r#"version=4
3441opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3442"#;
3443 let wf = text.parse::<super::WatchFile>().unwrap();
3444
3445 let version_node = wf.version_node().unwrap();
3447 assert_eq!(version_node.line(), 0);
3448 assert_eq!(version_node.column(), 0);
3449 assert_eq!(version_node.line_col(), (0, 0));
3450
3451 let entries: Vec<_> = wf.entries().collect();
3453 assert_eq!(entries.len(), 1);
3454
3455 assert_eq!(entries[0].line(), 1);
3457 assert_eq!(entries[0].column(), 0);
3458 assert_eq!(entries[0].line_col(), (1, 0));
3459
3460 let option_list = entries[0].option_list().unwrap();
3462 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3465 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3468 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3471 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3474 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3478 assert_eq!(options.len(), 1);
3479 assert_eq!(options[0].key(), Some("compression".to_string()));
3480 assert_eq!(options[0].value(), Some("xz".to_string()));
3481 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3485 assert_eq!(compression_opt.line(), 1);
3486 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3488 }
3489
3490 #[test]
3491 fn test_parse_str_relaxed() {
3492 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3493 r#"version=4
3494ERRORS IN THIS LINE
3495opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3496"#,
3497 );
3498 assert_eq!(wf.version(), 4);
3499 assert_eq!(wf.entries().count(), 2);
3500
3501 let entries = wf.entries().collect::<Vec<_>>();
3502
3503 let entry = &entries[0];
3504 assert_eq!(entry.url(), "ERRORS");
3505
3506 let entry = &entries[1];
3507 assert_eq!(entry.url(), "https://example.com/releases");
3508 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3509 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3510 }
3511
3512 #[test]
3513 fn test_parse_entry_with_comment_before() {
3514 let input = concat!(
3518 "version=4\n",
3519 "# try also https://pypi.debian.net/tomoscan/watch\n",
3520 "opts=uversionmangle=s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/ \\\n",
3521 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))\n"
3522 );
3523 let wf: super::WatchFile = input.parse().unwrap();
3524 assert_eq!(wf.to_string(), input);
3526 assert_eq!(wf.entries().count(), 1);
3527 let entry = wf.entries().next().unwrap();
3528 assert_eq!(
3529 entry.url(),
3530 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))"
3531 );
3532 assert_eq!(
3533 entry.get_option("uversionmangle"),
3534 Some("s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/".to_string())
3535 );
3536 }
3537
3538 #[test]
3539 fn test_parse_multiple_comments_before_entry() {
3540 let input = concat!(
3543 "version=4\n",
3544 "# first comment\n",
3545 "# second comment\n",
3546 "# third comment\n",
3547 "https://example.com/foo foo-(.*).tar.gz\n",
3548 );
3549 let wf: super::WatchFile = input.parse().unwrap();
3550 assert_eq!(wf.to_string(), input);
3551 assert_eq!(wf.entries().count(), 1);
3552 assert_eq!(
3553 wf.entries().next().unwrap().url(),
3554 "https://example.com/foo"
3555 );
3556 }
3557
3558 #[test]
3559 fn test_parse_blank_lines_between_entries() {
3560 let input = concat!(
3562 "version=4\n",
3563 "https://example.com/foo .*/foo-(\\d+)\\.tar\\.gz\n",
3564 "\n",
3565 "https://example.com/bar .*/bar-(\\d+)\\.tar\\.gz\n",
3566 );
3567 let wf: super::WatchFile = input.parse().unwrap();
3568 assert_eq!(wf.to_string(), input);
3569 assert_eq!(wf.entries().count(), 2);
3570 }
3571
3572 #[test]
3573 fn test_parse_trailing_unparseable_tokens_produce_error() {
3574 let input = "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n=garbage\n";
3577 let result = input.parse::<super::WatchFile>();
3578 assert!(result.is_err(), "expected parse error for trailing garbage");
3579 let wf = super::WatchFile::from_str_relaxed(input);
3581 assert_eq!(wf.to_string(), input);
3582 }
3583
3584 #[test]
3585 fn test_parse_roundtrip_full_file() {
3586 let inputs = [
3588 "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n",
3589 "version=4\n# a comment\nhttps://example.com/foo foo-(.*).tar.gz\n",
3590 concat!(
3591 "version=4\n",
3592 "opts=uversionmangle=s/rc/~rc/ \\\n",
3593 " https://example.com/foo foo-(.*).tar.gz\n",
3594 ),
3595 concat!(
3596 "version=4\n",
3597 "# comment before entry\n",
3598 "opts=uversionmangle=s/rc/~rc/ \\\n",
3599 "https://example.com/foo foo-(.*).tar.gz\n",
3600 "# comment between entries\n",
3601 "https://example.com/bar bar-(.*).tar.gz\n",
3602 ),
3603 ];
3604 for input in &inputs {
3605 let wf: super::WatchFile = input.parse().unwrap();
3606 assert_eq!(
3607 wf.to_string(),
3608 *input,
3609 "round-trip failed for input: {:?}",
3610 input
3611 );
3612 }
3613 }
3614}