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 loop {
241 self.skip_ws();
242 if self.current() == Some(NEWLINE) {
243 self.bump();
244 } else {
245 break;
246 }
247 }
248 if self.current().is_none() {
249 return false;
250 }
251 self.builder.start_node(ENTRY.into());
252 self.parse_options_list();
253 for i in 0..4 {
254 if self.current() == Some(NEWLINE) || self.current().is_none() {
255 break;
256 }
257 if self.current() == Some(CONTINUATION) {
258 self.bump();
259 self.skip_ws();
260 continue;
261 }
262 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
263 self.builder.start_node(ERROR.into());
264 self.errors.push(format!(
265 "expected value, got {:?} (i={})",
266 self.current(),
267 i
268 ));
269 if self.current().is_some() {
270 self.bump();
271 }
272 self.builder.finish_node();
273 } else {
274 match i {
276 0 => {
277 self.builder.start_node(URL.into());
279 self.bump();
280 self.builder.finish_node();
281 }
282 1 => {
283 self.builder.start_node(MATCHING_PATTERN.into());
285 self.bump();
286 self.builder.finish_node();
287 }
288 2 => {
289 self.builder.start_node(VERSION_POLICY.into());
291 self.bump();
292 self.builder.finish_node();
293 }
294 3 => {
295 self.builder.start_node(SCRIPT.into());
297 self.bump();
298 self.builder.finish_node();
299 }
300 _ => {
301 self.bump();
302 }
303 }
304 }
305 self.skip_ws();
306 }
307 if self.current() != Some(NEWLINE) && self.current().is_some() {
308 self.builder.start_node(ERROR.into());
309 self.errors
310 .push(format!("expected newline, not {:?}", self.current()));
311 if self.current().is_some() {
312 self.bump();
313 }
314 self.builder.finish_node();
315 } else if self.current().is_some() {
316 self.bump();
318 }
319 self.builder.finish_node();
320 true
321 }
322
323 fn parse_option(&mut self) -> bool {
324 if self.current().is_none() {
325 return false;
326 }
327 while self.current() == Some(CONTINUATION) {
328 self.bump();
329 }
330 if self.current() == Some(WHITESPACE) {
331 return false;
332 }
333 self.builder.start_node(OPTION.into());
334 if self.current() != Some(KEY) {
335 self.builder.start_node(ERROR.into());
336 self.errors.push("expected key".to_string());
337 self.bump();
338 self.builder.finish_node();
339 } else {
340 self.bump();
341 }
342 if self.current() == Some(EQUALS) {
343 self.bump();
344 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
345 self.builder.start_node(ERROR.into());
346 self.errors
347 .push(format!("expected value, got {:?}", self.current()));
348 self.bump();
349 self.builder.finish_node();
350 } else {
351 self.bump();
352 }
353 } else if self.current() == Some(COMMA) {
354 } else {
355 self.builder.start_node(ERROR.into());
356 self.errors.push("expected `=`".to_string());
357 if self.current().is_some() {
358 self.bump();
359 }
360 self.builder.finish_node();
361 }
362 self.builder.finish_node();
363 true
364 }
365
366 fn parse_options_list(&mut self) {
367 self.skip_ws();
368 if self.tokens.last() == Some(&(KEY, "opts".to_string()))
369 || self.tokens.last() == Some(&(KEY, "options".to_string()))
370 {
371 self.builder.start_node(OPTS_LIST.into());
372 self.bump();
373 self.skip_ws();
374 if self.current() != Some(EQUALS) {
375 self.builder.start_node(ERROR.into());
376 self.errors.push("expected `=`".to_string());
377 if self.current().is_some() {
378 self.bump();
379 }
380 self.builder.finish_node();
381 } else {
382 self.bump();
383 }
384 let quoted = if self.current() == Some(QUOTE) {
385 self.bump();
386 true
387 } else {
388 false
389 };
390 loop {
391 if quoted {
392 if self.current() == Some(QUOTE) {
393 self.bump();
394 break;
395 }
396 self.skip_ws();
397 }
398 if !self.parse_option() {
399 break;
400 }
401 if self.current() == Some(COMMA) {
402 self.builder.start_node(OPTION_SEPARATOR.into());
403 self.bump();
404 self.builder.finish_node();
405 } else if !quoted {
406 break;
407 }
408 }
409 self.builder.finish_node();
410 self.skip_ws();
411 }
412 }
413
414 fn parse(mut self) -> InternalParse {
415 self.builder.start_node(ROOT.into());
417 while self.current() == Some(WHITESPACE)
419 || self.current() == Some(CONTINUATION)
420 || self.current() == Some(COMMENT)
421 || self.current() == Some(NEWLINE)
422 {
423 self.bump();
424 }
425 if let Some(_v) = self.parse_version() {
426 }
428 loop {
430 if !self.parse_watch_entry() {
431 break;
432 }
433 }
434 self.skip_ws();
436 if self.current().is_some() {
439 self.builder.start_node(ERROR.into());
440 self.errors
441 .push("unexpected tokens after last entry".to_string());
442 while self.current().is_some() {
443 self.bump();
444 }
445 self.builder.finish_node();
446 }
447 self.builder.finish_node();
449
450 InternalParse {
452 green_node: self.builder.finish(),
453 errors: self.errors,
454 }
455 }
456 fn bump(&mut self) {
458 if let Some((kind, text)) = self.tokens.pop() {
459 self.builder.token(kind.into(), text.as_str());
460 }
461 }
462 fn current(&self) -> Option<SyntaxKind> {
464 self.tokens.last().map(|(kind, _)| *kind)
465 }
466 fn skip_ws(&mut self) {
467 while self.current() == Some(WHITESPACE)
468 || self.current() == Some(CONTINUATION)
469 || self.current() == Some(COMMENT)
470 {
471 self.bump()
472 }
473 }
474 }
475
476 let mut tokens = lex(text);
477 tokens.reverse();
478 Parser {
479 tokens,
480 builder: GreenNodeBuilder::new(),
481 errors: Vec::new(),
482 }
483 .parse()
484}
485
486type SyntaxNode = rowan::SyntaxNode<Lang>;
492#[allow(unused)]
493type SyntaxToken = rowan::SyntaxToken<Lang>;
494#[allow(unused)]
495type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
496
497impl InternalParse {
498 fn syntax(&self) -> SyntaxNode {
499 SyntaxNode::new_root_mut(self.green_node.clone())
500 }
501
502 fn root(&self) -> WatchFile {
503 WatchFile::cast(self.syntax()).expect("root node should be a WatchFile")
504 }
505}
506
507fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
510 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
511 let mut line = 0;
512 let mut last_newline_offset = rowan::TextSize::from(0);
513
514 for element in root.preorder_with_tokens() {
515 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
516 if token.text_range().start() >= offset {
517 break;
518 }
519
520 for (idx, _) in token.text().match_indices('\n') {
522 line += 1;
523 last_newline_offset =
524 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
525 }
526 }
527 }
528
529 let column: usize = (offset - last_newline_offset).into();
530 (line, column)
531}
532
533macro_rules! ast_node {
534 ($ast:ident, $kind:ident) => {
535 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
536 #[repr(transparent)]
537 pub struct $ast(SyntaxNode);
539 impl $ast {
540 #[allow(unused)]
541 fn cast(node: SyntaxNode) -> Option<Self> {
542 if node.kind() == $kind {
543 Some(Self(node))
544 } else {
545 None
546 }
547 }
548
549 pub fn line(&self) -> usize {
551 line_col_at_offset(&self.0, self.0.text_range().start()).0
552 }
553
554 pub fn column(&self) -> usize {
556 line_col_at_offset(&self.0, self.0.text_range().start()).1
557 }
558
559 pub fn line_col(&self) -> (usize, usize) {
562 line_col_at_offset(&self.0, self.0.text_range().start())
563 }
564 }
565
566 impl std::fmt::Display for $ast {
567 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
568 write!(f, "{}", self.0.text())
569 }
570 }
571 };
572}
573
574ast_node!(WatchFile, ROOT);
575ast_node!(Version, VERSION);
576ast_node!(Entry, ENTRY);
577ast_node!(_Option, OPTION);
578ast_node!(Url, URL);
579ast_node!(MatchingPattern, MATCHING_PATTERN);
580ast_node!(VersionPolicyNode, VERSION_POLICY);
581ast_node!(ScriptNode, SCRIPT);
582
583#[derive(Clone, PartialEq, Eq, Hash)]
585#[repr(transparent)]
586pub struct OptionList(SyntaxNode);
588
589impl OptionList {
590 #[allow(unused)]
591 fn cast(node: SyntaxNode) -> Option<Self> {
592 if node.kind() == OPTS_LIST {
593 Some(Self(node))
594 } else {
595 None
596 }
597 }
598
599 pub fn line(&self) -> usize {
601 line_col_at_offset(&self.0, self.0.text_range().start()).0
602 }
603
604 pub fn column(&self) -> usize {
606 line_col_at_offset(&self.0, self.0.text_range().start()).1
607 }
608
609 pub fn line_col(&self) -> (usize, usize) {
612 line_col_at_offset(&self.0, self.0.text_range().start())
613 }
614}
615
616impl std::fmt::Display for OptionList {
617 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
618 write!(f, "{}", self.0.text())
619 }
620}
621
622impl WatchFile {
623 #[cfg(feature = "deb822")]
625 pub(crate) fn syntax(&self) -> &SyntaxNode {
626 &self.0
627 }
628
629 pub fn new(version: Option<u32>) -> WatchFile {
631 let mut builder = GreenNodeBuilder::new();
632
633 builder.start_node(ROOT.into());
634 if let Some(version) = version {
635 builder.start_node(VERSION.into());
636 builder.token(KEY.into(), "version");
637 builder.token(EQUALS.into(), "=");
638 builder.token(VALUE.into(), version.to_string().as_str());
639 builder.token(NEWLINE.into(), "\n");
640 builder.finish_node();
641 }
642 builder.finish_node();
643 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
644 }
645
646 pub fn version_node(&self) -> Option<Version> {
648 self.0.children().find_map(Version::cast)
649 }
650
651 pub fn version(&self) -> u32 {
653 self.version_node()
654 .map(|it| it.version())
655 .unwrap_or(DEFAULT_VERSION)
656 }
657
658 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
660 self.0.children().filter_map(Entry::cast)
661 }
662
663 pub fn set_version(&mut self, new_version: u32) {
665 let mut builder = GreenNodeBuilder::new();
667 builder.start_node(VERSION.into());
668 builder.token(KEY.into(), "version");
669 builder.token(EQUALS.into(), "=");
670 builder.token(VALUE.into(), new_version.to_string().as_str());
671 builder.token(NEWLINE.into(), "\n");
672 builder.finish_node();
673 let new_version_green = builder.finish();
674
675 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
677
678 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
680
681 if let Some(pos) = version_pos {
682 self.0
684 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
685 } else {
686 self.0.splice_children(0..0, vec![new_version_node.into()]);
688 }
689 }
690
691 #[cfg(feature = "discover")]
711 pub async fn uscan(
712 &self,
713 package: impl Fn() -> String + Send + Sync,
714 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
715 let mut all_releases = Vec::new();
716
717 for entry in self.entries() {
718 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
719 let releases = parsed_entry.discover(|| package()).await?;
720 all_releases.push(releases);
721 }
722
723 Ok(all_releases)
724 }
725
726 #[cfg(all(feature = "discover", feature = "blocking"))]
744 pub fn uscan_blocking(
745 &self,
746 package: impl Fn() -> String,
747 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
748 let mut all_releases = Vec::new();
749
750 for entry in self.entries() {
751 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
752 let releases = parsed_entry.discover_blocking(|| package())?;
753 all_releases.push(releases);
754 }
755
756 Ok(all_releases)
757 }
758
759 pub fn add_entry(&mut self, entry: Entry) -> Entry {
786 let insert_pos = self.0.children_with_tokens().count();
788
789 let entry_green = entry.0.green().into_owned();
791 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
792
793 self.0
795 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
796
797 Entry::cast(
799 self.0
800 .children()
801 .nth(insert_pos)
802 .expect("Entry was just inserted"),
803 )
804 .expect("Inserted node should be an Entry")
805 }
806
807 pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
809 let mut buf_reader = std::io::BufReader::new(reader);
810 let mut content = String::new();
811 buf_reader
812 .read_to_string(&mut content)
813 .map_err(|e| ParseError(vec![e.to_string()]))?;
814 content.parse()
815 }
816
817 pub fn from_reader_relaxed<R: std::io::Read>(mut r: R) -> Result<Self, std::io::Error> {
819 let mut content = String::new();
820 r.read_to_string(&mut content)?;
821 let parsed = parse(&content);
822 Ok(parsed.root())
823 }
824
825 pub fn from_str_relaxed(s: &str) -> Self {
827 let parsed = parse(s);
828 parsed.root()
829 }
830}
831
832impl FromStr for WatchFile {
833 type Err = ParseError;
834
835 fn from_str(s: &str) -> Result<Self, Self::Err> {
836 let parsed = parse(s);
837 if parsed.errors.is_empty() {
838 Ok(parsed.root())
839 } else {
840 Err(ParseError(parsed.errors))
841 }
842 }
843}
844
845pub fn parse_watch_file(text: &str) -> Parse<WatchFile> {
848 let parsed = parse(text);
849 Parse::new(parsed.green_node, parsed.errors)
850}
851
852impl Version {
853 pub fn version(&self) -> u32 {
855 self.0
856 .children_with_tokens()
857 .find_map(|it| match it {
858 SyntaxElement::Token(token) => {
859 if token.kind() == VALUE {
860 token.text().parse().ok()
861 } else {
862 None
863 }
864 }
865 _ => None,
866 })
867 .unwrap_or(DEFAULT_VERSION)
868 }
869}
870
871#[derive(Debug, Clone, Default)]
895pub struct EntryBuilder {
896 url: Option<String>,
897 matching_pattern: Option<String>,
898 version_policy: Option<String>,
899 script: Option<String>,
900 opts: std::collections::HashMap<String, String>,
901}
902
903impl EntryBuilder {
904 pub fn new(url: impl Into<String>) -> Self {
906 EntryBuilder {
907 url: Some(url.into()),
908 matching_pattern: None,
909 version_policy: None,
910 script: None,
911 opts: std::collections::HashMap::new(),
912 }
913 }
914
915 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
917 self.matching_pattern = Some(pattern.into());
918 self
919 }
920
921 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
923 self.version_policy = Some(policy.into());
924 self
925 }
926
927 pub fn script(mut self, script: impl Into<String>) -> Self {
929 self.script = Some(script.into());
930 self
931 }
932
933 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
935 self.opts.insert(key.into(), value.into());
936 self
937 }
938
939 pub fn flag(mut self, key: impl Into<String>) -> Self {
943 self.opts.insert(key.into(), String::new());
944 self
945 }
946
947 pub fn build(self) -> Entry {
953 let url = self.url.expect("URL is required for entry");
954
955 let mut builder = GreenNodeBuilder::new();
956
957 builder.start_node(ENTRY.into());
958
959 if !self.opts.is_empty() {
961 builder.start_node(OPTS_LIST.into());
962 builder.token(KEY.into(), "opts");
963 builder.token(EQUALS.into(), "=");
964
965 let mut first = true;
966 for (key, value) in self.opts.iter() {
967 if !first {
968 builder.token(COMMA.into(), ",");
969 }
970 first = false;
971
972 builder.start_node(OPTION.into());
973 builder.token(KEY.into(), key);
974 if !value.is_empty() {
975 builder.token(EQUALS.into(), "=");
976 builder.token(VALUE.into(), value);
977 }
978 builder.finish_node();
979 }
980
981 builder.finish_node();
982 builder.token(WHITESPACE.into(), " ");
983 }
984
985 builder.start_node(URL.into());
987 builder.token(VALUE.into(), &url);
988 builder.finish_node();
989
990 if let Some(pattern) = self.matching_pattern {
992 builder.token(WHITESPACE.into(), " ");
993 builder.start_node(MATCHING_PATTERN.into());
994 builder.token(VALUE.into(), &pattern);
995 builder.finish_node();
996 }
997
998 if let Some(policy) = self.version_policy {
1000 builder.token(WHITESPACE.into(), " ");
1001 builder.start_node(VERSION_POLICY.into());
1002 builder.token(VALUE.into(), &policy);
1003 builder.finish_node();
1004 }
1005
1006 if let Some(script_val) = self.script {
1008 builder.token(WHITESPACE.into(), " ");
1009 builder.start_node(SCRIPT.into());
1010 builder.token(VALUE.into(), &script_val);
1011 builder.finish_node();
1012 }
1013
1014 builder.token(NEWLINE.into(), "\n");
1015 builder.finish_node();
1016
1017 Entry(SyntaxNode::new_root_mut(builder.finish()))
1018 }
1019}
1020
1021impl Entry {
1022 #[cfg(feature = "deb822")]
1024 pub(crate) fn syntax(&self) -> &SyntaxNode {
1025 &self.0
1026 }
1027
1028 pub fn builder(url: impl Into<String>) -> EntryBuilder {
1042 EntryBuilder::new(url)
1043 }
1044
1045 pub fn option_list(&self) -> Option<OptionList> {
1047 self.0.children().find_map(OptionList::cast)
1048 }
1049
1050 pub fn get_option(&self, key: &str) -> Option<String> {
1052 self.option_list().and_then(|ol| ol.get_option(key))
1053 }
1054
1055 pub fn has_option(&self, key: &str) -> bool {
1057 self.option_list().is_some_and(|ol| ol.has_option(key))
1058 }
1059
1060 pub fn component(&self) -> Option<String> {
1062 self.get_option("component")
1063 }
1064
1065 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
1067 self.try_ctype().map_err(|_| ())
1068 }
1069
1070 pub fn try_ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
1072 self.get_option("ctype").map(|s| s.parse()).transpose()
1073 }
1074
1075 pub fn compression(&self) -> Result<Option<Compression>, ()> {
1077 self.try_compression().map_err(|_| ())
1078 }
1079
1080 pub fn try_compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
1082 self.get_option("compression")
1083 .map(|s| s.parse())
1084 .transpose()
1085 }
1086
1087 pub fn repack(&self) -> bool {
1089 self.has_option("repack")
1090 }
1091
1092 pub fn repacksuffix(&self) -> Option<String> {
1094 self.get_option("repacksuffix")
1095 }
1096
1097 pub fn mode(&self) -> Result<Mode, ()> {
1099 self.try_mode().map_err(|_| ())
1100 }
1101
1102 pub fn try_mode(&self) -> Result<Mode, crate::types::ParseError> {
1104 Ok(self
1105 .get_option("mode")
1106 .map(|s| s.parse())
1107 .transpose()?
1108 .unwrap_or_default())
1109 }
1110
1111 pub fn pretty(&self) -> Result<Pretty, ()> {
1113 self.try_pretty().map_err(|_| ())
1114 }
1115
1116 pub fn try_pretty(&self) -> Result<Pretty, crate::types::ParseError> {
1118 Ok(self
1119 .get_option("pretty")
1120 .map(|s| s.parse())
1121 .transpose()?
1122 .unwrap_or_default())
1123 }
1124
1125 pub fn date(&self) -> String {
1128 self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
1129 }
1130
1131 pub fn gitexport(&self) -> Result<GitExport, ()> {
1133 self.try_gitexport().map_err(|_| ())
1134 }
1135
1136 pub fn try_gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
1138 Ok(self
1139 .get_option("gitexport")
1140 .map(|s| s.parse())
1141 .transpose()?
1142 .unwrap_or_default())
1143 }
1144
1145 pub fn gitmode(&self) -> Result<GitMode, ()> {
1147 self.try_gitmode().map_err(|_| ())
1148 }
1149
1150 pub fn try_gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
1152 Ok(self
1153 .get_option("gitmode")
1154 .map(|s| s.parse())
1155 .transpose()?
1156 .unwrap_or_default())
1157 }
1158
1159 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
1161 self.try_pgpmode().map_err(|_| ())
1162 }
1163
1164 pub fn try_pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
1166 Ok(self
1167 .get_option("pgpmode")
1168 .map(|s| s.parse())
1169 .transpose()?
1170 .unwrap_or_default())
1171 }
1172
1173 pub fn searchmode(&self) -> Result<SearchMode, ()> {
1175 self.try_searchmode().map_err(|_| ())
1176 }
1177
1178 pub fn try_searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
1180 Ok(self
1181 .get_option("searchmode")
1182 .map(|s| s.parse())
1183 .transpose()?
1184 .unwrap_or_default())
1185 }
1186
1187 pub fn decompress(&self) -> bool {
1189 self.has_option("decompress")
1190 }
1191
1192 pub fn bare(&self) -> bool {
1195 self.has_option("bare")
1196 }
1197
1198 pub fn user_agent(&self) -> Option<String> {
1200 self.get_option("user-agent")
1201 }
1202
1203 pub fn passive(&self) -> Option<bool> {
1205 if self.has_option("passive") || self.has_option("pasv") {
1206 Some(true)
1207 } else if self.has_option("active") || self.has_option("nopasv") {
1208 Some(false)
1209 } else {
1210 None
1211 }
1212 }
1213
1214 pub fn unzipoptions(&self) -> Option<String> {
1217 self.get_option("unzipopt")
1218 }
1219
1220 pub fn dversionmangle(&self) -> Option<String> {
1222 self.get_option("dversionmangle")
1223 .or_else(|| self.get_option("versionmangle"))
1224 }
1225
1226 pub fn dirversionmangle(&self) -> Option<String> {
1230 self.get_option("dirversionmangle")
1231 }
1232
1233 pub fn pagemangle(&self) -> Option<String> {
1235 self.get_option("pagemangle")
1236 }
1237
1238 pub fn uversionmangle(&self) -> Option<String> {
1242 self.get_option("uversionmangle")
1243 .or_else(|| self.get_option("versionmangle"))
1244 }
1245
1246 pub fn versionmangle(&self) -> Option<String> {
1248 self.get_option("versionmangle")
1249 }
1250
1251 pub fn hrefdecode(&self) -> bool {
1256 self.get_option("hrefdecode").is_some()
1257 }
1258
1259 pub fn downloadurlmangle(&self) -> Option<String> {
1262 self.get_option("downloadurlmangle")
1263 }
1264
1265 pub fn filenamemangle(&self) -> Option<String> {
1273 self.get_option("filenamemangle")
1274 }
1275
1276 pub fn pgpsigurlmangle(&self) -> Option<String> {
1278 self.get_option("pgpsigurlmangle")
1279 }
1280
1281 pub fn oversionmangle(&self) -> Option<String> {
1284 self.get_option("oversionmangle")
1285 }
1286
1287 pub fn apply_uversionmangle(
1300 &self,
1301 version: &str,
1302 ) -> Result<String, crate::mangle::MangleError> {
1303 if let Some(vm) = self.uversionmangle() {
1304 crate::mangle::apply_mangle(&vm, version)
1305 } else {
1306 Ok(version.to_string())
1307 }
1308 }
1309
1310 pub fn apply_dversionmangle(
1323 &self,
1324 version: &str,
1325 ) -> Result<String, crate::mangle::MangleError> {
1326 if let Some(vm) = self.dversionmangle() {
1327 crate::mangle::apply_mangle(&vm, version)
1328 } else {
1329 Ok(version.to_string())
1330 }
1331 }
1332
1333 pub fn apply_oversionmangle(
1346 &self,
1347 version: &str,
1348 ) -> Result<String, crate::mangle::MangleError> {
1349 if let Some(vm) = self.oversionmangle() {
1350 crate::mangle::apply_mangle(&vm, version)
1351 } else {
1352 Ok(version.to_string())
1353 }
1354 }
1355
1356 pub fn apply_dirversionmangle(
1369 &self,
1370 version: &str,
1371 ) -> Result<String, crate::mangle::MangleError> {
1372 if let Some(vm) = self.dirversionmangle() {
1373 crate::mangle::apply_mangle(&vm, version)
1374 } else {
1375 Ok(version.to_string())
1376 }
1377 }
1378
1379 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1395 if let Some(vm) = self.filenamemangle() {
1396 crate::mangle::apply_mangle(&vm, url)
1397 } else {
1398 Ok(url.to_string())
1399 }
1400 }
1401
1402 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1418 if let Some(vm) = self.pagemangle() {
1419 let page_str = String::from_utf8_lossy(page);
1420 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1421 Ok(mangled.into_bytes())
1422 } else {
1423 Ok(page.to_vec())
1424 }
1425 }
1426
1427 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1443 if let Some(vm) = self.downloadurlmangle() {
1444 crate::mangle::apply_mangle(&vm, url)
1445 } else {
1446 Ok(url.to_string())
1447 }
1448 }
1449
1450 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1452 let mut options = std::collections::HashMap::new();
1453
1454 if let Some(ol) = self.option_list() {
1455 for opt in ol.options() {
1456 let key = opt.key();
1457 let value = opt.value();
1458 if let (Some(key), Some(value)) = (key, value) {
1459 options.insert(key.to_string(), value.to_string());
1460 }
1461 }
1462 }
1463
1464 options
1465 }
1466
1467 fn items(&self) -> impl Iterator<Item = String> + '_ {
1468 self.0.children_with_tokens().filter_map(|it| match it {
1469 SyntaxElement::Token(token) => {
1470 if token.kind() == VALUE || token.kind() == KEY {
1471 Some(token.text().to_string())
1472 } else {
1473 None
1474 }
1475 }
1476 SyntaxElement::Node(node) => {
1477 match node.kind() {
1479 URL => Url::cast(node).map(|n| n.url()),
1480 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1481 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1482 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1483 _ => None,
1484 }
1485 }
1486 })
1487 }
1488
1489 pub fn url_node(&self) -> Option<Url> {
1491 self.0.children().find_map(Url::cast)
1492 }
1493
1494 pub fn url(&self) -> String {
1496 self.url_node().map(|it| it.url()).unwrap_or_else(|| {
1497 self.items().next().unwrap()
1499 })
1500 }
1501
1502 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1504 self.0.children().find_map(MatchingPattern::cast)
1505 }
1506
1507 pub fn matching_pattern(&self) -> Option<String> {
1509 self.matching_pattern_node()
1510 .map(|it| it.pattern())
1511 .or_else(|| {
1512 self.items().nth(1)
1514 })
1515 }
1516
1517 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1519 self.0.children().find_map(VersionPolicyNode::cast)
1520 }
1521
1522 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1524 self.version_node()
1525 .map(|it| it.policy().parse())
1526 .transpose()
1527 .map_err(|e: crate::types::ParseError| e.to_string())
1528 .or_else(|_e| {
1529 self.items()
1531 .nth(2)
1532 .map(|it| it.parse())
1533 .transpose()
1534 .map_err(|e: crate::types::ParseError| e.to_string())
1535 })
1536 }
1537
1538 pub fn script_node(&self) -> Option<ScriptNode> {
1540 self.0.children().find_map(ScriptNode::cast)
1541 }
1542
1543 pub fn script(&self) -> Option<String> {
1545 self.script_node().map(|it| it.script()).or_else(|| {
1546 self.items().nth(3)
1548 })
1549 }
1550
1551 pub fn format_url(
1553 &self,
1554 package: impl FnOnce() -> String,
1555 component: impl FnOnce() -> String,
1556 ) -> url::Url {
1557 crate::subst::subst(self.url().as_str(), package, component)
1558 .parse()
1559 .unwrap()
1560 }
1561
1562 pub fn set_url(&mut self, new_url: &str) {
1564 let mut builder = GreenNodeBuilder::new();
1566 builder.start_node(URL.into());
1567 builder.token(VALUE.into(), new_url);
1568 builder.finish_node();
1569 let new_url_green = builder.finish();
1570
1571 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1573
1574 let url_pos = self
1576 .0
1577 .children_with_tokens()
1578 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1579
1580 if let Some(pos) = url_pos {
1581 self.0
1583 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1584 }
1585 }
1586
1587 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1593 let mut builder = GreenNodeBuilder::new();
1595 builder.start_node(MATCHING_PATTERN.into());
1596 builder.token(VALUE.into(), new_pattern);
1597 builder.finish_node();
1598 let new_pattern_green = builder.finish();
1599
1600 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1602
1603 let pattern_pos = self.0.children_with_tokens().position(
1605 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1606 );
1607
1608 if let Some(pos) = pattern_pos {
1609 self.0
1611 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1612 }
1613 }
1615
1616 pub fn set_version_policy(&mut self, new_policy: &str) {
1622 let mut builder = GreenNodeBuilder::new();
1624 builder.start_node(VERSION_POLICY.into());
1625 builder.token(VALUE.into(), new_policy);
1627 builder.finish_node();
1628 let new_policy_green = builder.finish();
1629
1630 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1632
1633 let policy_pos = self.0.children_with_tokens().position(
1635 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1636 );
1637
1638 if let Some(pos) = policy_pos {
1639 self.0
1641 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1642 }
1643 }
1645
1646 pub fn set_script(&mut self, new_script: &str) {
1652 let mut builder = GreenNodeBuilder::new();
1654 builder.start_node(SCRIPT.into());
1655 builder.token(VALUE.into(), new_script);
1657 builder.finish_node();
1658 let new_script_green = builder.finish();
1659
1660 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1662
1663 let script_pos = self
1665 .0
1666 .children_with_tokens()
1667 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1668
1669 if let Some(pos) = script_pos {
1670 self.0
1672 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1673 }
1674 }
1676
1677 pub fn set_option(&mut self, option: crate::types::WatchOption) {
1683 let key = watch_option_to_key(&option);
1684 let value = watch_option_to_value(&option);
1685 self.set_opt(key, &value);
1686 }
1687
1688 pub fn set_opt(&mut self, key: &str, value: &str) {
1694 let opts_pos = self.0.children_with_tokens().position(
1696 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1697 );
1698
1699 if let Some(_opts_idx) = opts_pos {
1700 if let Some(mut ol) = self.option_list() {
1701 if let Some(mut opt) = ol.find_option(key) {
1703 opt.set_value(value);
1705 } else {
1707 ol.add_option(key, value);
1709 }
1711 }
1712 } else {
1713 let mut builder = GreenNodeBuilder::new();
1715 builder.start_node(OPTS_LIST.into());
1716 builder.token(KEY.into(), "opts");
1717 builder.token(EQUALS.into(), "=");
1718 builder.start_node(OPTION.into());
1719 builder.token(KEY.into(), key);
1720 builder.token(EQUALS.into(), "=");
1721 builder.token(VALUE.into(), value);
1722 builder.finish_node();
1723 builder.finish_node();
1724 let new_opts_green = builder.finish();
1725 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1726
1727 let url_pos = self
1729 .0
1730 .children_with_tokens()
1731 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1732
1733 if let Some(url_idx) = url_pos {
1734 let mut combined_builder = GreenNodeBuilder::new();
1737 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1739 combined_builder.finish_node();
1740 let temp_green = combined_builder.finish();
1741 let temp_root = SyntaxNode::new_root_mut(temp_green);
1742 let space_element = temp_root.children_with_tokens().next().unwrap();
1743
1744 self.0
1745 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1746 } else {
1747 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1748 }
1749 }
1750 }
1751
1752 pub fn del_opt(&mut self, option: crate::types::WatchOption) {
1759 let key = watch_option_to_key(&option);
1760 if let Some(mut ol) = self.option_list() {
1761 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1762
1763 if option_count == 1 && ol.has_option(key) {
1764 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1766
1767 if let Some(opts_idx) = opts_pos {
1768 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1770
1771 while self.0.children_with_tokens().next().is_some_and(|e| {
1773 matches!(
1774 e,
1775 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1776 )
1777 }) {
1778 self.0.splice_children(0..1, vec![]);
1779 }
1780 }
1781 } else {
1782 ol.remove_option(key);
1784 }
1785 }
1786 }
1787
1788 pub fn del_opt_str(&mut self, key: &str) {
1795 if let Some(mut ol) = self.option_list() {
1796 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1797
1798 if option_count == 1 && ol.has_option(key) {
1799 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1801
1802 if let Some(opts_idx) = opts_pos {
1803 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1805
1806 while self.0.children_with_tokens().next().is_some_and(|e| {
1808 matches!(
1809 e,
1810 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1811 )
1812 }) {
1813 self.0.splice_children(0..1, vec![]);
1814 }
1815 }
1816 } else {
1817 ol.remove_option(key);
1819 }
1820 }
1821 }
1822}
1823
1824impl std::fmt::Debug for OptionList {
1825 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1826 f.debug_struct("OptionList")
1827 .field("text", &self.0.text().to_string())
1828 .finish()
1829 }
1830}
1831
1832impl OptionList {
1833 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1835 self.0.children().filter_map(_Option::cast)
1836 }
1837
1838 pub fn find_option(&self, key: &str) -> Option<_Option> {
1840 self.options().find(|opt| opt.key().as_deref() == Some(key))
1841 }
1842
1843 pub fn has_option(&self, key: &str) -> bool {
1845 self.options().any(|it| it.key().as_deref() == Some(key))
1846 }
1847
1848 #[cfg(feature = "deb822")]
1851 pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
1852 self.options().filter_map(|opt| {
1853 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
1854 Some((key, value))
1855 } else {
1856 None
1857 }
1858 })
1859 }
1860
1861 pub fn get_option(&self, key: &str) -> Option<String> {
1863 for child in self.options() {
1864 if child.key().as_deref() == Some(key) {
1865 return child.value();
1866 }
1867 }
1868 None
1869 }
1870
1871 fn add_option(&mut self, key: &str, value: &str) {
1873 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1874
1875 let mut builder = GreenNodeBuilder::new();
1877 builder.start_node(ROOT.into()); if option_count > 0 {
1880 builder.start_node(OPTION_SEPARATOR.into());
1881 builder.token(COMMA.into(), ",");
1882 builder.finish_node();
1883 }
1884
1885 builder.start_node(OPTION.into());
1886 builder.token(KEY.into(), key);
1887 builder.token(EQUALS.into(), "=");
1888 builder.token(VALUE.into(), value);
1889 builder.finish_node();
1890
1891 builder.finish_node(); let combined_green = builder.finish();
1893
1894 let temp_root = SyntaxNode::new_root_mut(combined_green);
1896 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1897
1898 let insert_pos = self.0.children_with_tokens().count();
1899 self.0.splice_children(insert_pos..insert_pos, new_children);
1900 }
1901
1902 fn remove_option(&mut self, key: &str) -> bool {
1904 if let Some(mut opt) = self.find_option(key) {
1905 opt.remove();
1906 true
1907 } else {
1908 false
1909 }
1910 }
1911}
1912
1913impl _Option {
1914 pub fn key(&self) -> Option<String> {
1916 self.0.children_with_tokens().find_map(|it| match it {
1917 SyntaxElement::Token(token) => {
1918 if token.kind() == KEY {
1919 Some(token.text().to_string())
1920 } else {
1921 None
1922 }
1923 }
1924 _ => None,
1925 })
1926 }
1927
1928 pub fn value(&self) -> Option<String> {
1930 self.0
1931 .children_with_tokens()
1932 .filter_map(|it| match it {
1933 SyntaxElement::Token(token) => {
1934 if token.kind() == VALUE || token.kind() == KEY {
1935 Some(token.text().to_string())
1936 } else {
1937 None
1938 }
1939 }
1940 _ => None,
1941 })
1942 .nth(1)
1943 }
1944
1945 pub fn set_value(&mut self, new_value: &str) {
1947 let key = self.key().expect("Option must have a key");
1948
1949 let mut builder = GreenNodeBuilder::new();
1951 builder.start_node(OPTION.into());
1952 builder.token(KEY.into(), &key);
1953 builder.token(EQUALS.into(), "=");
1954 builder.token(VALUE.into(), new_value);
1955 builder.finish_node();
1956 let new_option_green = builder.finish();
1957 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1958
1959 if let Some(parent) = self.0.parent() {
1961 let idx = self.0.index();
1962 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1963 }
1964 }
1965
1966 pub fn remove(&mut self) {
1968 let next_sep = self
1970 .0
1971 .next_sibling()
1972 .filter(|n| n.kind() == OPTION_SEPARATOR);
1973 let prev_sep = self
1974 .0
1975 .prev_sibling()
1976 .filter(|n| n.kind() == OPTION_SEPARATOR);
1977
1978 if let Some(sep) = next_sep {
1980 sep.detach();
1981 } else if let Some(sep) = prev_sep {
1982 sep.detach();
1983 }
1984
1985 self.0.detach();
1987 }
1988}
1989
1990impl Url {
1991 pub fn url(&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 MatchingPattern {
2010 pub fn pattern(&self) -> String {
2012 self.0
2013 .children_with_tokens()
2014 .find_map(|it| match it {
2015 SyntaxElement::Token(token) => {
2016 if token.kind() == VALUE {
2017 Some(token.text().to_string())
2018 } else {
2019 None
2020 }
2021 }
2022 _ => None,
2023 })
2024 .unwrap()
2025 }
2026}
2027
2028impl VersionPolicyNode {
2029 pub fn policy(&self) -> String {
2031 self.0
2032 .children_with_tokens()
2033 .find_map(|it| match it {
2034 SyntaxElement::Token(token) => {
2035 if token.kind() == VALUE || token.kind() == KEY {
2037 Some(token.text().to_string())
2038 } else {
2039 None
2040 }
2041 }
2042 _ => None,
2043 })
2044 .unwrap()
2045 }
2046}
2047
2048impl ScriptNode {
2049 pub fn script(&self) -> String {
2051 self.0
2052 .children_with_tokens()
2053 .find_map(|it| match it {
2054 SyntaxElement::Token(token) => {
2055 if token.kind() == VALUE || token.kind() == KEY {
2057 Some(token.text().to_string())
2058 } else {
2059 None
2060 }
2061 }
2062 _ => None,
2063 })
2064 .unwrap()
2065 }
2066}
2067
2068#[cfg(test)]
2069mod tests {
2070 use super::*;
2071
2072 #[test]
2073 fn test_entry_node_structure() {
2074 let wf: super::WatchFile = r#"version=4
2076opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2077"#
2078 .parse()
2079 .unwrap();
2080
2081 let entry = wf.entries().next().unwrap();
2082
2083 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2085 assert_eq!(entry.url(), "https://example.com/releases");
2086
2087 assert_eq!(
2089 entry
2090 .0
2091 .children()
2092 .find(|n| n.kind() == MATCHING_PATTERN)
2093 .is_some(),
2094 true
2095 );
2096 assert_eq!(
2097 entry.matching_pattern(),
2098 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2099 );
2100
2101 assert_eq!(
2103 entry
2104 .0
2105 .children()
2106 .find(|n| n.kind() == VERSION_POLICY)
2107 .is_some(),
2108 true
2109 );
2110 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2111
2112 assert_eq!(
2114 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2115 true
2116 );
2117 assert_eq!(entry.script(), Some("uupdate".into()));
2118 }
2119
2120 #[test]
2121 fn test_entry_node_structure_partial() {
2122 let wf: super::WatchFile = r#"version=4
2124https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2125"#
2126 .parse()
2127 .unwrap();
2128
2129 let entry = wf.entries().next().unwrap();
2130
2131 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2133 assert_eq!(
2134 entry
2135 .0
2136 .children()
2137 .find(|n| n.kind() == MATCHING_PATTERN)
2138 .is_some(),
2139 true
2140 );
2141
2142 assert_eq!(
2144 entry
2145 .0
2146 .children()
2147 .find(|n| n.kind() == VERSION_POLICY)
2148 .is_some(),
2149 false
2150 );
2151 assert_eq!(
2152 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2153 false
2154 );
2155
2156 assert_eq!(entry.url(), "https://github.com/example/tags");
2158 assert_eq!(
2159 entry.matching_pattern(),
2160 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2161 );
2162 assert_eq!(entry.version(), Ok(None));
2163 assert_eq!(entry.script(), None);
2164 }
2165
2166 #[test]
2167 fn test_parse_v1() {
2168 const WATCHV1: &str = r#"version=4
2169opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2170 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2171"#;
2172 let parsed = parse(WATCHV1);
2173 let node = parsed.syntax();
2175 assert_eq!(
2176 format!("{:#?}", node),
2177 r#"ROOT@0..161
2178 VERSION@0..10
2179 KEY@0..7 "version"
2180 EQUALS@7..8 "="
2181 VALUE@8..9 "4"
2182 NEWLINE@9..10 "\n"
2183 ENTRY@10..161
2184 OPTS_LIST@10..86
2185 KEY@10..14 "opts"
2186 EQUALS@14..15 "="
2187 OPTION@15..19
2188 KEY@15..19 "bare"
2189 OPTION_SEPARATOR@19..20
2190 COMMA@19..20 ","
2191 OPTION@20..86
2192 KEY@20..34 "filenamemangle"
2193 EQUALS@34..35 "="
2194 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2195 WHITESPACE@86..87 " "
2196 CONTINUATION@87..89 "\\\n"
2197 WHITESPACE@89..91 " "
2198 URL@91..138
2199 VALUE@91..138 "https://github.com/sy ..."
2200 WHITESPACE@138..139 " "
2201 MATCHING_PATTERN@139..160
2202 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2203 NEWLINE@160..161 "\n"
2204"#
2205 );
2206
2207 let root = parsed.root();
2208 assert_eq!(root.version(), 4);
2209 let entries = root.entries().collect::<Vec<_>>();
2210 assert_eq!(entries.len(), 1);
2211 let entry = &entries[0];
2212 assert_eq!(
2213 entry.url(),
2214 "https://github.com/syncthing/syncthing-gtk/tags"
2215 );
2216 assert_eq!(
2217 entry.matching_pattern(),
2218 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2219 );
2220 assert_eq!(entry.version(), Ok(None));
2221 assert_eq!(entry.script(), None);
2222
2223 assert_eq!(node.text(), WATCHV1);
2224 }
2225
2226 #[test]
2227 fn test_parse_v2() {
2228 let parsed = parse(
2229 r#"version=4
2230https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2231# comment
2232"#,
2233 );
2234 assert_eq!(parsed.errors, Vec::<String>::new());
2235 let node = parsed.syntax();
2236 assert_eq!(
2237 format!("{:#?}", node),
2238 r###"ROOT@0..90
2239 VERSION@0..10
2240 KEY@0..7 "version"
2241 EQUALS@7..8 "="
2242 VALUE@8..9 "4"
2243 NEWLINE@9..10 "\n"
2244 ENTRY@10..80
2245 URL@10..57
2246 VALUE@10..57 "https://github.com/sy ..."
2247 WHITESPACE@57..58 " "
2248 MATCHING_PATTERN@58..79
2249 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2250 NEWLINE@79..80 "\n"
2251 COMMENT@80..89 "# comment"
2252 NEWLINE@89..90 "\n"
2253"###
2254 );
2255
2256 let root = parsed.root();
2257 assert_eq!(root.version(), 4);
2258 let entries = root.entries().collect::<Vec<_>>();
2259 assert_eq!(entries.len(), 1);
2260 let entry = &entries[0];
2261 assert_eq!(
2262 entry.url(),
2263 "https://github.com/syncthing/syncthing-gtk/tags"
2264 );
2265 assert_eq!(
2266 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2267 "https://github.com/syncthing/syncthing-gtk/tags"
2268 .parse()
2269 .unwrap()
2270 );
2271 }
2272
2273 #[test]
2274 fn test_parse_v3() {
2275 let parsed = parse(
2276 r#"version=4
2277https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2278# comment
2279"#,
2280 );
2281 assert_eq!(parsed.errors, Vec::<String>::new());
2282 let root = parsed.root();
2283 assert_eq!(root.version(), 4);
2284 let entries = root.entries().collect::<Vec<_>>();
2285 assert_eq!(entries.len(), 1);
2286 let entry = &entries[0];
2287 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2288 assert_eq!(
2289 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2290 "https://github.com/syncthing/syncthing-gtk/tags"
2291 .parse()
2292 .unwrap()
2293 );
2294 }
2295
2296 #[test]
2297 fn test_thread_safe_parsing() {
2298 let text = r#"version=4
2299https://github.com/example/example/tags example-(.*)\.tar\.gz
2300"#;
2301
2302 let parsed = parse_watch_file(text);
2303 assert!(parsed.is_ok());
2304 assert_eq!(parsed.errors().len(), 0);
2305
2306 let watchfile = parsed.tree();
2308 assert_eq!(watchfile.version(), 4);
2309 let entries: Vec<_> = watchfile.entries().collect();
2310 assert_eq!(entries.len(), 1);
2311 }
2312
2313 #[test]
2314 fn test_parse_clone_and_eq() {
2315 let text = r#"version=4
2316https://github.com/example/example/tags example-(.*)\.tar\.gz
2317"#;
2318
2319 let parsed1 = parse_watch_file(text);
2320 let parsed2 = parsed1.clone();
2321
2322 assert_eq!(parsed1, parsed2);
2324
2325 let watchfile1 = parsed1.tree();
2327 let watchfile2 = watchfile1.clone();
2328 assert_eq!(watchfile1, watchfile2);
2329 }
2330
2331 #[test]
2332 fn test_parse_v4() {
2333 let cl: super::WatchFile = r#"version=4
2334opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2335 https://github.com/example/example-cat/tags \
2336 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2337"#
2338 .parse()
2339 .unwrap();
2340 assert_eq!(cl.version(), 4);
2341 let entries = cl.entries().collect::<Vec<_>>();
2342 assert_eq!(entries.len(), 1);
2343 let entry = &entries[0];
2344 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2345 assert_eq!(
2346 entry.matching_pattern(),
2347 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2348 );
2349 assert!(entry.repack());
2350 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2351 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2352 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2353 assert_eq!(entry.script(), Some("uupdate".into()));
2354 assert_eq!(
2355 entry.format_url(|| "example-cat".to_string(), || String::new()),
2356 "https://github.com/example/example-cat/tags"
2357 .parse()
2358 .unwrap()
2359 );
2360 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2361 }
2362
2363 #[test]
2364 fn test_git_mode() {
2365 let text = r#"version=3
2366opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2367https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2368refs/tags/(.*) debian
2369"#;
2370 let parsed = parse(text);
2371 assert_eq!(parsed.errors, Vec::<String>::new());
2372 let cl = parsed.root();
2373 assert_eq!(cl.version(), 3);
2374 let entries = cl.entries().collect::<Vec<_>>();
2375 assert_eq!(entries.len(), 1);
2376 let entry = &entries[0];
2377 assert_eq!(
2378 entry.url(),
2379 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2380 );
2381 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2382 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2383 assert_eq!(entry.script(), None);
2384 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2385 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2386 assert_eq!(entry.mode(), Ok(Mode::Git));
2387 }
2388
2389 #[test]
2390 fn test_parse_quoted() {
2391 const WATCHV1: &str = r#"version=4
2392opts="bare, filenamemangle=blah" \
2393 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2394"#;
2395 let parsed = parse(WATCHV1);
2396 let node = parsed.syntax();
2398
2399 let root = parsed.root();
2400 assert_eq!(root.version(), 4);
2401 let entries = root.entries().collect::<Vec<_>>();
2402 assert_eq!(entries.len(), 1);
2403 let entry = &entries[0];
2404
2405 assert_eq!(
2406 entry.url(),
2407 "https://github.com/syncthing/syncthing-gtk/tags"
2408 );
2409 assert_eq!(
2410 entry.matching_pattern(),
2411 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2412 );
2413 assert_eq!(entry.version(), Ok(None));
2414 assert_eq!(entry.script(), None);
2415
2416 assert_eq!(node.text(), WATCHV1);
2417 }
2418
2419 #[test]
2420 fn test_set_url() {
2421 let wf: super::WatchFile = r#"version=4
2423https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2424"#
2425 .parse()
2426 .unwrap();
2427
2428 let mut entry = wf.entries().next().unwrap();
2429 assert_eq!(
2430 entry.url(),
2431 "https://github.com/syncthing/syncthing-gtk/tags"
2432 );
2433
2434 entry.set_url("https://newurl.example.org/path");
2435 assert_eq!(entry.url(), "https://newurl.example.org/path");
2436 assert_eq!(
2437 entry.matching_pattern(),
2438 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2439 );
2440
2441 assert_eq!(
2443 entry.to_string(),
2444 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2445 );
2446 }
2447
2448 #[test]
2449 fn test_set_url_with_options() {
2450 let wf: super::WatchFile = r#"version=4
2452opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2453"#
2454 .parse()
2455 .unwrap();
2456
2457 let mut entry = wf.entries().next().unwrap();
2458 assert_eq!(entry.url(), "https://foo.com/bar");
2459 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2460
2461 entry.set_url("https://example.com/baz");
2462 assert_eq!(entry.url(), "https://example.com/baz");
2463
2464 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2466 assert_eq!(
2467 entry.matching_pattern(),
2468 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2469 );
2470
2471 assert_eq!(
2473 entry.to_string(),
2474 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2475 );
2476 }
2477
2478 #[test]
2479 fn test_set_url_complex() {
2480 let wf: super::WatchFile = r#"version=4
2482opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2483 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2484"#
2485 .parse()
2486 .unwrap();
2487
2488 let mut entry = wf.entries().next().unwrap();
2489 assert_eq!(
2490 entry.url(),
2491 "https://github.com/syncthing/syncthing-gtk/tags"
2492 );
2493
2494 entry.set_url("https://gitlab.com/newproject/tags");
2495 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2496
2497 assert!(entry.bare());
2499 assert_eq!(
2500 entry.filenamemangle(),
2501 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2502 );
2503 assert_eq!(
2504 entry.matching_pattern(),
2505 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2506 );
2507
2508 assert_eq!(
2510 entry.to_string(),
2511 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2512 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2513"#
2514 );
2515 }
2516
2517 #[test]
2518 fn test_set_url_with_all_fields() {
2519 let wf: super::WatchFile = r#"version=4
2521opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2522 https://github.com/example/example-cat/tags \
2523 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2524"#
2525 .parse()
2526 .unwrap();
2527
2528 let mut entry = wf.entries().next().unwrap();
2529 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2530 assert_eq!(
2531 entry.matching_pattern(),
2532 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2533 );
2534 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2535 assert_eq!(entry.script(), Some("uupdate".into()));
2536
2537 entry.set_url("https://gitlab.example.org/project/releases");
2538 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2539
2540 assert!(entry.repack());
2542 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2543 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2544 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2545 assert_eq!(
2546 entry.matching_pattern(),
2547 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2548 );
2549 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2550 assert_eq!(entry.script(), Some("uupdate".into()));
2551
2552 assert_eq!(
2554 entry.to_string(),
2555 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2556 https://gitlab.example.org/project/releases \
2557 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2558"#
2559 );
2560 }
2561
2562 #[test]
2563 fn test_set_url_quoted_options() {
2564 let wf: super::WatchFile = r#"version=4
2566opts="bare, filenamemangle=blah" \
2567 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2568"#
2569 .parse()
2570 .unwrap();
2571
2572 let mut entry = wf.entries().next().unwrap();
2573 assert_eq!(
2574 entry.url(),
2575 "https://github.com/syncthing/syncthing-gtk/tags"
2576 );
2577
2578 entry.set_url("https://example.org/new/path");
2579 assert_eq!(entry.url(), "https://example.org/new/path");
2580
2581 assert_eq!(
2583 entry.to_string(),
2584 r#"opts="bare, filenamemangle=blah" \
2585 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2586"#
2587 );
2588 }
2589
2590 #[test]
2591 fn test_set_opt_update_existing() {
2592 let wf: super::WatchFile = r#"version=4
2594opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2595"#
2596 .parse()
2597 .unwrap();
2598
2599 let mut entry = wf.entries().next().unwrap();
2600 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2601 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2602
2603 entry.set_opt("foo", "updated");
2604 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2605 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2606
2607 assert_eq!(
2609 entry.to_string(),
2610 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2611 );
2612 }
2613
2614 #[test]
2615 fn test_set_opt_add_new() {
2616 let wf: super::WatchFile = r#"version=4
2618opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2619"#
2620 .parse()
2621 .unwrap();
2622
2623 let mut entry = wf.entries().next().unwrap();
2624 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2625 assert_eq!(entry.get_option("bar"), None);
2626
2627 entry.set_opt("bar", "baz");
2628 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2629 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2630
2631 assert_eq!(
2633 entry.to_string(),
2634 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2635 );
2636 }
2637
2638 #[test]
2639 fn test_set_opt_create_options_list() {
2640 let wf: super::WatchFile = r#"version=4
2642https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2643"#
2644 .parse()
2645 .unwrap();
2646
2647 let mut entry = wf.entries().next().unwrap();
2648 assert_eq!(entry.option_list(), None);
2649
2650 entry.set_opt("compression", "xz");
2651 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2652
2653 assert_eq!(
2655 entry.to_string(),
2656 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2657 );
2658 }
2659
2660 #[test]
2661 fn test_del_opt_remove_single() {
2662 let wf: super::WatchFile = r#"version=4
2664opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2665"#
2666 .parse()
2667 .unwrap();
2668
2669 let mut entry = wf.entries().next().unwrap();
2670 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2671 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2672 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2673
2674 entry.del_opt_str("bar");
2675 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2676 assert_eq!(entry.get_option("bar"), None);
2677 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2678
2679 assert_eq!(
2681 entry.to_string(),
2682 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2683 );
2684 }
2685
2686 #[test]
2687 fn test_del_opt_remove_first() {
2688 let wf: super::WatchFile = r#"version=4
2690opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2691"#
2692 .parse()
2693 .unwrap();
2694
2695 let mut entry = wf.entries().next().unwrap();
2696 entry.del_opt_str("foo");
2697 assert_eq!(entry.get_option("foo"), None);
2698 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2699
2700 assert_eq!(
2702 entry.to_string(),
2703 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2704 );
2705 }
2706
2707 #[test]
2708 fn test_del_opt_remove_last() {
2709 let wf: super::WatchFile = r#"version=4
2711opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2712"#
2713 .parse()
2714 .unwrap();
2715
2716 let mut entry = wf.entries().next().unwrap();
2717 entry.del_opt_str("bar");
2718 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2719 assert_eq!(entry.get_option("bar"), None);
2720
2721 assert_eq!(
2723 entry.to_string(),
2724 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2725 );
2726 }
2727
2728 #[test]
2729 fn test_del_opt_remove_only_option() {
2730 let wf: super::WatchFile = r#"version=4
2732opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2733"#
2734 .parse()
2735 .unwrap();
2736
2737 let mut entry = wf.entries().next().unwrap();
2738 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2739
2740 entry.del_opt_str("foo");
2741 assert_eq!(entry.get_option("foo"), None);
2742 assert_eq!(entry.option_list(), None);
2743
2744 assert_eq!(
2746 entry.to_string(),
2747 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2748 );
2749 }
2750
2751 #[test]
2752 fn test_del_opt_nonexistent() {
2753 let wf: super::WatchFile = r#"version=4
2755opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2756"#
2757 .parse()
2758 .unwrap();
2759
2760 let mut entry = wf.entries().next().unwrap();
2761 let original = entry.to_string();
2762
2763 entry.del_opt_str("nonexistent");
2764 assert_eq!(entry.to_string(), original);
2765 }
2766
2767 #[test]
2768 fn test_set_opt_multiple_operations() {
2769 let wf: super::WatchFile = r#"version=4
2771https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2772"#
2773 .parse()
2774 .unwrap();
2775
2776 let mut entry = wf.entries().next().unwrap();
2777
2778 entry.set_opt("compression", "xz");
2779 entry.set_opt("repack", "");
2780 entry.set_opt("dversionmangle", "s/\\+ds//");
2781
2782 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2783 assert_eq!(
2784 entry.get_option("dversionmangle"),
2785 Some("s/\\+ds//".to_string())
2786 );
2787 }
2788
2789 #[test]
2790 fn test_set_matching_pattern() {
2791 let wf: super::WatchFile = r#"version=4
2793https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2794"#
2795 .parse()
2796 .unwrap();
2797
2798 let mut entry = wf.entries().next().unwrap();
2799 assert_eq!(
2800 entry.matching_pattern(),
2801 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2802 );
2803
2804 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2805 assert_eq!(
2806 entry.matching_pattern(),
2807 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2808 );
2809
2810 assert_eq!(entry.url(), "https://github.com/example/tags");
2812
2813 assert_eq!(
2815 entry.to_string(),
2816 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2817 );
2818 }
2819
2820 #[test]
2821 fn test_set_matching_pattern_with_all_fields() {
2822 let wf: super::WatchFile = r#"version=4
2824opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2825"#
2826 .parse()
2827 .unwrap();
2828
2829 let mut entry = wf.entries().next().unwrap();
2830 assert_eq!(
2831 entry.matching_pattern(),
2832 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2833 );
2834
2835 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2836 assert_eq!(
2837 entry.matching_pattern(),
2838 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2839 );
2840
2841 assert_eq!(entry.url(), "https://example.com/releases");
2843 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2844 assert_eq!(entry.script(), Some("uupdate".into()));
2845 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2846
2847 assert_eq!(
2849 entry.to_string(),
2850 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2851 );
2852 }
2853
2854 #[test]
2855 fn test_set_version_policy() {
2856 let wf: super::WatchFile = r#"version=4
2858https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2859"#
2860 .parse()
2861 .unwrap();
2862
2863 let mut entry = wf.entries().next().unwrap();
2864 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2865
2866 entry.set_version_policy("previous");
2867 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2868
2869 assert_eq!(entry.url(), "https://example.com/releases");
2871 assert_eq!(
2872 entry.matching_pattern(),
2873 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2874 );
2875 assert_eq!(entry.script(), Some("uupdate".into()));
2876
2877 assert_eq!(
2879 entry.to_string(),
2880 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2881 );
2882 }
2883
2884 #[test]
2885 fn test_set_version_policy_with_options() {
2886 let wf: super::WatchFile = r#"version=4
2888opts=repack,compression=xz \
2889 https://github.com/example/example-cat/tags \
2890 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2891"#
2892 .parse()
2893 .unwrap();
2894
2895 let mut entry = wf.entries().next().unwrap();
2896 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2897
2898 entry.set_version_policy("ignore");
2899 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2900
2901 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2903 assert_eq!(
2904 entry.matching_pattern(),
2905 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2906 );
2907 assert_eq!(entry.script(), Some("uupdate".into()));
2908 assert!(entry.repack());
2909
2910 assert_eq!(
2912 entry.to_string(),
2913 r#"opts=repack,compression=xz \
2914 https://github.com/example/example-cat/tags \
2915 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2916"#
2917 );
2918 }
2919
2920 #[test]
2921 fn test_set_script() {
2922 let wf: super::WatchFile = r#"version=4
2924https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2925"#
2926 .parse()
2927 .unwrap();
2928
2929 let mut entry = wf.entries().next().unwrap();
2930 assert_eq!(entry.script(), Some("uupdate".into()));
2931
2932 entry.set_script("uscan");
2933 assert_eq!(entry.script(), Some("uscan".into()));
2934
2935 assert_eq!(entry.url(), "https://example.com/releases");
2937 assert_eq!(
2938 entry.matching_pattern(),
2939 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2940 );
2941 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2942
2943 assert_eq!(
2945 entry.to_string(),
2946 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2947 );
2948 }
2949
2950 #[test]
2951 fn test_set_script_with_options() {
2952 let wf: super::WatchFile = r#"version=4
2954opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2955"#
2956 .parse()
2957 .unwrap();
2958
2959 let mut entry = wf.entries().next().unwrap();
2960 assert_eq!(entry.script(), Some("uupdate".into()));
2961
2962 entry.set_script("custom-script.sh");
2963 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2964
2965 assert_eq!(entry.url(), "https://example.com/releases");
2967 assert_eq!(
2968 entry.matching_pattern(),
2969 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2970 );
2971 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2972 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2973
2974 assert_eq!(
2976 entry.to_string(),
2977 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2978 );
2979 }
2980
2981 #[test]
2982 fn test_apply_dversionmangle() {
2983 let wf: super::WatchFile = r#"version=4
2985opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2986"#
2987 .parse()
2988 .unwrap();
2989 let entry = wf.entries().next().unwrap();
2990 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2991 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2992
2993 let wf: super::WatchFile = r#"version=4
2995opts=versionmangle=s/^v// https://example.com/ .*
2996"#
2997 .parse()
2998 .unwrap();
2999 let entry = wf.entries().next().unwrap();
3000 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
3001
3002 let wf: super::WatchFile = r#"version=4
3004opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
3005"#
3006 .parse()
3007 .unwrap();
3008 let entry = wf.entries().next().unwrap();
3009 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
3010
3011 let wf: super::WatchFile = r#"version=4
3013https://example.com/ .*
3014"#
3015 .parse()
3016 .unwrap();
3017 let entry = wf.entries().next().unwrap();
3018 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
3019 }
3020
3021 #[test]
3022 fn test_apply_oversionmangle() {
3023 let wf: super::WatchFile = r#"version=4
3025opts=oversionmangle=s/$/-1/ https://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-1");
3031 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
3032
3033 let wf: super::WatchFile = r#"version=4
3035opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
3036"#
3037 .parse()
3038 .unwrap();
3039 let entry = wf.entries().next().unwrap();
3040 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
3041
3042 let wf: super::WatchFile = r#"version=4
3044https://example.com/ .*
3045"#
3046 .parse()
3047 .unwrap();
3048 let entry = wf.entries().next().unwrap();
3049 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
3050 }
3051
3052 #[test]
3053 fn test_apply_dirversionmangle() {
3054 let wf: super::WatchFile = r#"version=4
3056opts=dirversionmangle=s/^v// https://example.com/ .*
3057"#
3058 .parse()
3059 .unwrap();
3060 let entry = wf.entries().next().unwrap();
3061 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3062 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
3063
3064 let wf: super::WatchFile = r#"version=4
3066opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
3067"#
3068 .parse()
3069 .unwrap();
3070 let entry = wf.entries().next().unwrap();
3071 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3072
3073 let wf: super::WatchFile = r#"version=4
3075https://example.com/ .*
3076"#
3077 .parse()
3078 .unwrap();
3079 let entry = wf.entries().next().unwrap();
3080 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
3081 }
3082
3083 #[test]
3084 fn test_apply_filenamemangle() {
3085 let wf: super::WatchFile = r#"version=4
3087opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
3088"#
3089 .parse()
3090 .unwrap();
3091 let entry = wf.entries().next().unwrap();
3092 assert_eq!(
3093 entry
3094 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
3095 .unwrap(),
3096 "mypackage-1.0.tar.gz"
3097 );
3098 assert_eq!(
3099 entry
3100 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
3101 .unwrap(),
3102 "mypackage-2.5.3.tar.gz"
3103 );
3104
3105 let wf: super::WatchFile = r#"version=4
3107opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
3108"#
3109 .parse()
3110 .unwrap();
3111 let entry = wf.entries().next().unwrap();
3112 assert_eq!(
3113 entry
3114 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
3115 .unwrap(),
3116 "file.tar.gz"
3117 );
3118
3119 let wf: super::WatchFile = r#"version=4
3121https://example.com/ .*
3122"#
3123 .parse()
3124 .unwrap();
3125 let entry = wf.entries().next().unwrap();
3126 assert_eq!(
3127 entry
3128 .apply_filenamemangle("https://example.com/file.tar.gz")
3129 .unwrap(),
3130 "https://example.com/file.tar.gz"
3131 );
3132 }
3133
3134 #[test]
3135 fn test_apply_pagemangle() {
3136 let wf: super::WatchFile = r#"version=4
3138opts=pagemangle=s/&/&/g https://example.com/ .*
3139"#
3140 .parse()
3141 .unwrap();
3142 let entry = wf.entries().next().unwrap();
3143 assert_eq!(
3144 entry.apply_pagemangle(b"foo & bar").unwrap(),
3145 b"foo & bar"
3146 );
3147 assert_eq!(
3148 entry
3149 .apply_pagemangle(b"& foo & bar &")
3150 .unwrap(),
3151 b"& foo & bar &"
3152 );
3153
3154 let wf: super::WatchFile = r#"version=4
3156opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3157"#
3158 .parse()
3159 .unwrap();
3160 let entry = wf.entries().next().unwrap();
3161 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3162
3163 let wf: super::WatchFile = r#"version=4
3165https://example.com/ .*
3166"#
3167 .parse()
3168 .unwrap();
3169 let entry = wf.entries().next().unwrap();
3170 assert_eq!(
3171 entry.apply_pagemangle(b"foo & bar").unwrap(),
3172 b"foo & bar"
3173 );
3174 }
3175
3176 #[test]
3177 fn test_apply_downloadurlmangle() {
3178 let wf: super::WatchFile = r#"version=4
3180opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3181"#
3182 .parse()
3183 .unwrap();
3184 let entry = wf.entries().next().unwrap();
3185 assert_eq!(
3186 entry
3187 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3188 .unwrap(),
3189 "https://example.com/download/file.tar.gz"
3190 );
3191
3192 let wf: super::WatchFile = r#"version=4
3194opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3195"#
3196 .parse()
3197 .unwrap();
3198 let entry = wf.entries().next().unwrap();
3199 assert_eq!(
3200 entry
3201 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3202 .unwrap(),
3203 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3204 );
3205
3206 let wf: super::WatchFile = r#"version=4
3208https://example.com/ .*
3209"#
3210 .parse()
3211 .unwrap();
3212 let entry = wf.entries().next().unwrap();
3213 assert_eq!(
3214 entry
3215 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3216 .unwrap(),
3217 "https://example.com/archive/file.tar.gz"
3218 );
3219 }
3220
3221 #[test]
3222 fn test_entry_builder_minimal() {
3223 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3225 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3226 .build();
3227
3228 assert_eq!(entry.url(), "https://github.com/example/tags");
3229 assert_eq!(
3230 entry.matching_pattern().as_deref(),
3231 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3232 );
3233 assert_eq!(entry.version(), Ok(None));
3234 assert_eq!(entry.script(), None);
3235 assert!(entry.opts().is_empty());
3236 }
3237
3238 #[test]
3239 fn test_entry_builder_url_only() {
3240 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3242
3243 assert_eq!(entry.url(), "https://example.com/releases");
3244 assert_eq!(entry.matching_pattern(), None);
3245 assert_eq!(entry.version(), Ok(None));
3246 assert_eq!(entry.script(), None);
3247 assert!(entry.opts().is_empty());
3248 }
3249
3250 #[test]
3251 fn test_entry_builder_with_all_fields() {
3252 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3254 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3255 .version_policy("debian")
3256 .script("uupdate")
3257 .opt("compression", "xz")
3258 .flag("repack")
3259 .build();
3260
3261 assert_eq!(entry.url(), "https://github.com/example/tags");
3262 assert_eq!(
3263 entry.matching_pattern().as_deref(),
3264 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3265 );
3266 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3267 assert_eq!(entry.script(), Some("uupdate".into()));
3268 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3269 assert!(entry.has_option("repack"));
3270 assert!(entry.repack());
3271 }
3272
3273 #[test]
3274 fn test_entry_builder_multiple_options() {
3275 let entry = super::EntryBuilder::new("https://example.com/tags")
3277 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3278 .opt("compression", "xz")
3279 .opt("dversionmangle", "s/\\+ds//")
3280 .opt("repacksuffix", "+ds")
3281 .build();
3282
3283 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3284 assert_eq!(
3285 entry.get_option("dversionmangle"),
3286 Some("s/\\+ds//".to_string())
3287 );
3288 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3289 }
3290
3291 #[test]
3292 fn test_entry_builder_via_entry() {
3293 let entry = super::Entry::builder("https://github.com/example/tags")
3295 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3296 .version_policy("debian")
3297 .build();
3298
3299 assert_eq!(entry.url(), "https://github.com/example/tags");
3300 assert_eq!(
3301 entry.matching_pattern().as_deref(),
3302 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3303 );
3304 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3305 }
3306
3307 #[test]
3308 fn test_watchfile_add_entry_to_empty() {
3309 let mut wf = super::WatchFile::new(Some(4));
3311
3312 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3313 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3314 .build();
3315
3316 wf.add_entry(entry);
3317
3318 assert_eq!(wf.version(), 4);
3319 assert_eq!(wf.entries().count(), 1);
3320
3321 let added_entry = wf.entries().next().unwrap();
3322 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3323 assert_eq!(
3324 added_entry.matching_pattern().as_deref(),
3325 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3326 );
3327 }
3328
3329 #[test]
3330 fn test_watchfile_add_multiple_entries() {
3331 let mut wf = super::WatchFile::new(Some(4));
3333
3334 wf.add_entry(
3335 super::EntryBuilder::new("https://github.com/example1/tags")
3336 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3337 .build(),
3338 );
3339
3340 wf.add_entry(
3341 super::EntryBuilder::new("https://github.com/example2/releases")
3342 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3343 .opt("compression", "xz")
3344 .build(),
3345 );
3346
3347 assert_eq!(wf.entries().count(), 2);
3348
3349 let entries: Vec<_> = wf.entries().collect();
3350 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3351 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3352 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3353 }
3354
3355 #[test]
3356 fn test_watchfile_add_entry_to_existing() {
3357 let mut wf: super::WatchFile = r#"version=4
3359https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3360"#
3361 .parse()
3362 .unwrap();
3363
3364 assert_eq!(wf.entries().count(), 1);
3365
3366 wf.add_entry(
3367 super::EntryBuilder::new("https://github.com/example/new")
3368 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3369 .opt("compression", "xz")
3370 .version_policy("debian")
3371 .build(),
3372 );
3373
3374 assert_eq!(wf.entries().count(), 2);
3375
3376 let entries: Vec<_> = wf.entries().collect();
3377 assert_eq!(entries[0].url(), "https://example.com/old");
3378 assert_eq!(entries[1].url(), "https://github.com/example/new");
3379 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3380 }
3381
3382 #[test]
3383 fn test_entry_builder_formatting() {
3384 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3386 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3387 .opt("compression", "xz")
3388 .flag("repack")
3389 .version_policy("debian")
3390 .script("uupdate")
3391 .build();
3392
3393 let entry_str = entry.to_string();
3394
3395 assert!(entry_str.starts_with("opts="));
3397 assert!(entry_str.contains("https://github.com/example/tags"));
3399 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3401 assert!(entry_str.contains("debian"));
3403 assert!(entry_str.contains("uupdate"));
3405 assert!(entry_str.ends_with('\n'));
3407 }
3408
3409 #[test]
3410 fn test_watchfile_add_entry_preserves_format() {
3411 let mut wf = super::WatchFile::new(Some(4));
3413
3414 wf.add_entry(
3415 super::EntryBuilder::new("https://github.com/example/tags")
3416 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3417 .build(),
3418 );
3419
3420 let wf_str = wf.to_string();
3421
3422 assert!(wf_str.starts_with("version=4\n"));
3424 assert!(wf_str.contains("https://github.com/example/tags"));
3426
3427 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3429 assert_eq!(reparsed.version(), 4);
3430 assert_eq!(reparsed.entries().count(), 1);
3431 }
3432
3433 #[test]
3434 fn test_line_col() {
3435 let text = r#"version=4
3436opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3437"#;
3438 let wf = text.parse::<super::WatchFile>().unwrap();
3439
3440 let version_node = wf.version_node().unwrap();
3442 assert_eq!(version_node.line(), 0);
3443 assert_eq!(version_node.column(), 0);
3444 assert_eq!(version_node.line_col(), (0, 0));
3445
3446 let entries: Vec<_> = wf.entries().collect();
3448 assert_eq!(entries.len(), 1);
3449
3450 assert_eq!(entries[0].line(), 1);
3452 assert_eq!(entries[0].column(), 0);
3453 assert_eq!(entries[0].line_col(), (1, 0));
3454
3455 let option_list = entries[0].option_list().unwrap();
3457 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3460 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3463 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3466 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3469 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3473 assert_eq!(options.len(), 1);
3474 assert_eq!(options[0].key(), Some("compression".to_string()));
3475 assert_eq!(options[0].value(), Some("xz".to_string()));
3476 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3480 assert_eq!(compression_opt.line(), 1);
3481 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3483 }
3484
3485 #[test]
3486 fn test_parse_str_relaxed() {
3487 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3488 r#"version=4
3489ERRORS IN THIS LINE
3490opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3491"#,
3492 );
3493 assert_eq!(wf.version(), 4);
3494 assert_eq!(wf.entries().count(), 2);
3495
3496 let entries = wf.entries().collect::<Vec<_>>();
3497
3498 let entry = &entries[0];
3499 assert_eq!(entry.url(), "ERRORS");
3500
3501 let entry = &entries[1];
3502 assert_eq!(entry.url(), "https://example.com/releases");
3503 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3504 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3505 }
3506
3507 #[test]
3508 fn test_parse_entry_with_comment_before() {
3509 let input = concat!(
3513 "version=4\n",
3514 "# try also https://pypi.debian.net/tomoscan/watch\n",
3515 "opts=uversionmangle=s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/ \\\n",
3516 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))\n"
3517 );
3518 let wf: super::WatchFile = input.parse().unwrap();
3519 assert_eq!(wf.to_string(), input);
3521 assert_eq!(wf.entries().count(), 1);
3522 let entry = wf.entries().next().unwrap();
3523 assert_eq!(
3524 entry.url(),
3525 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))"
3526 );
3527 assert_eq!(
3528 entry.get_option("uversionmangle"),
3529 Some("s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/".to_string())
3530 );
3531 }
3532
3533 #[test]
3534 fn test_parse_multiple_comments_before_entry() {
3535 let input = concat!(
3538 "version=4\n",
3539 "# first comment\n",
3540 "# second comment\n",
3541 "# third comment\n",
3542 "https://example.com/foo foo-(.*).tar.gz\n",
3543 );
3544 let wf: super::WatchFile = input.parse().unwrap();
3545 assert_eq!(wf.to_string(), input);
3546 assert_eq!(wf.entries().count(), 1);
3547 assert_eq!(
3548 wf.entries().next().unwrap().url(),
3549 "https://example.com/foo"
3550 );
3551 }
3552
3553 #[test]
3554 fn test_parse_blank_lines_between_entries() {
3555 let input = concat!(
3557 "version=4\n",
3558 "https://example.com/foo .*/foo-(\\d+)\\.tar\\.gz\n",
3559 "\n",
3560 "https://example.com/bar .*/bar-(\\d+)\\.tar\\.gz\n",
3561 );
3562 let wf: super::WatchFile = input.parse().unwrap();
3563 assert_eq!(wf.to_string(), input);
3564 assert_eq!(wf.entries().count(), 2);
3565 }
3566
3567 #[test]
3568 fn test_parse_trailing_unparseable_tokens_produce_error() {
3569 let input = "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n=garbage\n";
3572 let result = input.parse::<super::WatchFile>();
3573 assert!(result.is_err(), "expected parse error for trailing garbage");
3574 let wf = super::WatchFile::from_str_relaxed(input);
3576 assert_eq!(wf.to_string(), input);
3577 }
3578
3579 #[test]
3580 fn test_parse_roundtrip_full_file() {
3581 let inputs = [
3583 "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n",
3584 "version=4\n# a comment\nhttps://example.com/foo foo-(.*).tar.gz\n",
3585 concat!(
3586 "version=4\n",
3587 "opts=uversionmangle=s/rc/~rc/ \\\n",
3588 " https://example.com/foo foo-(.*).tar.gz\n",
3589 ),
3590 concat!(
3591 "version=4\n",
3592 "# comment before entry\n",
3593 "opts=uversionmangle=s/rc/~rc/ \\\n",
3594 "https://example.com/foo foo-(.*).tar.gz\n",
3595 "# comment between entries\n",
3596 "https://example.com/bar bar-(.*).tar.gz\n",
3597 ),
3598 ];
3599 for input in &inputs {
3600 let wf: super::WatchFile = input.parse().unwrap();
3601 assert_eq!(
3602 wf.to_string(),
3603 *input,
3604 "round-trip failed for input: {:?}",
3605 input
3606 );
3607 }
3608 }
3609}