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
166struct InternalParse {
168 green_node: GreenNode,
169 errors: Vec<String>,
170}
171
172fn parse(text: &str) -> InternalParse {
173 struct Parser {
174 tokens: Vec<(SyntaxKind, String)>,
177 builder: GreenNodeBuilder<'static>,
179 errors: Vec<String>,
182 }
183
184 impl Parser {
185 fn parse_version(&mut self) -> Option<u32> {
186 let mut version = None;
187 if self.tokens.last() == Some(&(KEY, "version".to_string())) {
188 self.builder.start_node(VERSION.into());
189 self.bump();
190 self.skip_ws();
191 if self.current() != Some(EQUALS) {
192 self.builder.start_node(ERROR.into());
193 self.errors.push("expected `=`".to_string());
194 self.bump();
195 self.builder.finish_node();
196 } else {
197 self.bump();
198 }
199 if self.current() != Some(VALUE) {
200 self.builder.start_node(ERROR.into());
201 self.errors
202 .push(format!("expected value, got {:?}", self.current()));
203 self.bump();
204 self.builder.finish_node();
205 } else if let Some((_, value)) = self.tokens.last() {
206 let version_str = value;
207 match version_str.parse() {
208 Ok(v) => {
209 version = Some(v);
210 self.bump();
211 }
212 Err(_) => {
213 self.builder.start_node(ERROR.into());
214 self.errors
215 .push(format!("invalid version: {}", version_str));
216 self.bump();
217 self.builder.finish_node();
218 }
219 }
220 } else {
221 self.builder.start_node(ERROR.into());
222 self.errors.push("expected version value".to_string());
223 self.builder.finish_node();
224 }
225 if self.current() != Some(NEWLINE) {
226 self.builder.start_node(ERROR.into());
227 self.errors.push("expected newline".to_string());
228 self.bump();
229 self.builder.finish_node();
230 } else {
231 self.bump();
232 }
233 self.builder.finish_node();
234 }
235 version
236 }
237
238 fn parse_watch_entry(&mut self) -> bool {
239 self.skip_ws();
240 if self.current().is_none() {
241 return false;
242 }
243 if self.current() == Some(NEWLINE) {
244 self.bump();
245 return false;
246 }
247 self.builder.start_node(ENTRY.into());
248 self.parse_options_list();
249 for i in 0..4 {
250 if self.current() == Some(NEWLINE) || self.current().is_none() {
251 break;
252 }
253 if self.current() == Some(CONTINUATION) {
254 self.bump();
255 self.skip_ws();
256 continue;
257 }
258 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
259 self.builder.start_node(ERROR.into());
260 self.errors.push(format!(
261 "expected value, got {:?} (i={})",
262 self.current(),
263 i
264 ));
265 if self.current().is_some() {
266 self.bump();
267 }
268 self.builder.finish_node();
269 } else {
270 match i {
272 0 => {
273 self.builder.start_node(URL.into());
275 self.bump();
276 self.builder.finish_node();
277 }
278 1 => {
279 self.builder.start_node(MATCHING_PATTERN.into());
281 self.bump();
282 self.builder.finish_node();
283 }
284 2 => {
285 self.builder.start_node(VERSION_POLICY.into());
287 self.bump();
288 self.builder.finish_node();
289 }
290 3 => {
291 self.builder.start_node(SCRIPT.into());
293 self.bump();
294 self.builder.finish_node();
295 }
296 _ => {
297 self.bump();
298 }
299 }
300 }
301 self.skip_ws();
302 }
303 if self.current() != Some(NEWLINE) && self.current().is_some() {
304 self.builder.start_node(ERROR.into());
305 self.errors
306 .push(format!("expected newline, not {:?}", self.current()));
307 if self.current().is_some() {
308 self.bump();
309 }
310 self.builder.finish_node();
311 } else if self.current().is_some() {
312 self.bump();
314 }
315 self.builder.finish_node();
316 true
317 }
318
319 fn parse_option(&mut self) -> bool {
320 if self.current().is_none() {
321 return false;
322 }
323 while self.current() == Some(CONTINUATION) {
324 self.bump();
325 }
326 if self.current() == Some(WHITESPACE) {
327 return false;
328 }
329 self.builder.start_node(OPTION.into());
330 if self.current() != Some(KEY) {
331 self.builder.start_node(ERROR.into());
332 self.errors.push("expected key".to_string());
333 self.bump();
334 self.builder.finish_node();
335 } else {
336 self.bump();
337 }
338 if self.current() == Some(EQUALS) {
339 self.bump();
340 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
341 self.builder.start_node(ERROR.into());
342 self.errors
343 .push(format!("expected value, got {:?}", self.current()));
344 self.bump();
345 self.builder.finish_node();
346 } else {
347 self.bump();
348 }
349 } else if self.current() == Some(COMMA) {
350 } else {
351 self.builder.start_node(ERROR.into());
352 self.errors.push("expected `=`".to_string());
353 if self.current().is_some() {
354 self.bump();
355 }
356 self.builder.finish_node();
357 }
358 self.builder.finish_node();
359 true
360 }
361
362 fn parse_options_list(&mut self) {
363 self.skip_ws();
364 if self.tokens.last() == Some(&(KEY, "opts".to_string()))
365 || self.tokens.last() == Some(&(KEY, "options".to_string()))
366 {
367 self.builder.start_node(OPTS_LIST.into());
368 self.bump();
369 self.skip_ws();
370 if self.current() != Some(EQUALS) {
371 self.builder.start_node(ERROR.into());
372 self.errors.push("expected `=`".to_string());
373 if self.current().is_some() {
374 self.bump();
375 }
376 self.builder.finish_node();
377 } else {
378 self.bump();
379 }
380 let quoted = if self.current() == Some(QUOTE) {
381 self.bump();
382 true
383 } else {
384 false
385 };
386 loop {
387 if quoted {
388 if self.current() == Some(QUOTE) {
389 self.bump();
390 break;
391 }
392 self.skip_ws();
393 }
394 if !self.parse_option() {
395 break;
396 }
397 if self.current() == Some(COMMA) {
398 self.builder.start_node(OPTION_SEPARATOR.into());
399 self.bump();
400 self.builder.finish_node();
401 } else if !quoted {
402 break;
403 }
404 }
405 self.builder.finish_node();
406 self.skip_ws();
407 }
408 }
409
410 fn parse(mut self) -> InternalParse {
411 self.builder.start_node(ROOT.into());
413 while self.current() == Some(WHITESPACE)
415 || self.current() == Some(CONTINUATION)
416 || self.current() == Some(COMMENT)
417 || self.current() == Some(NEWLINE)
418 {
419 self.bump();
420 }
421 if let Some(_v) = self.parse_version() {
422 }
424 loop {
426 if !self.parse_watch_entry() {
427 break;
428 }
429 }
430 self.skip_ws();
432 self.builder.finish_node();
434
435 InternalParse {
437 green_node: self.builder.finish(),
438 errors: self.errors,
439 }
440 }
441 fn bump(&mut self) {
443 if let Some((kind, text)) = self.tokens.pop() {
444 self.builder.token(kind.into(), text.as_str());
445 }
446 }
447 fn current(&self) -> Option<SyntaxKind> {
449 self.tokens.last().map(|(kind, _)| *kind)
450 }
451 fn skip_ws(&mut self) {
452 while self.current() == Some(WHITESPACE)
453 || self.current() == Some(CONTINUATION)
454 || self.current() == Some(COMMENT)
455 {
456 self.bump()
457 }
458 }
459 }
460
461 let mut tokens = lex(text);
462 tokens.reverse();
463 Parser {
464 tokens,
465 builder: GreenNodeBuilder::new(),
466 errors: Vec::new(),
467 }
468 .parse()
469}
470
471type SyntaxNode = rowan::SyntaxNode<Lang>;
477#[allow(unused)]
478type SyntaxToken = rowan::SyntaxToken<Lang>;
479#[allow(unused)]
480type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
481
482impl InternalParse {
483 fn syntax(&self) -> SyntaxNode {
484 SyntaxNode::new_root_mut(self.green_node.clone())
485 }
486
487 fn root(&self) -> WatchFile {
488 WatchFile::cast(self.syntax()).expect("root node should be a WatchFile")
489 }
490}
491
492fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
495 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
496 let mut line = 0;
497 let mut last_newline_offset = rowan::TextSize::from(0);
498
499 for element in root.preorder_with_tokens() {
500 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
501 if token.text_range().start() >= offset {
502 break;
503 }
504
505 for (idx, _) in token.text().match_indices('\n') {
507 line += 1;
508 last_newline_offset =
509 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
510 }
511 }
512 }
513
514 let column: usize = (offset - last_newline_offset).into();
515 (line, column)
516}
517
518macro_rules! ast_node {
519 ($ast:ident, $kind:ident) => {
520 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
521 #[repr(transparent)]
522 pub struct $ast(SyntaxNode);
524 impl $ast {
525 #[allow(unused)]
526 fn cast(node: SyntaxNode) -> Option<Self> {
527 if node.kind() == $kind {
528 Some(Self(node))
529 } else {
530 None
531 }
532 }
533
534 pub fn line(&self) -> usize {
536 line_col_at_offset(&self.0, self.0.text_range().start()).0
537 }
538
539 pub fn column(&self) -> usize {
541 line_col_at_offset(&self.0, self.0.text_range().start()).1
542 }
543
544 pub fn line_col(&self) -> (usize, usize) {
547 line_col_at_offset(&self.0, self.0.text_range().start())
548 }
549 }
550
551 impl std::fmt::Display for $ast {
552 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
553 write!(f, "{}", self.0.text())
554 }
555 }
556 };
557}
558
559ast_node!(WatchFile, ROOT);
560ast_node!(Version, VERSION);
561ast_node!(Entry, ENTRY);
562ast_node!(_Option, OPTION);
563ast_node!(Url, URL);
564ast_node!(MatchingPattern, MATCHING_PATTERN);
565ast_node!(VersionPolicyNode, VERSION_POLICY);
566ast_node!(ScriptNode, SCRIPT);
567
568#[derive(Clone, PartialEq, Eq, Hash)]
570#[repr(transparent)]
571pub struct OptionList(SyntaxNode);
573
574impl OptionList {
575 #[allow(unused)]
576 fn cast(node: SyntaxNode) -> Option<Self> {
577 if node.kind() == OPTS_LIST {
578 Some(Self(node))
579 } else {
580 None
581 }
582 }
583
584 pub fn line(&self) -> usize {
586 line_col_at_offset(&self.0, self.0.text_range().start()).0
587 }
588
589 pub fn column(&self) -> usize {
591 line_col_at_offset(&self.0, self.0.text_range().start()).1
592 }
593
594 pub fn line_col(&self) -> (usize, usize) {
597 line_col_at_offset(&self.0, self.0.text_range().start())
598 }
599}
600
601impl std::fmt::Display for OptionList {
602 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
603 write!(f, "{}", self.0.text())
604 }
605}
606
607impl WatchFile {
608 #[cfg(feature = "deb822")]
610 pub(crate) fn syntax(&self) -> &SyntaxNode {
611 &self.0
612 }
613
614 pub fn new(version: Option<u32>) -> WatchFile {
616 let mut builder = GreenNodeBuilder::new();
617
618 builder.start_node(ROOT.into());
619 if let Some(version) = version {
620 builder.start_node(VERSION.into());
621 builder.token(KEY.into(), "version");
622 builder.token(EQUALS.into(), "=");
623 builder.token(VALUE.into(), version.to_string().as_str());
624 builder.token(NEWLINE.into(), "\n");
625 builder.finish_node();
626 }
627 builder.finish_node();
628 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
629 }
630
631 pub fn version_node(&self) -> Option<Version> {
633 self.0.children().find_map(Version::cast)
634 }
635
636 pub fn version(&self) -> u32 {
638 self.version_node()
639 .map(|it| it.version())
640 .unwrap_or(DEFAULT_VERSION)
641 }
642
643 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
645 self.0.children().filter_map(Entry::cast)
646 }
647
648 pub fn set_version(&mut self, new_version: u32) {
650 let mut builder = GreenNodeBuilder::new();
652 builder.start_node(VERSION.into());
653 builder.token(KEY.into(), "version");
654 builder.token(EQUALS.into(), "=");
655 builder.token(VALUE.into(), new_version.to_string().as_str());
656 builder.token(NEWLINE.into(), "\n");
657 builder.finish_node();
658 let new_version_green = builder.finish();
659
660 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
662
663 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
665
666 if let Some(pos) = version_pos {
667 self.0
669 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
670 } else {
671 self.0.splice_children(0..0, vec![new_version_node.into()]);
673 }
674 }
675
676 #[cfg(feature = "discover")]
696 pub async fn uscan(
697 &self,
698 package: impl Fn() -> String + Send + Sync,
699 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
700 let mut all_releases = Vec::new();
701
702 for entry in self.entries() {
703 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
704 let releases = parsed_entry.discover(|| package()).await?;
705 all_releases.push(releases);
706 }
707
708 Ok(all_releases)
709 }
710
711 #[cfg(all(feature = "discover", feature = "blocking"))]
729 pub fn uscan_blocking(
730 &self,
731 package: impl Fn() -> String,
732 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
733 let mut all_releases = Vec::new();
734
735 for entry in self.entries() {
736 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
737 let releases = parsed_entry.discover_blocking(|| package())?;
738 all_releases.push(releases);
739 }
740
741 Ok(all_releases)
742 }
743
744 pub fn add_entry(&mut self, entry: Entry) -> Entry {
771 let insert_pos = self.0.children_with_tokens().count();
773
774 let entry_green = entry.0.green().into_owned();
776 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
777
778 self.0
780 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
781
782 Entry::cast(
784 self.0
785 .children()
786 .nth(insert_pos)
787 .expect("Entry was just inserted"),
788 )
789 .expect("Inserted node should be an Entry")
790 }
791
792 pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
794 let mut buf_reader = std::io::BufReader::new(reader);
795 let mut content = String::new();
796 buf_reader
797 .read_to_string(&mut content)
798 .map_err(|e| ParseError(vec![e.to_string()]))?;
799 content.parse()
800 }
801
802 pub fn from_reader_relaxed<R: std::io::Read>(mut r: R) -> Result<Self, std::io::Error> {
804 let mut content = String::new();
805 r.read_to_string(&mut content)?;
806 let parsed = parse(&content);
807 Ok(parsed.root())
808 }
809
810 pub fn from_str_relaxed(s: &str) -> Self {
812 let parsed = parse(s);
813 parsed.root()
814 }
815}
816
817impl FromStr for WatchFile {
818 type Err = ParseError;
819
820 fn from_str(s: &str) -> Result<Self, Self::Err> {
821 let parsed = parse(s);
822 if parsed.errors.is_empty() {
823 Ok(parsed.root())
824 } else {
825 Err(ParseError(parsed.errors))
826 }
827 }
828}
829
830pub fn parse_watch_file(text: &str) -> Parse<WatchFile> {
833 let parsed = parse(text);
834 Parse::new(parsed.green_node, parsed.errors)
835}
836
837impl Version {
838 pub fn version(&self) -> u32 {
840 self.0
841 .children_with_tokens()
842 .find_map(|it| match it {
843 SyntaxElement::Token(token) => {
844 if token.kind() == VALUE {
845 token.text().parse().ok()
846 } else {
847 None
848 }
849 }
850 _ => None,
851 })
852 .unwrap_or(DEFAULT_VERSION)
853 }
854}
855
856#[derive(Debug, Clone, Default)]
880pub struct EntryBuilder {
881 url: Option<String>,
882 matching_pattern: Option<String>,
883 version_policy: Option<String>,
884 script: Option<String>,
885 opts: std::collections::HashMap<String, String>,
886}
887
888impl EntryBuilder {
889 pub fn new(url: impl Into<String>) -> Self {
891 EntryBuilder {
892 url: Some(url.into()),
893 matching_pattern: None,
894 version_policy: None,
895 script: None,
896 opts: std::collections::HashMap::new(),
897 }
898 }
899
900 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
902 self.matching_pattern = Some(pattern.into());
903 self
904 }
905
906 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
908 self.version_policy = Some(policy.into());
909 self
910 }
911
912 pub fn script(mut self, script: impl Into<String>) -> Self {
914 self.script = Some(script.into());
915 self
916 }
917
918 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
920 self.opts.insert(key.into(), value.into());
921 self
922 }
923
924 pub fn flag(mut self, key: impl Into<String>) -> Self {
928 self.opts.insert(key.into(), String::new());
929 self
930 }
931
932 pub fn build(self) -> Entry {
938 let url = self.url.expect("URL is required for entry");
939
940 let mut builder = GreenNodeBuilder::new();
941
942 builder.start_node(ENTRY.into());
943
944 if !self.opts.is_empty() {
946 builder.start_node(OPTS_LIST.into());
947 builder.token(KEY.into(), "opts");
948 builder.token(EQUALS.into(), "=");
949
950 let mut first = true;
951 for (key, value) in self.opts.iter() {
952 if !first {
953 builder.token(COMMA.into(), ",");
954 }
955 first = false;
956
957 builder.start_node(OPTION.into());
958 builder.token(KEY.into(), key);
959 if !value.is_empty() {
960 builder.token(EQUALS.into(), "=");
961 builder.token(VALUE.into(), value);
962 }
963 builder.finish_node();
964 }
965
966 builder.finish_node();
967 builder.token(WHITESPACE.into(), " ");
968 }
969
970 builder.start_node(URL.into());
972 builder.token(VALUE.into(), &url);
973 builder.finish_node();
974
975 if let Some(pattern) = self.matching_pattern {
977 builder.token(WHITESPACE.into(), " ");
978 builder.start_node(MATCHING_PATTERN.into());
979 builder.token(VALUE.into(), &pattern);
980 builder.finish_node();
981 }
982
983 if let Some(policy) = self.version_policy {
985 builder.token(WHITESPACE.into(), " ");
986 builder.start_node(VERSION_POLICY.into());
987 builder.token(VALUE.into(), &policy);
988 builder.finish_node();
989 }
990
991 if let Some(script_val) = self.script {
993 builder.token(WHITESPACE.into(), " ");
994 builder.start_node(SCRIPT.into());
995 builder.token(VALUE.into(), &script_val);
996 builder.finish_node();
997 }
998
999 builder.token(NEWLINE.into(), "\n");
1000 builder.finish_node();
1001
1002 Entry(SyntaxNode::new_root_mut(builder.finish()))
1003 }
1004}
1005
1006impl Entry {
1007 #[cfg(feature = "deb822")]
1009 pub(crate) fn syntax(&self) -> &SyntaxNode {
1010 &self.0
1011 }
1012
1013 pub fn builder(url: impl Into<String>) -> EntryBuilder {
1027 EntryBuilder::new(url)
1028 }
1029
1030 pub fn option_list(&self) -> Option<OptionList> {
1032 self.0.children().find_map(OptionList::cast)
1033 }
1034
1035 pub fn get_option(&self, key: &str) -> Option<String> {
1037 self.option_list().and_then(|ol| ol.get_option(key))
1038 }
1039
1040 pub fn has_option(&self, key: &str) -> bool {
1042 self.option_list().is_some_and(|ol| ol.has_option(key))
1043 }
1044
1045 pub fn component(&self) -> Option<String> {
1047 self.get_option("component")
1048 }
1049
1050 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
1052 self.try_ctype().map_err(|_| ())
1053 }
1054
1055 pub fn try_ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
1057 self.get_option("ctype").map(|s| s.parse()).transpose()
1058 }
1059
1060 pub fn compression(&self) -> Result<Option<Compression>, ()> {
1062 self.try_compression().map_err(|_| ())
1063 }
1064
1065 pub fn try_compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
1067 self.get_option("compression")
1068 .map(|s| s.parse())
1069 .transpose()
1070 }
1071
1072 pub fn repack(&self) -> bool {
1074 self.has_option("repack")
1075 }
1076
1077 pub fn repacksuffix(&self) -> Option<String> {
1079 self.get_option("repacksuffix")
1080 }
1081
1082 pub fn mode(&self) -> Result<Mode, ()> {
1084 self.try_mode().map_err(|_| ())
1085 }
1086
1087 pub fn try_mode(&self) -> Result<Mode, crate::types::ParseError> {
1089 Ok(self
1090 .get_option("mode")
1091 .map(|s| s.parse())
1092 .transpose()?
1093 .unwrap_or_default())
1094 }
1095
1096 pub fn pretty(&self) -> Result<Pretty, ()> {
1098 self.try_pretty().map_err(|_| ())
1099 }
1100
1101 pub fn try_pretty(&self) -> Result<Pretty, crate::types::ParseError> {
1103 Ok(self
1104 .get_option("pretty")
1105 .map(|s| s.parse())
1106 .transpose()?
1107 .unwrap_or_default())
1108 }
1109
1110 pub fn date(&self) -> String {
1113 self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
1114 }
1115
1116 pub fn gitexport(&self) -> Result<GitExport, ()> {
1118 self.try_gitexport().map_err(|_| ())
1119 }
1120
1121 pub fn try_gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
1123 Ok(self
1124 .get_option("gitexport")
1125 .map(|s| s.parse())
1126 .transpose()?
1127 .unwrap_or_default())
1128 }
1129
1130 pub fn gitmode(&self) -> Result<GitMode, ()> {
1132 self.try_gitmode().map_err(|_| ())
1133 }
1134
1135 pub fn try_gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
1137 Ok(self
1138 .get_option("gitmode")
1139 .map(|s| s.parse())
1140 .transpose()?
1141 .unwrap_or_default())
1142 }
1143
1144 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
1146 self.try_pgpmode().map_err(|_| ())
1147 }
1148
1149 pub fn try_pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
1151 Ok(self
1152 .get_option("pgpmode")
1153 .map(|s| s.parse())
1154 .transpose()?
1155 .unwrap_or_default())
1156 }
1157
1158 pub fn searchmode(&self) -> Result<SearchMode, ()> {
1160 self.try_searchmode().map_err(|_| ())
1161 }
1162
1163 pub fn try_searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
1165 Ok(self
1166 .get_option("searchmode")
1167 .map(|s| s.parse())
1168 .transpose()?
1169 .unwrap_or_default())
1170 }
1171
1172 pub fn decompress(&self) -> bool {
1174 self.has_option("decompress")
1175 }
1176
1177 pub fn bare(&self) -> bool {
1180 self.has_option("bare")
1181 }
1182
1183 pub fn user_agent(&self) -> Option<String> {
1185 self.get_option("user-agent")
1186 }
1187
1188 pub fn passive(&self) -> Option<bool> {
1190 if self.has_option("passive") || self.has_option("pasv") {
1191 Some(true)
1192 } else if self.has_option("active") || self.has_option("nopasv") {
1193 Some(false)
1194 } else {
1195 None
1196 }
1197 }
1198
1199 pub fn unzipoptions(&self) -> Option<String> {
1202 self.get_option("unzipopt")
1203 }
1204
1205 pub fn dversionmangle(&self) -> Option<String> {
1207 self.get_option("dversionmangle")
1208 .or_else(|| self.get_option("versionmangle"))
1209 }
1210
1211 pub fn dirversionmangle(&self) -> Option<String> {
1215 self.get_option("dirversionmangle")
1216 }
1217
1218 pub fn pagemangle(&self) -> Option<String> {
1220 self.get_option("pagemangle")
1221 }
1222
1223 pub fn uversionmangle(&self) -> Option<String> {
1227 self.get_option("uversionmangle")
1228 .or_else(|| self.get_option("versionmangle"))
1229 }
1230
1231 pub fn versionmangle(&self) -> Option<String> {
1233 self.get_option("versionmangle")
1234 }
1235
1236 pub fn hrefdecode(&self) -> bool {
1241 self.get_option("hrefdecode").is_some()
1242 }
1243
1244 pub fn downloadurlmangle(&self) -> Option<String> {
1247 self.get_option("downloadurlmangle")
1248 }
1249
1250 pub fn filenamemangle(&self) -> Option<String> {
1258 self.get_option("filenamemangle")
1259 }
1260
1261 pub fn pgpsigurlmangle(&self) -> Option<String> {
1263 self.get_option("pgpsigurlmangle")
1264 }
1265
1266 pub fn oversionmangle(&self) -> Option<String> {
1269 self.get_option("oversionmangle")
1270 }
1271
1272 pub fn apply_uversionmangle(
1285 &self,
1286 version: &str,
1287 ) -> Result<String, crate::mangle::MangleError> {
1288 if let Some(vm) = self.uversionmangle() {
1289 crate::mangle::apply_mangle(&vm, version)
1290 } else {
1291 Ok(version.to_string())
1292 }
1293 }
1294
1295 pub fn apply_dversionmangle(
1308 &self,
1309 version: &str,
1310 ) -> Result<String, crate::mangle::MangleError> {
1311 if let Some(vm) = self.dversionmangle() {
1312 crate::mangle::apply_mangle(&vm, version)
1313 } else {
1314 Ok(version.to_string())
1315 }
1316 }
1317
1318 pub fn apply_oversionmangle(
1331 &self,
1332 version: &str,
1333 ) -> Result<String, crate::mangle::MangleError> {
1334 if let Some(vm) = self.oversionmangle() {
1335 crate::mangle::apply_mangle(&vm, version)
1336 } else {
1337 Ok(version.to_string())
1338 }
1339 }
1340
1341 pub fn apply_dirversionmangle(
1354 &self,
1355 version: &str,
1356 ) -> Result<String, crate::mangle::MangleError> {
1357 if let Some(vm) = self.dirversionmangle() {
1358 crate::mangle::apply_mangle(&vm, version)
1359 } else {
1360 Ok(version.to_string())
1361 }
1362 }
1363
1364 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1380 if let Some(vm) = self.filenamemangle() {
1381 crate::mangle::apply_mangle(&vm, url)
1382 } else {
1383 Ok(url.to_string())
1384 }
1385 }
1386
1387 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1403 if let Some(vm) = self.pagemangle() {
1404 let page_str = String::from_utf8_lossy(page);
1405 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1406 Ok(mangled.into_bytes())
1407 } else {
1408 Ok(page.to_vec())
1409 }
1410 }
1411
1412 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1428 if let Some(vm) = self.downloadurlmangle() {
1429 crate::mangle::apply_mangle(&vm, url)
1430 } else {
1431 Ok(url.to_string())
1432 }
1433 }
1434
1435 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1437 let mut options = std::collections::HashMap::new();
1438
1439 if let Some(ol) = self.option_list() {
1440 for opt in ol.options() {
1441 let key = opt.key();
1442 let value = opt.value();
1443 if let (Some(key), Some(value)) = (key, value) {
1444 options.insert(key.to_string(), value.to_string());
1445 }
1446 }
1447 }
1448
1449 options
1450 }
1451
1452 fn items(&self) -> impl Iterator<Item = String> + '_ {
1453 self.0.children_with_tokens().filter_map(|it| match it {
1454 SyntaxElement::Token(token) => {
1455 if token.kind() == VALUE || token.kind() == KEY {
1456 Some(token.text().to_string())
1457 } else {
1458 None
1459 }
1460 }
1461 SyntaxElement::Node(node) => {
1462 match node.kind() {
1464 URL => Url::cast(node).map(|n| n.url()),
1465 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1466 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1467 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1468 _ => None,
1469 }
1470 }
1471 })
1472 }
1473
1474 pub fn url_node(&self) -> Option<Url> {
1476 self.0.children().find_map(Url::cast)
1477 }
1478
1479 pub fn url(&self) -> String {
1481 self.url_node().map(|it| it.url()).unwrap_or_else(|| {
1482 self.items().next().unwrap()
1484 })
1485 }
1486
1487 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1489 self.0.children().find_map(MatchingPattern::cast)
1490 }
1491
1492 pub fn matching_pattern(&self) -> Option<String> {
1494 self.matching_pattern_node()
1495 .map(|it| it.pattern())
1496 .or_else(|| {
1497 self.items().nth(1)
1499 })
1500 }
1501
1502 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1504 self.0.children().find_map(VersionPolicyNode::cast)
1505 }
1506
1507 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1509 self.version_node()
1510 .map(|it| it.policy().parse())
1511 .transpose()
1512 .map_err(|e: crate::types::ParseError| e.to_string())
1513 .or_else(|_e| {
1514 self.items()
1516 .nth(2)
1517 .map(|it| it.parse())
1518 .transpose()
1519 .map_err(|e: crate::types::ParseError| e.to_string())
1520 })
1521 }
1522
1523 pub fn script_node(&self) -> Option<ScriptNode> {
1525 self.0.children().find_map(ScriptNode::cast)
1526 }
1527
1528 pub fn script(&self) -> Option<String> {
1530 self.script_node().map(|it| it.script()).or_else(|| {
1531 self.items().nth(3)
1533 })
1534 }
1535
1536 pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
1538 crate::subst::subst(self.url().as_str(), package)
1539 .parse()
1540 .unwrap()
1541 }
1542
1543 pub fn set_url(&mut self, new_url: &str) {
1545 let mut builder = GreenNodeBuilder::new();
1547 builder.start_node(URL.into());
1548 builder.token(VALUE.into(), new_url);
1549 builder.finish_node();
1550 let new_url_green = builder.finish();
1551
1552 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1554
1555 let url_pos = self
1557 .0
1558 .children_with_tokens()
1559 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1560
1561 if let Some(pos) = url_pos {
1562 self.0
1564 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1565 }
1566 }
1567
1568 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1574 let mut builder = GreenNodeBuilder::new();
1576 builder.start_node(MATCHING_PATTERN.into());
1577 builder.token(VALUE.into(), new_pattern);
1578 builder.finish_node();
1579 let new_pattern_green = builder.finish();
1580
1581 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1583
1584 let pattern_pos = self.0.children_with_tokens().position(
1586 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1587 );
1588
1589 if let Some(pos) = pattern_pos {
1590 self.0
1592 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1593 }
1594 }
1596
1597 pub fn set_version_policy(&mut self, new_policy: &str) {
1603 let mut builder = GreenNodeBuilder::new();
1605 builder.start_node(VERSION_POLICY.into());
1606 builder.token(VALUE.into(), new_policy);
1608 builder.finish_node();
1609 let new_policy_green = builder.finish();
1610
1611 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1613
1614 let policy_pos = self.0.children_with_tokens().position(
1616 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1617 );
1618
1619 if let Some(pos) = policy_pos {
1620 self.0
1622 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1623 }
1624 }
1626
1627 pub fn set_script(&mut self, new_script: &str) {
1633 let mut builder = GreenNodeBuilder::new();
1635 builder.start_node(SCRIPT.into());
1636 builder.token(VALUE.into(), new_script);
1638 builder.finish_node();
1639 let new_script_green = builder.finish();
1640
1641 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1643
1644 let script_pos = self
1646 .0
1647 .children_with_tokens()
1648 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1649
1650 if let Some(pos) = script_pos {
1651 self.0
1653 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1654 }
1655 }
1657
1658 pub fn set_option(&mut self, option: crate::types::WatchOption) {
1664 let key = watch_option_to_key(&option);
1665 let value = watch_option_to_value(&option);
1666 self.set_opt(key, &value);
1667 }
1668
1669 pub fn set_opt(&mut self, key: &str, value: &str) {
1675 let opts_pos = self.0.children_with_tokens().position(
1677 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1678 );
1679
1680 if let Some(_opts_idx) = opts_pos {
1681 if let Some(mut ol) = self.option_list() {
1682 if let Some(mut opt) = ol.find_option(key) {
1684 opt.set_value(value);
1686 } else {
1688 ol.add_option(key, value);
1690 }
1692 }
1693 } else {
1694 let mut builder = GreenNodeBuilder::new();
1696 builder.start_node(OPTS_LIST.into());
1697 builder.token(KEY.into(), "opts");
1698 builder.token(EQUALS.into(), "=");
1699 builder.start_node(OPTION.into());
1700 builder.token(KEY.into(), key);
1701 builder.token(EQUALS.into(), "=");
1702 builder.token(VALUE.into(), value);
1703 builder.finish_node();
1704 builder.finish_node();
1705 let new_opts_green = builder.finish();
1706 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1707
1708 let url_pos = self
1710 .0
1711 .children_with_tokens()
1712 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1713
1714 if let Some(url_idx) = url_pos {
1715 let mut combined_builder = GreenNodeBuilder::new();
1718 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1720 combined_builder.finish_node();
1721 let temp_green = combined_builder.finish();
1722 let temp_root = SyntaxNode::new_root_mut(temp_green);
1723 let space_element = temp_root.children_with_tokens().next().unwrap();
1724
1725 self.0
1726 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1727 } else {
1728 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1729 }
1730 }
1731 }
1732
1733 pub fn del_opt(&mut self, option: crate::types::WatchOption) {
1740 let key = watch_option_to_key(&option);
1741 if let Some(mut ol) = self.option_list() {
1742 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1743
1744 if option_count == 1 && ol.has_option(key) {
1745 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1747
1748 if let Some(opts_idx) = opts_pos {
1749 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1751
1752 while self.0.children_with_tokens().next().map_or(false, |e| {
1754 matches!(
1755 e,
1756 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1757 )
1758 }) {
1759 self.0.splice_children(0..1, vec![]);
1760 }
1761 }
1762 } else {
1763 ol.remove_option(key);
1765 }
1766 }
1767 }
1768
1769 pub fn del_opt_str(&mut self, key: &str) {
1776 if let Some(mut ol) = self.option_list() {
1777 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1778
1779 if option_count == 1 && ol.has_option(key) {
1780 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1782
1783 if let Some(opts_idx) = opts_pos {
1784 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1786
1787 while self.0.children_with_tokens().next().map_or(false, |e| {
1789 matches!(
1790 e,
1791 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1792 )
1793 }) {
1794 self.0.splice_children(0..1, vec![]);
1795 }
1796 }
1797 } else {
1798 ol.remove_option(key);
1800 }
1801 }
1802 }
1803}
1804
1805impl std::fmt::Debug for OptionList {
1806 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1807 f.debug_struct("OptionList")
1808 .field("text", &self.0.text().to_string())
1809 .finish()
1810 }
1811}
1812
1813impl OptionList {
1814 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1816 self.0.children().filter_map(_Option::cast)
1817 }
1818
1819 pub fn find_option(&self, key: &str) -> Option<_Option> {
1821 self.options().find(|opt| opt.key().as_deref() == Some(key))
1822 }
1823
1824 pub fn has_option(&self, key: &str) -> bool {
1826 self.options().any(|it| it.key().as_deref() == Some(key))
1827 }
1828
1829 #[cfg(feature = "deb822")]
1832 pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
1833 self.options().filter_map(|opt| {
1834 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
1835 Some((key, value))
1836 } else {
1837 None
1838 }
1839 })
1840 }
1841
1842 pub fn get_option(&self, key: &str) -> Option<String> {
1844 for child in self.options() {
1845 if child.key().as_deref() == Some(key) {
1846 return child.value();
1847 }
1848 }
1849 None
1850 }
1851
1852 fn add_option(&mut self, key: &str, value: &str) {
1854 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1855
1856 let mut builder = GreenNodeBuilder::new();
1858 builder.start_node(ROOT.into()); if option_count > 0 {
1861 builder.start_node(OPTION_SEPARATOR.into());
1862 builder.token(COMMA.into(), ",");
1863 builder.finish_node();
1864 }
1865
1866 builder.start_node(OPTION.into());
1867 builder.token(KEY.into(), key);
1868 builder.token(EQUALS.into(), "=");
1869 builder.token(VALUE.into(), value);
1870 builder.finish_node();
1871
1872 builder.finish_node(); let combined_green = builder.finish();
1874
1875 let temp_root = SyntaxNode::new_root_mut(combined_green);
1877 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1878
1879 let insert_pos = self.0.children_with_tokens().count();
1880 self.0.splice_children(insert_pos..insert_pos, new_children);
1881 }
1882
1883 fn remove_option(&mut self, key: &str) -> bool {
1885 if let Some(mut opt) = self.find_option(key) {
1886 opt.remove();
1887 true
1888 } else {
1889 false
1890 }
1891 }
1892}
1893
1894impl _Option {
1895 pub fn key(&self) -> Option<String> {
1897 self.0.children_with_tokens().find_map(|it| match it {
1898 SyntaxElement::Token(token) => {
1899 if token.kind() == KEY {
1900 Some(token.text().to_string())
1901 } else {
1902 None
1903 }
1904 }
1905 _ => None,
1906 })
1907 }
1908
1909 pub fn value(&self) -> Option<String> {
1911 self.0
1912 .children_with_tokens()
1913 .filter_map(|it| match it {
1914 SyntaxElement::Token(token) => {
1915 if token.kind() == VALUE || token.kind() == KEY {
1916 Some(token.text().to_string())
1917 } else {
1918 None
1919 }
1920 }
1921 _ => None,
1922 })
1923 .nth(1)
1924 }
1925
1926 pub fn set_value(&mut self, new_value: &str) {
1928 let key = self.key().expect("Option must have a key");
1929
1930 let mut builder = GreenNodeBuilder::new();
1932 builder.start_node(OPTION.into());
1933 builder.token(KEY.into(), &key);
1934 builder.token(EQUALS.into(), "=");
1935 builder.token(VALUE.into(), new_value);
1936 builder.finish_node();
1937 let new_option_green = builder.finish();
1938 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1939
1940 if let Some(parent) = self.0.parent() {
1942 let idx = self.0.index();
1943 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1944 }
1945 }
1946
1947 pub fn remove(&mut self) {
1949 let next_sep = self
1951 .0
1952 .next_sibling()
1953 .filter(|n| n.kind() == OPTION_SEPARATOR);
1954 let prev_sep = self
1955 .0
1956 .prev_sibling()
1957 .filter(|n| n.kind() == OPTION_SEPARATOR);
1958
1959 if let Some(sep) = next_sep {
1961 sep.detach();
1962 } else if let Some(sep) = prev_sep {
1963 sep.detach();
1964 }
1965
1966 self.0.detach();
1968 }
1969}
1970
1971impl Url {
1972 pub fn url(&self) -> String {
1974 self.0
1975 .children_with_tokens()
1976 .find_map(|it| match it {
1977 SyntaxElement::Token(token) => {
1978 if token.kind() == VALUE {
1979 Some(token.text().to_string())
1980 } else {
1981 None
1982 }
1983 }
1984 _ => None,
1985 })
1986 .unwrap()
1987 }
1988}
1989
1990impl MatchingPattern {
1991 pub fn pattern(&self) -> String {
1993 self.0
1994 .children_with_tokens()
1995 .find_map(|it| match it {
1996 SyntaxElement::Token(token) => {
1997 if token.kind() == VALUE {
1998 Some(token.text().to_string())
1999 } else {
2000 None
2001 }
2002 }
2003 _ => None,
2004 })
2005 .unwrap()
2006 }
2007}
2008
2009impl VersionPolicyNode {
2010 pub fn policy(&self) -> String {
2012 self.0
2013 .children_with_tokens()
2014 .find_map(|it| match it {
2015 SyntaxElement::Token(token) => {
2016 if token.kind() == VALUE || token.kind() == KEY {
2018 Some(token.text().to_string())
2019 } else {
2020 None
2021 }
2022 }
2023 _ => None,
2024 })
2025 .unwrap()
2026 }
2027}
2028
2029impl ScriptNode {
2030 pub fn script(&self) -> String {
2032 self.0
2033 .children_with_tokens()
2034 .find_map(|it| match it {
2035 SyntaxElement::Token(token) => {
2036 if token.kind() == VALUE || token.kind() == KEY {
2038 Some(token.text().to_string())
2039 } else {
2040 None
2041 }
2042 }
2043 _ => None,
2044 })
2045 .unwrap()
2046 }
2047}
2048
2049#[cfg(test)]
2050mod tests {
2051 use super::*;
2052
2053 #[test]
2054 fn test_entry_node_structure() {
2055 let wf: super::WatchFile = r#"version=4
2057opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2058"#
2059 .parse()
2060 .unwrap();
2061
2062 let entry = wf.entries().next().unwrap();
2063
2064 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2066 assert_eq!(entry.url(), "https://example.com/releases");
2067
2068 assert_eq!(
2070 entry
2071 .0
2072 .children()
2073 .find(|n| n.kind() == MATCHING_PATTERN)
2074 .is_some(),
2075 true
2076 );
2077 assert_eq!(
2078 entry.matching_pattern(),
2079 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2080 );
2081
2082 assert_eq!(
2084 entry
2085 .0
2086 .children()
2087 .find(|n| n.kind() == VERSION_POLICY)
2088 .is_some(),
2089 true
2090 );
2091 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2092
2093 assert_eq!(
2095 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2096 true
2097 );
2098 assert_eq!(entry.script(), Some("uupdate".into()));
2099 }
2100
2101 #[test]
2102 fn test_entry_node_structure_partial() {
2103 let wf: super::WatchFile = r#"version=4
2105https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2106"#
2107 .parse()
2108 .unwrap();
2109
2110 let entry = wf.entries().next().unwrap();
2111
2112 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2114 assert_eq!(
2115 entry
2116 .0
2117 .children()
2118 .find(|n| n.kind() == MATCHING_PATTERN)
2119 .is_some(),
2120 true
2121 );
2122
2123 assert_eq!(
2125 entry
2126 .0
2127 .children()
2128 .find(|n| n.kind() == VERSION_POLICY)
2129 .is_some(),
2130 false
2131 );
2132 assert_eq!(
2133 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2134 false
2135 );
2136
2137 assert_eq!(entry.url(), "https://github.com/example/tags");
2139 assert_eq!(
2140 entry.matching_pattern(),
2141 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2142 );
2143 assert_eq!(entry.version(), Ok(None));
2144 assert_eq!(entry.script(), None);
2145 }
2146
2147 #[test]
2148 fn test_parse_v1() {
2149 const WATCHV1: &str = r#"version=4
2150opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2151 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2152"#;
2153 let parsed = parse(WATCHV1);
2154 let node = parsed.syntax();
2156 assert_eq!(
2157 format!("{:#?}", node),
2158 r#"ROOT@0..161
2159 VERSION@0..10
2160 KEY@0..7 "version"
2161 EQUALS@7..8 "="
2162 VALUE@8..9 "4"
2163 NEWLINE@9..10 "\n"
2164 ENTRY@10..161
2165 OPTS_LIST@10..86
2166 KEY@10..14 "opts"
2167 EQUALS@14..15 "="
2168 OPTION@15..19
2169 KEY@15..19 "bare"
2170 OPTION_SEPARATOR@19..20
2171 COMMA@19..20 ","
2172 OPTION@20..86
2173 KEY@20..34 "filenamemangle"
2174 EQUALS@34..35 "="
2175 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2176 WHITESPACE@86..87 " "
2177 CONTINUATION@87..89 "\\\n"
2178 WHITESPACE@89..91 " "
2179 URL@91..138
2180 VALUE@91..138 "https://github.com/sy ..."
2181 WHITESPACE@138..139 " "
2182 MATCHING_PATTERN@139..160
2183 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2184 NEWLINE@160..161 "\n"
2185"#
2186 );
2187
2188 let root = parsed.root();
2189 assert_eq!(root.version(), 4);
2190 let entries = root.entries().collect::<Vec<_>>();
2191 assert_eq!(entries.len(), 1);
2192 let entry = &entries[0];
2193 assert_eq!(
2194 entry.url(),
2195 "https://github.com/syncthing/syncthing-gtk/tags"
2196 );
2197 assert_eq!(
2198 entry.matching_pattern(),
2199 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2200 );
2201 assert_eq!(entry.version(), Ok(None));
2202 assert_eq!(entry.script(), None);
2203
2204 assert_eq!(node.text(), WATCHV1);
2205 }
2206
2207 #[test]
2208 fn test_parse_v2() {
2209 let parsed = parse(
2210 r#"version=4
2211https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2212# comment
2213"#,
2214 );
2215 assert_eq!(parsed.errors, Vec::<String>::new());
2216 let node = parsed.syntax();
2217 assert_eq!(
2218 format!("{:#?}", node),
2219 r###"ROOT@0..90
2220 VERSION@0..10
2221 KEY@0..7 "version"
2222 EQUALS@7..8 "="
2223 VALUE@8..9 "4"
2224 NEWLINE@9..10 "\n"
2225 ENTRY@10..80
2226 URL@10..57
2227 VALUE@10..57 "https://github.com/sy ..."
2228 WHITESPACE@57..58 " "
2229 MATCHING_PATTERN@58..79
2230 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2231 NEWLINE@79..80 "\n"
2232 COMMENT@80..89 "# comment"
2233 NEWLINE@89..90 "\n"
2234"###
2235 );
2236
2237 let root = parsed.root();
2238 assert_eq!(root.version(), 4);
2239 let entries = root.entries().collect::<Vec<_>>();
2240 assert_eq!(entries.len(), 1);
2241 let entry = &entries[0];
2242 assert_eq!(
2243 entry.url(),
2244 "https://github.com/syncthing/syncthing-gtk/tags"
2245 );
2246 assert_eq!(
2247 entry.format_url(|| "syncthing-gtk".to_string()),
2248 "https://github.com/syncthing/syncthing-gtk/tags"
2249 .parse()
2250 .unwrap()
2251 );
2252 }
2253
2254 #[test]
2255 fn test_parse_v3() {
2256 let parsed = parse(
2257 r#"version=4
2258https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2259# comment
2260"#,
2261 );
2262 assert_eq!(parsed.errors, Vec::<String>::new());
2263 let root = parsed.root();
2264 assert_eq!(root.version(), 4);
2265 let entries = root.entries().collect::<Vec<_>>();
2266 assert_eq!(entries.len(), 1);
2267 let entry = &entries[0];
2268 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2269 assert_eq!(
2270 entry.format_url(|| "syncthing-gtk".to_string()),
2271 "https://github.com/syncthing/syncthing-gtk/tags"
2272 .parse()
2273 .unwrap()
2274 );
2275 }
2276
2277 #[test]
2278 fn test_thread_safe_parsing() {
2279 let text = r#"version=4
2280https://github.com/example/example/tags example-(.*)\.tar\.gz
2281"#;
2282
2283 let parsed = parse_watch_file(text);
2284 assert!(parsed.is_ok());
2285 assert_eq!(parsed.errors().len(), 0);
2286
2287 let watchfile = parsed.tree();
2289 assert_eq!(watchfile.version(), 4);
2290 let entries: Vec<_> = watchfile.entries().collect();
2291 assert_eq!(entries.len(), 1);
2292 }
2293
2294 #[test]
2295 fn test_parse_clone_and_eq() {
2296 let text = r#"version=4
2297https://github.com/example/example/tags example-(.*)\.tar\.gz
2298"#;
2299
2300 let parsed1 = parse_watch_file(text);
2301 let parsed2 = parsed1.clone();
2302
2303 assert_eq!(parsed1, parsed2);
2305
2306 let watchfile1 = parsed1.tree();
2308 let watchfile2 = watchfile1.clone();
2309 assert_eq!(watchfile1, watchfile2);
2310 }
2311
2312 #[test]
2313 fn test_parse_v4() {
2314 let cl: super::WatchFile = r#"version=4
2315opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2316 https://github.com/example/example-cat/tags \
2317 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2318"#
2319 .parse()
2320 .unwrap();
2321 assert_eq!(cl.version(), 4);
2322 let entries = cl.entries().collect::<Vec<_>>();
2323 assert_eq!(entries.len(), 1);
2324 let entry = &entries[0];
2325 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2326 assert_eq!(
2327 entry.matching_pattern(),
2328 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2329 );
2330 assert!(entry.repack());
2331 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2332 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2333 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2334 assert_eq!(entry.script(), Some("uupdate".into()));
2335 assert_eq!(
2336 entry.format_url(|| "example-cat".to_string()),
2337 "https://github.com/example/example-cat/tags"
2338 .parse()
2339 .unwrap()
2340 );
2341 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2342 }
2343
2344 #[test]
2345 fn test_git_mode() {
2346 let text = r#"version=3
2347opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2348https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2349refs/tags/(.*) debian
2350"#;
2351 let parsed = parse(text);
2352 assert_eq!(parsed.errors, Vec::<String>::new());
2353 let cl = parsed.root();
2354 assert_eq!(cl.version(), 3);
2355 let entries = cl.entries().collect::<Vec<_>>();
2356 assert_eq!(entries.len(), 1);
2357 let entry = &entries[0];
2358 assert_eq!(
2359 entry.url(),
2360 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2361 );
2362 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2363 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2364 assert_eq!(entry.script(), None);
2365 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2366 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2367 assert_eq!(entry.mode(), Ok(Mode::Git));
2368 }
2369
2370 #[test]
2371 fn test_parse_quoted() {
2372 const WATCHV1: &str = r#"version=4
2373opts="bare, filenamemangle=blah" \
2374 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2375"#;
2376 let parsed = parse(WATCHV1);
2377 let node = parsed.syntax();
2379
2380 let root = parsed.root();
2381 assert_eq!(root.version(), 4);
2382 let entries = root.entries().collect::<Vec<_>>();
2383 assert_eq!(entries.len(), 1);
2384 let entry = &entries[0];
2385
2386 assert_eq!(
2387 entry.url(),
2388 "https://github.com/syncthing/syncthing-gtk/tags"
2389 );
2390 assert_eq!(
2391 entry.matching_pattern(),
2392 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2393 );
2394 assert_eq!(entry.version(), Ok(None));
2395 assert_eq!(entry.script(), None);
2396
2397 assert_eq!(node.text(), WATCHV1);
2398 }
2399
2400 #[test]
2401 fn test_set_url() {
2402 let wf: super::WatchFile = r#"version=4
2404https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2405"#
2406 .parse()
2407 .unwrap();
2408
2409 let mut entry = wf.entries().next().unwrap();
2410 assert_eq!(
2411 entry.url(),
2412 "https://github.com/syncthing/syncthing-gtk/tags"
2413 );
2414
2415 entry.set_url("https://newurl.example.org/path");
2416 assert_eq!(entry.url(), "https://newurl.example.org/path");
2417 assert_eq!(
2418 entry.matching_pattern(),
2419 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2420 );
2421
2422 assert_eq!(
2424 entry.to_string(),
2425 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2426 );
2427 }
2428
2429 #[test]
2430 fn test_set_url_with_options() {
2431 let wf: super::WatchFile = r#"version=4
2433opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2434"#
2435 .parse()
2436 .unwrap();
2437
2438 let mut entry = wf.entries().next().unwrap();
2439 assert_eq!(entry.url(), "https://foo.com/bar");
2440 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2441
2442 entry.set_url("https://example.com/baz");
2443 assert_eq!(entry.url(), "https://example.com/baz");
2444
2445 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2447 assert_eq!(
2448 entry.matching_pattern(),
2449 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2450 );
2451
2452 assert_eq!(
2454 entry.to_string(),
2455 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2456 );
2457 }
2458
2459 #[test]
2460 fn test_set_url_complex() {
2461 let wf: super::WatchFile = r#"version=4
2463opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2464 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2465"#
2466 .parse()
2467 .unwrap();
2468
2469 let mut entry = wf.entries().next().unwrap();
2470 assert_eq!(
2471 entry.url(),
2472 "https://github.com/syncthing/syncthing-gtk/tags"
2473 );
2474
2475 entry.set_url("https://gitlab.com/newproject/tags");
2476 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2477
2478 assert!(entry.bare());
2480 assert_eq!(
2481 entry.filenamemangle(),
2482 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2483 );
2484 assert_eq!(
2485 entry.matching_pattern(),
2486 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2487 );
2488
2489 assert_eq!(
2491 entry.to_string(),
2492 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2493 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2494"#
2495 );
2496 }
2497
2498 #[test]
2499 fn test_set_url_with_all_fields() {
2500 let wf: super::WatchFile = r#"version=4
2502opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2503 https://github.com/example/example-cat/tags \
2504 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2505"#
2506 .parse()
2507 .unwrap();
2508
2509 let mut entry = wf.entries().next().unwrap();
2510 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2511 assert_eq!(
2512 entry.matching_pattern(),
2513 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2514 );
2515 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2516 assert_eq!(entry.script(), Some("uupdate".into()));
2517
2518 entry.set_url("https://gitlab.example.org/project/releases");
2519 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2520
2521 assert!(entry.repack());
2523 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2524 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2525 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2526 assert_eq!(
2527 entry.matching_pattern(),
2528 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2529 );
2530 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2531 assert_eq!(entry.script(), Some("uupdate".into()));
2532
2533 assert_eq!(
2535 entry.to_string(),
2536 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2537 https://gitlab.example.org/project/releases \
2538 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2539"#
2540 );
2541 }
2542
2543 #[test]
2544 fn test_set_url_quoted_options() {
2545 let wf: super::WatchFile = r#"version=4
2547opts="bare, filenamemangle=blah" \
2548 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2549"#
2550 .parse()
2551 .unwrap();
2552
2553 let mut entry = wf.entries().next().unwrap();
2554 assert_eq!(
2555 entry.url(),
2556 "https://github.com/syncthing/syncthing-gtk/tags"
2557 );
2558
2559 entry.set_url("https://example.org/new/path");
2560 assert_eq!(entry.url(), "https://example.org/new/path");
2561
2562 assert_eq!(
2564 entry.to_string(),
2565 r#"opts="bare, filenamemangle=blah" \
2566 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2567"#
2568 );
2569 }
2570
2571 #[test]
2572 fn test_set_opt_update_existing() {
2573 let wf: super::WatchFile = r#"version=4
2575opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2576"#
2577 .parse()
2578 .unwrap();
2579
2580 let mut entry = wf.entries().next().unwrap();
2581 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2582 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2583
2584 entry.set_opt("foo", "updated");
2585 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2586 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2587
2588 assert_eq!(
2590 entry.to_string(),
2591 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2592 );
2593 }
2594
2595 #[test]
2596 fn test_set_opt_add_new() {
2597 let wf: super::WatchFile = r#"version=4
2599opts=foo=blah 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"), None);
2607
2608 entry.set_opt("bar", "baz");
2609 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2610 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2611
2612 assert_eq!(
2614 entry.to_string(),
2615 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2616 );
2617 }
2618
2619 #[test]
2620 fn test_set_opt_create_options_list() {
2621 let wf: super::WatchFile = r#"version=4
2623https://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.option_list(), None);
2630
2631 entry.set_opt("compression", "xz");
2632 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2633
2634 assert_eq!(
2636 entry.to_string(),
2637 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2638 );
2639 }
2640
2641 #[test]
2642 fn test_del_opt_remove_single() {
2643 let wf: super::WatchFile = r#"version=4
2645opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2646"#
2647 .parse()
2648 .unwrap();
2649
2650 let mut entry = wf.entries().next().unwrap();
2651 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2652 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2653 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2654
2655 entry.del_opt_str("bar");
2656 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2657 assert_eq!(entry.get_option("bar"), None);
2658 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2659
2660 assert_eq!(
2662 entry.to_string(),
2663 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2664 );
2665 }
2666
2667 #[test]
2668 fn test_del_opt_remove_first() {
2669 let wf: super::WatchFile = r#"version=4
2671opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2672"#
2673 .parse()
2674 .unwrap();
2675
2676 let mut entry = wf.entries().next().unwrap();
2677 entry.del_opt_str("foo");
2678 assert_eq!(entry.get_option("foo"), None);
2679 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2680
2681 assert_eq!(
2683 entry.to_string(),
2684 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2685 );
2686 }
2687
2688 #[test]
2689 fn test_del_opt_remove_last() {
2690 let wf: super::WatchFile = r#"version=4
2692opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2693"#
2694 .parse()
2695 .unwrap();
2696
2697 let mut entry = wf.entries().next().unwrap();
2698 entry.del_opt_str("bar");
2699 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2700 assert_eq!(entry.get_option("bar"), None);
2701
2702 assert_eq!(
2704 entry.to_string(),
2705 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2706 );
2707 }
2708
2709 #[test]
2710 fn test_del_opt_remove_only_option() {
2711 let wf: super::WatchFile = r#"version=4
2713opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2714"#
2715 .parse()
2716 .unwrap();
2717
2718 let mut entry = wf.entries().next().unwrap();
2719 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2720
2721 entry.del_opt_str("foo");
2722 assert_eq!(entry.get_option("foo"), None);
2723 assert_eq!(entry.option_list(), None);
2724
2725 assert_eq!(
2727 entry.to_string(),
2728 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2729 );
2730 }
2731
2732 #[test]
2733 fn test_del_opt_nonexistent() {
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 let original = entry.to_string();
2743
2744 entry.del_opt_str("nonexistent");
2745 assert_eq!(entry.to_string(), original);
2746 }
2747
2748 #[test]
2749 fn test_set_opt_multiple_operations() {
2750 let wf: super::WatchFile = r#"version=4
2752https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2753"#
2754 .parse()
2755 .unwrap();
2756
2757 let mut entry = wf.entries().next().unwrap();
2758
2759 entry.set_opt("compression", "xz");
2760 entry.set_opt("repack", "");
2761 entry.set_opt("dversionmangle", "s/\\+ds//");
2762
2763 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2764 assert_eq!(
2765 entry.get_option("dversionmangle"),
2766 Some("s/\\+ds//".to_string())
2767 );
2768 }
2769
2770 #[test]
2771 fn test_set_matching_pattern() {
2772 let wf: super::WatchFile = r#"version=4
2774https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2775"#
2776 .parse()
2777 .unwrap();
2778
2779 let mut entry = wf.entries().next().unwrap();
2780 assert_eq!(
2781 entry.matching_pattern(),
2782 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2783 );
2784
2785 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2786 assert_eq!(
2787 entry.matching_pattern(),
2788 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2789 );
2790
2791 assert_eq!(entry.url(), "https://github.com/example/tags");
2793
2794 assert_eq!(
2796 entry.to_string(),
2797 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2798 );
2799 }
2800
2801 #[test]
2802 fn test_set_matching_pattern_with_all_fields() {
2803 let wf: super::WatchFile = r#"version=4
2805opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2806"#
2807 .parse()
2808 .unwrap();
2809
2810 let mut entry = wf.entries().next().unwrap();
2811 assert_eq!(
2812 entry.matching_pattern(),
2813 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2814 );
2815
2816 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2817 assert_eq!(
2818 entry.matching_pattern(),
2819 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2820 );
2821
2822 assert_eq!(entry.url(), "https://example.com/releases");
2824 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2825 assert_eq!(entry.script(), Some("uupdate".into()));
2826 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2827
2828 assert_eq!(
2830 entry.to_string(),
2831 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2832 );
2833 }
2834
2835 #[test]
2836 fn test_set_version_policy() {
2837 let wf: super::WatchFile = r#"version=4
2839https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2840"#
2841 .parse()
2842 .unwrap();
2843
2844 let mut entry = wf.entries().next().unwrap();
2845 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2846
2847 entry.set_version_policy("previous");
2848 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2849
2850 assert_eq!(entry.url(), "https://example.com/releases");
2852 assert_eq!(
2853 entry.matching_pattern(),
2854 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2855 );
2856 assert_eq!(entry.script(), Some("uupdate".into()));
2857
2858 assert_eq!(
2860 entry.to_string(),
2861 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2862 );
2863 }
2864
2865 #[test]
2866 fn test_set_version_policy_with_options() {
2867 let wf: super::WatchFile = r#"version=4
2869opts=repack,compression=xz \
2870 https://github.com/example/example-cat/tags \
2871 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2872"#
2873 .parse()
2874 .unwrap();
2875
2876 let mut entry = wf.entries().next().unwrap();
2877 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2878
2879 entry.set_version_policy("ignore");
2880 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2881
2882 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2884 assert_eq!(
2885 entry.matching_pattern(),
2886 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2887 );
2888 assert_eq!(entry.script(), Some("uupdate".into()));
2889 assert!(entry.repack());
2890
2891 assert_eq!(
2893 entry.to_string(),
2894 r#"opts=repack,compression=xz \
2895 https://github.com/example/example-cat/tags \
2896 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2897"#
2898 );
2899 }
2900
2901 #[test]
2902 fn test_set_script() {
2903 let wf: super::WatchFile = r#"version=4
2905https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2906"#
2907 .parse()
2908 .unwrap();
2909
2910 let mut entry = wf.entries().next().unwrap();
2911 assert_eq!(entry.script(), Some("uupdate".into()));
2912
2913 entry.set_script("uscan");
2914 assert_eq!(entry.script(), Some("uscan".into()));
2915
2916 assert_eq!(entry.url(), "https://example.com/releases");
2918 assert_eq!(
2919 entry.matching_pattern(),
2920 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2921 );
2922 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2923
2924 assert_eq!(
2926 entry.to_string(),
2927 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2928 );
2929 }
2930
2931 #[test]
2932 fn test_set_script_with_options() {
2933 let wf: super::WatchFile = r#"version=4
2935opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2936"#
2937 .parse()
2938 .unwrap();
2939
2940 let mut entry = wf.entries().next().unwrap();
2941 assert_eq!(entry.script(), Some("uupdate".into()));
2942
2943 entry.set_script("custom-script.sh");
2944 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2945
2946 assert_eq!(entry.url(), "https://example.com/releases");
2948 assert_eq!(
2949 entry.matching_pattern(),
2950 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2951 );
2952 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2953 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2954
2955 assert_eq!(
2957 entry.to_string(),
2958 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2959 );
2960 }
2961
2962 #[test]
2963 fn test_apply_dversionmangle() {
2964 let wf: super::WatchFile = r#"version=4
2966opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2967"#
2968 .parse()
2969 .unwrap();
2970 let entry = wf.entries().next().unwrap();
2971 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2972 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2973
2974 let wf: super::WatchFile = r#"version=4
2976opts=versionmangle=s/^v// https://example.com/ .*
2977"#
2978 .parse()
2979 .unwrap();
2980 let entry = wf.entries().next().unwrap();
2981 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
2982
2983 let wf: super::WatchFile = r#"version=4
2985opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
2986"#
2987 .parse()
2988 .unwrap();
2989 let entry = wf.entries().next().unwrap();
2990 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
2991
2992 let wf: super::WatchFile = r#"version=4
2994https://example.com/ .*
2995"#
2996 .parse()
2997 .unwrap();
2998 let entry = wf.entries().next().unwrap();
2999 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
3000 }
3001
3002 #[test]
3003 fn test_apply_oversionmangle() {
3004 let wf: super::WatchFile = r#"version=4
3006opts=oversionmangle=s/$/-1/ https://example.com/ .*
3007"#
3008 .parse()
3009 .unwrap();
3010 let entry = wf.entries().next().unwrap();
3011 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
3012 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
3013
3014 let wf: super::WatchFile = r#"version=4
3016opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
3017"#
3018 .parse()
3019 .unwrap();
3020 let entry = wf.entries().next().unwrap();
3021 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
3022
3023 let wf: super::WatchFile = r#"version=4
3025https://example.com/ .*
3026"#
3027 .parse()
3028 .unwrap();
3029 let entry = wf.entries().next().unwrap();
3030 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
3031 }
3032
3033 #[test]
3034 fn test_apply_dirversionmangle() {
3035 let wf: super::WatchFile = r#"version=4
3037opts=dirversionmangle=s/^v// https://example.com/ .*
3038"#
3039 .parse()
3040 .unwrap();
3041 let entry = wf.entries().next().unwrap();
3042 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3043 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
3044
3045 let wf: super::WatchFile = r#"version=4
3047opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
3048"#
3049 .parse()
3050 .unwrap();
3051 let entry = wf.entries().next().unwrap();
3052 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3053
3054 let wf: super::WatchFile = r#"version=4
3056https://example.com/ .*
3057"#
3058 .parse()
3059 .unwrap();
3060 let entry = wf.entries().next().unwrap();
3061 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
3062 }
3063
3064 #[test]
3065 fn test_apply_filenamemangle() {
3066 let wf: super::WatchFile = r#"version=4
3068opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
3069"#
3070 .parse()
3071 .unwrap();
3072 let entry = wf.entries().next().unwrap();
3073 assert_eq!(
3074 entry
3075 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
3076 .unwrap(),
3077 "mypackage-1.0.tar.gz"
3078 );
3079 assert_eq!(
3080 entry
3081 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
3082 .unwrap(),
3083 "mypackage-2.5.3.tar.gz"
3084 );
3085
3086 let wf: super::WatchFile = r#"version=4
3088opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
3089"#
3090 .parse()
3091 .unwrap();
3092 let entry = wf.entries().next().unwrap();
3093 assert_eq!(
3094 entry
3095 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
3096 .unwrap(),
3097 "file.tar.gz"
3098 );
3099
3100 let wf: super::WatchFile = r#"version=4
3102https://example.com/ .*
3103"#
3104 .parse()
3105 .unwrap();
3106 let entry = wf.entries().next().unwrap();
3107 assert_eq!(
3108 entry
3109 .apply_filenamemangle("https://example.com/file.tar.gz")
3110 .unwrap(),
3111 "https://example.com/file.tar.gz"
3112 );
3113 }
3114
3115 #[test]
3116 fn test_apply_pagemangle() {
3117 let wf: super::WatchFile = r#"version=4
3119opts=pagemangle=s/&/&/g https://example.com/ .*
3120"#
3121 .parse()
3122 .unwrap();
3123 let entry = wf.entries().next().unwrap();
3124 assert_eq!(
3125 entry.apply_pagemangle(b"foo & bar").unwrap(),
3126 b"foo & bar"
3127 );
3128 assert_eq!(
3129 entry
3130 .apply_pagemangle(b"& foo & bar &")
3131 .unwrap(),
3132 b"& foo & bar &"
3133 );
3134
3135 let wf: super::WatchFile = r#"version=4
3137opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3138"#
3139 .parse()
3140 .unwrap();
3141 let entry = wf.entries().next().unwrap();
3142 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3143
3144 let wf: super::WatchFile = r#"version=4
3146https://example.com/ .*
3147"#
3148 .parse()
3149 .unwrap();
3150 let entry = wf.entries().next().unwrap();
3151 assert_eq!(
3152 entry.apply_pagemangle(b"foo & bar").unwrap(),
3153 b"foo & bar"
3154 );
3155 }
3156
3157 #[test]
3158 fn test_apply_downloadurlmangle() {
3159 let wf: super::WatchFile = r#"version=4
3161opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3162"#
3163 .parse()
3164 .unwrap();
3165 let entry = wf.entries().next().unwrap();
3166 assert_eq!(
3167 entry
3168 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3169 .unwrap(),
3170 "https://example.com/download/file.tar.gz"
3171 );
3172
3173 let wf: super::WatchFile = r#"version=4
3175opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3176"#
3177 .parse()
3178 .unwrap();
3179 let entry = wf.entries().next().unwrap();
3180 assert_eq!(
3181 entry
3182 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3183 .unwrap(),
3184 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3185 );
3186
3187 let wf: super::WatchFile = r#"version=4
3189https://example.com/ .*
3190"#
3191 .parse()
3192 .unwrap();
3193 let entry = wf.entries().next().unwrap();
3194 assert_eq!(
3195 entry
3196 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3197 .unwrap(),
3198 "https://example.com/archive/file.tar.gz"
3199 );
3200 }
3201
3202 #[test]
3203 fn test_entry_builder_minimal() {
3204 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3206 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3207 .build();
3208
3209 assert_eq!(entry.url(), "https://github.com/example/tags");
3210 assert_eq!(
3211 entry.matching_pattern().as_deref(),
3212 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3213 );
3214 assert_eq!(entry.version(), Ok(None));
3215 assert_eq!(entry.script(), None);
3216 assert!(entry.opts().is_empty());
3217 }
3218
3219 #[test]
3220 fn test_entry_builder_url_only() {
3221 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3223
3224 assert_eq!(entry.url(), "https://example.com/releases");
3225 assert_eq!(entry.matching_pattern(), None);
3226 assert_eq!(entry.version(), Ok(None));
3227 assert_eq!(entry.script(), None);
3228 assert!(entry.opts().is_empty());
3229 }
3230
3231 #[test]
3232 fn test_entry_builder_with_all_fields() {
3233 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3235 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3236 .version_policy("debian")
3237 .script("uupdate")
3238 .opt("compression", "xz")
3239 .flag("repack")
3240 .build();
3241
3242 assert_eq!(entry.url(), "https://github.com/example/tags");
3243 assert_eq!(
3244 entry.matching_pattern().as_deref(),
3245 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3246 );
3247 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3248 assert_eq!(entry.script(), Some("uupdate".into()));
3249 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3250 assert!(entry.has_option("repack"));
3251 assert!(entry.repack());
3252 }
3253
3254 #[test]
3255 fn test_entry_builder_multiple_options() {
3256 let entry = super::EntryBuilder::new("https://example.com/tags")
3258 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3259 .opt("compression", "xz")
3260 .opt("dversionmangle", "s/\\+ds//")
3261 .opt("repacksuffix", "+ds")
3262 .build();
3263
3264 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3265 assert_eq!(
3266 entry.get_option("dversionmangle"),
3267 Some("s/\\+ds//".to_string())
3268 );
3269 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3270 }
3271
3272 #[test]
3273 fn test_entry_builder_via_entry() {
3274 let entry = super::Entry::builder("https://github.com/example/tags")
3276 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3277 .version_policy("debian")
3278 .build();
3279
3280 assert_eq!(entry.url(), "https://github.com/example/tags");
3281 assert_eq!(
3282 entry.matching_pattern().as_deref(),
3283 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3284 );
3285 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3286 }
3287
3288 #[test]
3289 fn test_watchfile_add_entry_to_empty() {
3290 let mut wf = super::WatchFile::new(Some(4));
3292
3293 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3294 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3295 .build();
3296
3297 wf.add_entry(entry);
3298
3299 assert_eq!(wf.version(), 4);
3300 assert_eq!(wf.entries().count(), 1);
3301
3302 let added_entry = wf.entries().next().unwrap();
3303 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3304 assert_eq!(
3305 added_entry.matching_pattern().as_deref(),
3306 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3307 );
3308 }
3309
3310 #[test]
3311 fn test_watchfile_add_multiple_entries() {
3312 let mut wf = super::WatchFile::new(Some(4));
3314
3315 wf.add_entry(
3316 super::EntryBuilder::new("https://github.com/example1/tags")
3317 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3318 .build(),
3319 );
3320
3321 wf.add_entry(
3322 super::EntryBuilder::new("https://github.com/example2/releases")
3323 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3324 .opt("compression", "xz")
3325 .build(),
3326 );
3327
3328 assert_eq!(wf.entries().count(), 2);
3329
3330 let entries: Vec<_> = wf.entries().collect();
3331 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3332 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3333 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3334 }
3335
3336 #[test]
3337 fn test_watchfile_add_entry_to_existing() {
3338 let mut wf: super::WatchFile = r#"version=4
3340https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3341"#
3342 .parse()
3343 .unwrap();
3344
3345 assert_eq!(wf.entries().count(), 1);
3346
3347 wf.add_entry(
3348 super::EntryBuilder::new("https://github.com/example/new")
3349 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3350 .opt("compression", "xz")
3351 .version_policy("debian")
3352 .build(),
3353 );
3354
3355 assert_eq!(wf.entries().count(), 2);
3356
3357 let entries: Vec<_> = wf.entries().collect();
3358 assert_eq!(entries[0].url(), "https://example.com/old");
3359 assert_eq!(entries[1].url(), "https://github.com/example/new");
3360 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3361 }
3362
3363 #[test]
3364 fn test_entry_builder_formatting() {
3365 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3367 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3368 .opt("compression", "xz")
3369 .flag("repack")
3370 .version_policy("debian")
3371 .script("uupdate")
3372 .build();
3373
3374 let entry_str = entry.to_string();
3375
3376 assert!(entry_str.starts_with("opts="));
3378 assert!(entry_str.contains("https://github.com/example/tags"));
3380 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3382 assert!(entry_str.contains("debian"));
3384 assert!(entry_str.contains("uupdate"));
3386 assert!(entry_str.ends_with('\n'));
3388 }
3389
3390 #[test]
3391 fn test_watchfile_add_entry_preserves_format() {
3392 let mut wf = super::WatchFile::new(Some(4));
3394
3395 wf.add_entry(
3396 super::EntryBuilder::new("https://github.com/example/tags")
3397 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3398 .build(),
3399 );
3400
3401 let wf_str = wf.to_string();
3402
3403 assert!(wf_str.starts_with("version=4\n"));
3405 assert!(wf_str.contains("https://github.com/example/tags"));
3407
3408 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3410 assert_eq!(reparsed.version(), 4);
3411 assert_eq!(reparsed.entries().count(), 1);
3412 }
3413
3414 #[test]
3415 fn test_line_col() {
3416 let text = r#"version=4
3417opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3418"#;
3419 let wf = text.parse::<super::WatchFile>().unwrap();
3420
3421 let version_node = wf.version_node().unwrap();
3423 assert_eq!(version_node.line(), 0);
3424 assert_eq!(version_node.column(), 0);
3425 assert_eq!(version_node.line_col(), (0, 0));
3426
3427 let entries: Vec<_> = wf.entries().collect();
3429 assert_eq!(entries.len(), 1);
3430
3431 assert_eq!(entries[0].line(), 1);
3433 assert_eq!(entries[0].column(), 0);
3434 assert_eq!(entries[0].line_col(), (1, 0));
3435
3436 let option_list = entries[0].option_list().unwrap();
3438 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3441 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3444 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3447 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3450 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3454 assert_eq!(options.len(), 1);
3455 assert_eq!(options[0].key(), Some("compression".to_string()));
3456 assert_eq!(options[0].value(), Some("xz".to_string()));
3457 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3461 assert_eq!(compression_opt.line(), 1);
3462 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3464 }
3465
3466 #[test]
3467 fn test_parse_str_relaxed() {
3468 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3469 r#"version=4
3470ERRORS IN THIS LINE
3471opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3472"#,
3473 );
3474 assert_eq!(wf.version(), 4);
3475 assert_eq!(wf.entries().count(), 2);
3476
3477 let entries = wf.entries().collect::<Vec<_>>();
3478
3479 let entry = &entries[0];
3480 assert_eq!(entry.url(), "ERRORS");
3481
3482 let entry = &entries[1];
3483 assert_eq!(entry.url(), "https://example.com/releases");
3484 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3485 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3486 }
3487}