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(&self, package: impl FnOnce() -> String) -> url::Url {
1553 crate::subst::subst(self.url().as_str(), package)
1554 .parse()
1555 .unwrap()
1556 }
1557
1558 pub fn set_url(&mut self, new_url: &str) {
1560 let mut builder = GreenNodeBuilder::new();
1562 builder.start_node(URL.into());
1563 builder.token(VALUE.into(), new_url);
1564 builder.finish_node();
1565 let new_url_green = builder.finish();
1566
1567 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1569
1570 let url_pos = self
1572 .0
1573 .children_with_tokens()
1574 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1575
1576 if let Some(pos) = url_pos {
1577 self.0
1579 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1580 }
1581 }
1582
1583 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1589 let mut builder = GreenNodeBuilder::new();
1591 builder.start_node(MATCHING_PATTERN.into());
1592 builder.token(VALUE.into(), new_pattern);
1593 builder.finish_node();
1594 let new_pattern_green = builder.finish();
1595
1596 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1598
1599 let pattern_pos = self.0.children_with_tokens().position(
1601 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1602 );
1603
1604 if let Some(pos) = pattern_pos {
1605 self.0
1607 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1608 }
1609 }
1611
1612 pub fn set_version_policy(&mut self, new_policy: &str) {
1618 let mut builder = GreenNodeBuilder::new();
1620 builder.start_node(VERSION_POLICY.into());
1621 builder.token(VALUE.into(), new_policy);
1623 builder.finish_node();
1624 let new_policy_green = builder.finish();
1625
1626 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1628
1629 let policy_pos = self.0.children_with_tokens().position(
1631 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1632 );
1633
1634 if let Some(pos) = policy_pos {
1635 self.0
1637 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1638 }
1639 }
1641
1642 pub fn set_script(&mut self, new_script: &str) {
1648 let mut builder = GreenNodeBuilder::new();
1650 builder.start_node(SCRIPT.into());
1651 builder.token(VALUE.into(), new_script);
1653 builder.finish_node();
1654 let new_script_green = builder.finish();
1655
1656 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1658
1659 let script_pos = self
1661 .0
1662 .children_with_tokens()
1663 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1664
1665 if let Some(pos) = script_pos {
1666 self.0
1668 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1669 }
1670 }
1672
1673 pub fn set_option(&mut self, option: crate::types::WatchOption) {
1679 let key = watch_option_to_key(&option);
1680 let value = watch_option_to_value(&option);
1681 self.set_opt(key, &value);
1682 }
1683
1684 pub fn set_opt(&mut self, key: &str, value: &str) {
1690 let opts_pos = self.0.children_with_tokens().position(
1692 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1693 );
1694
1695 if let Some(_opts_idx) = opts_pos {
1696 if let Some(mut ol) = self.option_list() {
1697 if let Some(mut opt) = ol.find_option(key) {
1699 opt.set_value(value);
1701 } else {
1703 ol.add_option(key, value);
1705 }
1707 }
1708 } else {
1709 let mut builder = GreenNodeBuilder::new();
1711 builder.start_node(OPTS_LIST.into());
1712 builder.token(KEY.into(), "opts");
1713 builder.token(EQUALS.into(), "=");
1714 builder.start_node(OPTION.into());
1715 builder.token(KEY.into(), key);
1716 builder.token(EQUALS.into(), "=");
1717 builder.token(VALUE.into(), value);
1718 builder.finish_node();
1719 builder.finish_node();
1720 let new_opts_green = builder.finish();
1721 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1722
1723 let url_pos = self
1725 .0
1726 .children_with_tokens()
1727 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1728
1729 if let Some(url_idx) = url_pos {
1730 let mut combined_builder = GreenNodeBuilder::new();
1733 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1735 combined_builder.finish_node();
1736 let temp_green = combined_builder.finish();
1737 let temp_root = SyntaxNode::new_root_mut(temp_green);
1738 let space_element = temp_root.children_with_tokens().next().unwrap();
1739
1740 self.0
1741 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1742 } else {
1743 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1744 }
1745 }
1746 }
1747
1748 pub fn del_opt(&mut self, option: crate::types::WatchOption) {
1755 let key = watch_option_to_key(&option);
1756 if let Some(mut ol) = self.option_list() {
1757 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1758
1759 if option_count == 1 && ol.has_option(key) {
1760 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1762
1763 if let Some(opts_idx) = opts_pos {
1764 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1766
1767 while self.0.children_with_tokens().next().map_or(false, |e| {
1769 matches!(
1770 e,
1771 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1772 )
1773 }) {
1774 self.0.splice_children(0..1, vec![]);
1775 }
1776 }
1777 } else {
1778 ol.remove_option(key);
1780 }
1781 }
1782 }
1783
1784 pub fn del_opt_str(&mut self, key: &str) {
1791 if let Some(mut ol) = self.option_list() {
1792 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1793
1794 if option_count == 1 && ol.has_option(key) {
1795 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1797
1798 if let Some(opts_idx) = opts_pos {
1799 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1801
1802 while self.0.children_with_tokens().next().map_or(false, |e| {
1804 matches!(
1805 e,
1806 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1807 )
1808 }) {
1809 self.0.splice_children(0..1, vec![]);
1810 }
1811 }
1812 } else {
1813 ol.remove_option(key);
1815 }
1816 }
1817 }
1818}
1819
1820impl std::fmt::Debug for OptionList {
1821 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1822 f.debug_struct("OptionList")
1823 .field("text", &self.0.text().to_string())
1824 .finish()
1825 }
1826}
1827
1828impl OptionList {
1829 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1831 self.0.children().filter_map(_Option::cast)
1832 }
1833
1834 pub fn find_option(&self, key: &str) -> Option<_Option> {
1836 self.options().find(|opt| opt.key().as_deref() == Some(key))
1837 }
1838
1839 pub fn has_option(&self, key: &str) -> bool {
1841 self.options().any(|it| it.key().as_deref() == Some(key))
1842 }
1843
1844 #[cfg(feature = "deb822")]
1847 pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
1848 self.options().filter_map(|opt| {
1849 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
1850 Some((key, value))
1851 } else {
1852 None
1853 }
1854 })
1855 }
1856
1857 pub fn get_option(&self, key: &str) -> Option<String> {
1859 for child in self.options() {
1860 if child.key().as_deref() == Some(key) {
1861 return child.value();
1862 }
1863 }
1864 None
1865 }
1866
1867 fn add_option(&mut self, key: &str, value: &str) {
1869 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1870
1871 let mut builder = GreenNodeBuilder::new();
1873 builder.start_node(ROOT.into()); if option_count > 0 {
1876 builder.start_node(OPTION_SEPARATOR.into());
1877 builder.token(COMMA.into(), ",");
1878 builder.finish_node();
1879 }
1880
1881 builder.start_node(OPTION.into());
1882 builder.token(KEY.into(), key);
1883 builder.token(EQUALS.into(), "=");
1884 builder.token(VALUE.into(), value);
1885 builder.finish_node();
1886
1887 builder.finish_node(); let combined_green = builder.finish();
1889
1890 let temp_root = SyntaxNode::new_root_mut(combined_green);
1892 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1893
1894 let insert_pos = self.0.children_with_tokens().count();
1895 self.0.splice_children(insert_pos..insert_pos, new_children);
1896 }
1897
1898 fn remove_option(&mut self, key: &str) -> bool {
1900 if let Some(mut opt) = self.find_option(key) {
1901 opt.remove();
1902 true
1903 } else {
1904 false
1905 }
1906 }
1907}
1908
1909impl _Option {
1910 pub fn key(&self) -> Option<String> {
1912 self.0.children_with_tokens().find_map(|it| match it {
1913 SyntaxElement::Token(token) => {
1914 if token.kind() == KEY {
1915 Some(token.text().to_string())
1916 } else {
1917 None
1918 }
1919 }
1920 _ => None,
1921 })
1922 }
1923
1924 pub fn value(&self) -> Option<String> {
1926 self.0
1927 .children_with_tokens()
1928 .filter_map(|it| match it {
1929 SyntaxElement::Token(token) => {
1930 if token.kind() == VALUE || token.kind() == KEY {
1931 Some(token.text().to_string())
1932 } else {
1933 None
1934 }
1935 }
1936 _ => None,
1937 })
1938 .nth(1)
1939 }
1940
1941 pub fn set_value(&mut self, new_value: &str) {
1943 let key = self.key().expect("Option must have a key");
1944
1945 let mut builder = GreenNodeBuilder::new();
1947 builder.start_node(OPTION.into());
1948 builder.token(KEY.into(), &key);
1949 builder.token(EQUALS.into(), "=");
1950 builder.token(VALUE.into(), new_value);
1951 builder.finish_node();
1952 let new_option_green = builder.finish();
1953 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1954
1955 if let Some(parent) = self.0.parent() {
1957 let idx = self.0.index();
1958 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1959 }
1960 }
1961
1962 pub fn remove(&mut self) {
1964 let next_sep = self
1966 .0
1967 .next_sibling()
1968 .filter(|n| n.kind() == OPTION_SEPARATOR);
1969 let prev_sep = self
1970 .0
1971 .prev_sibling()
1972 .filter(|n| n.kind() == OPTION_SEPARATOR);
1973
1974 if let Some(sep) = next_sep {
1976 sep.detach();
1977 } else if let Some(sep) = prev_sep {
1978 sep.detach();
1979 }
1980
1981 self.0.detach();
1983 }
1984}
1985
1986impl Url {
1987 pub fn url(&self) -> String {
1989 self.0
1990 .children_with_tokens()
1991 .find_map(|it| match it {
1992 SyntaxElement::Token(token) => {
1993 if token.kind() == VALUE {
1994 Some(token.text().to_string())
1995 } else {
1996 None
1997 }
1998 }
1999 _ => None,
2000 })
2001 .unwrap()
2002 }
2003}
2004
2005impl MatchingPattern {
2006 pub fn pattern(&self) -> String {
2008 self.0
2009 .children_with_tokens()
2010 .find_map(|it| match it {
2011 SyntaxElement::Token(token) => {
2012 if token.kind() == VALUE {
2013 Some(token.text().to_string())
2014 } else {
2015 None
2016 }
2017 }
2018 _ => None,
2019 })
2020 .unwrap()
2021 }
2022}
2023
2024impl VersionPolicyNode {
2025 pub fn policy(&self) -> String {
2027 self.0
2028 .children_with_tokens()
2029 .find_map(|it| match it {
2030 SyntaxElement::Token(token) => {
2031 if token.kind() == VALUE || token.kind() == KEY {
2033 Some(token.text().to_string())
2034 } else {
2035 None
2036 }
2037 }
2038 _ => None,
2039 })
2040 .unwrap()
2041 }
2042}
2043
2044impl ScriptNode {
2045 pub fn script(&self) -> String {
2047 self.0
2048 .children_with_tokens()
2049 .find_map(|it| match it {
2050 SyntaxElement::Token(token) => {
2051 if token.kind() == VALUE || token.kind() == KEY {
2053 Some(token.text().to_string())
2054 } else {
2055 None
2056 }
2057 }
2058 _ => None,
2059 })
2060 .unwrap()
2061 }
2062}
2063
2064#[cfg(test)]
2065mod tests {
2066 use super::*;
2067
2068 #[test]
2069 fn test_entry_node_structure() {
2070 let wf: super::WatchFile = r#"version=4
2072opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2073"#
2074 .parse()
2075 .unwrap();
2076
2077 let entry = wf.entries().next().unwrap();
2078
2079 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2081 assert_eq!(entry.url(), "https://example.com/releases");
2082
2083 assert_eq!(
2085 entry
2086 .0
2087 .children()
2088 .find(|n| n.kind() == MATCHING_PATTERN)
2089 .is_some(),
2090 true
2091 );
2092 assert_eq!(
2093 entry.matching_pattern(),
2094 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2095 );
2096
2097 assert_eq!(
2099 entry
2100 .0
2101 .children()
2102 .find(|n| n.kind() == VERSION_POLICY)
2103 .is_some(),
2104 true
2105 );
2106 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2107
2108 assert_eq!(
2110 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2111 true
2112 );
2113 assert_eq!(entry.script(), Some("uupdate".into()));
2114 }
2115
2116 #[test]
2117 fn test_entry_node_structure_partial() {
2118 let wf: super::WatchFile = r#"version=4
2120https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2121"#
2122 .parse()
2123 .unwrap();
2124
2125 let entry = wf.entries().next().unwrap();
2126
2127 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2129 assert_eq!(
2130 entry
2131 .0
2132 .children()
2133 .find(|n| n.kind() == MATCHING_PATTERN)
2134 .is_some(),
2135 true
2136 );
2137
2138 assert_eq!(
2140 entry
2141 .0
2142 .children()
2143 .find(|n| n.kind() == VERSION_POLICY)
2144 .is_some(),
2145 false
2146 );
2147 assert_eq!(
2148 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2149 false
2150 );
2151
2152 assert_eq!(entry.url(), "https://github.com/example/tags");
2154 assert_eq!(
2155 entry.matching_pattern(),
2156 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2157 );
2158 assert_eq!(entry.version(), Ok(None));
2159 assert_eq!(entry.script(), None);
2160 }
2161
2162 #[test]
2163 fn test_parse_v1() {
2164 const WATCHV1: &str = r#"version=4
2165opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2166 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2167"#;
2168 let parsed = parse(WATCHV1);
2169 let node = parsed.syntax();
2171 assert_eq!(
2172 format!("{:#?}", node),
2173 r#"ROOT@0..161
2174 VERSION@0..10
2175 KEY@0..7 "version"
2176 EQUALS@7..8 "="
2177 VALUE@8..9 "4"
2178 NEWLINE@9..10 "\n"
2179 ENTRY@10..161
2180 OPTS_LIST@10..86
2181 KEY@10..14 "opts"
2182 EQUALS@14..15 "="
2183 OPTION@15..19
2184 KEY@15..19 "bare"
2185 OPTION_SEPARATOR@19..20
2186 COMMA@19..20 ","
2187 OPTION@20..86
2188 KEY@20..34 "filenamemangle"
2189 EQUALS@34..35 "="
2190 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2191 WHITESPACE@86..87 " "
2192 CONTINUATION@87..89 "\\\n"
2193 WHITESPACE@89..91 " "
2194 URL@91..138
2195 VALUE@91..138 "https://github.com/sy ..."
2196 WHITESPACE@138..139 " "
2197 MATCHING_PATTERN@139..160
2198 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2199 NEWLINE@160..161 "\n"
2200"#
2201 );
2202
2203 let root = parsed.root();
2204 assert_eq!(root.version(), 4);
2205 let entries = root.entries().collect::<Vec<_>>();
2206 assert_eq!(entries.len(), 1);
2207 let entry = &entries[0];
2208 assert_eq!(
2209 entry.url(),
2210 "https://github.com/syncthing/syncthing-gtk/tags"
2211 );
2212 assert_eq!(
2213 entry.matching_pattern(),
2214 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2215 );
2216 assert_eq!(entry.version(), Ok(None));
2217 assert_eq!(entry.script(), None);
2218
2219 assert_eq!(node.text(), WATCHV1);
2220 }
2221
2222 #[test]
2223 fn test_parse_v2() {
2224 let parsed = parse(
2225 r#"version=4
2226https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2227# comment
2228"#,
2229 );
2230 assert_eq!(parsed.errors, Vec::<String>::new());
2231 let node = parsed.syntax();
2232 assert_eq!(
2233 format!("{:#?}", node),
2234 r###"ROOT@0..90
2235 VERSION@0..10
2236 KEY@0..7 "version"
2237 EQUALS@7..8 "="
2238 VALUE@8..9 "4"
2239 NEWLINE@9..10 "\n"
2240 ENTRY@10..80
2241 URL@10..57
2242 VALUE@10..57 "https://github.com/sy ..."
2243 WHITESPACE@57..58 " "
2244 MATCHING_PATTERN@58..79
2245 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2246 NEWLINE@79..80 "\n"
2247 COMMENT@80..89 "# comment"
2248 NEWLINE@89..90 "\n"
2249"###
2250 );
2251
2252 let root = parsed.root();
2253 assert_eq!(root.version(), 4);
2254 let entries = root.entries().collect::<Vec<_>>();
2255 assert_eq!(entries.len(), 1);
2256 let entry = &entries[0];
2257 assert_eq!(
2258 entry.url(),
2259 "https://github.com/syncthing/syncthing-gtk/tags"
2260 );
2261 assert_eq!(
2262 entry.format_url(|| "syncthing-gtk".to_string()),
2263 "https://github.com/syncthing/syncthing-gtk/tags"
2264 .parse()
2265 .unwrap()
2266 );
2267 }
2268
2269 #[test]
2270 fn test_parse_v3() {
2271 let parsed = parse(
2272 r#"version=4
2273https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2274# comment
2275"#,
2276 );
2277 assert_eq!(parsed.errors, Vec::<String>::new());
2278 let root = parsed.root();
2279 assert_eq!(root.version(), 4);
2280 let entries = root.entries().collect::<Vec<_>>();
2281 assert_eq!(entries.len(), 1);
2282 let entry = &entries[0];
2283 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2284 assert_eq!(
2285 entry.format_url(|| "syncthing-gtk".to_string()),
2286 "https://github.com/syncthing/syncthing-gtk/tags"
2287 .parse()
2288 .unwrap()
2289 );
2290 }
2291
2292 #[test]
2293 fn test_thread_safe_parsing() {
2294 let text = r#"version=4
2295https://github.com/example/example/tags example-(.*)\.tar\.gz
2296"#;
2297
2298 let parsed = parse_watch_file(text);
2299 assert!(parsed.is_ok());
2300 assert_eq!(parsed.errors().len(), 0);
2301
2302 let watchfile = parsed.tree();
2304 assert_eq!(watchfile.version(), 4);
2305 let entries: Vec<_> = watchfile.entries().collect();
2306 assert_eq!(entries.len(), 1);
2307 }
2308
2309 #[test]
2310 fn test_parse_clone_and_eq() {
2311 let text = r#"version=4
2312https://github.com/example/example/tags example-(.*)\.tar\.gz
2313"#;
2314
2315 let parsed1 = parse_watch_file(text);
2316 let parsed2 = parsed1.clone();
2317
2318 assert_eq!(parsed1, parsed2);
2320
2321 let watchfile1 = parsed1.tree();
2323 let watchfile2 = watchfile1.clone();
2324 assert_eq!(watchfile1, watchfile2);
2325 }
2326
2327 #[test]
2328 fn test_parse_v4() {
2329 let cl: super::WatchFile = r#"version=4
2330opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2331 https://github.com/example/example-cat/tags \
2332 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2333"#
2334 .parse()
2335 .unwrap();
2336 assert_eq!(cl.version(), 4);
2337 let entries = cl.entries().collect::<Vec<_>>();
2338 assert_eq!(entries.len(), 1);
2339 let entry = &entries[0];
2340 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2341 assert_eq!(
2342 entry.matching_pattern(),
2343 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2344 );
2345 assert!(entry.repack());
2346 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2347 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2348 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2349 assert_eq!(entry.script(), Some("uupdate".into()));
2350 assert_eq!(
2351 entry.format_url(|| "example-cat".to_string()),
2352 "https://github.com/example/example-cat/tags"
2353 .parse()
2354 .unwrap()
2355 );
2356 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2357 }
2358
2359 #[test]
2360 fn test_git_mode() {
2361 let text = r#"version=3
2362opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2363https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2364refs/tags/(.*) debian
2365"#;
2366 let parsed = parse(text);
2367 assert_eq!(parsed.errors, Vec::<String>::new());
2368 let cl = parsed.root();
2369 assert_eq!(cl.version(), 3);
2370 let entries = cl.entries().collect::<Vec<_>>();
2371 assert_eq!(entries.len(), 1);
2372 let entry = &entries[0];
2373 assert_eq!(
2374 entry.url(),
2375 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2376 );
2377 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2378 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2379 assert_eq!(entry.script(), None);
2380 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2381 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2382 assert_eq!(entry.mode(), Ok(Mode::Git));
2383 }
2384
2385 #[test]
2386 fn test_parse_quoted() {
2387 const WATCHV1: &str = r#"version=4
2388opts="bare, filenamemangle=blah" \
2389 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2390"#;
2391 let parsed = parse(WATCHV1);
2392 let node = parsed.syntax();
2394
2395 let root = parsed.root();
2396 assert_eq!(root.version(), 4);
2397 let entries = root.entries().collect::<Vec<_>>();
2398 assert_eq!(entries.len(), 1);
2399 let entry = &entries[0];
2400
2401 assert_eq!(
2402 entry.url(),
2403 "https://github.com/syncthing/syncthing-gtk/tags"
2404 );
2405 assert_eq!(
2406 entry.matching_pattern(),
2407 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2408 );
2409 assert_eq!(entry.version(), Ok(None));
2410 assert_eq!(entry.script(), None);
2411
2412 assert_eq!(node.text(), WATCHV1);
2413 }
2414
2415 #[test]
2416 fn test_set_url() {
2417 let wf: super::WatchFile = r#"version=4
2419https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2420"#
2421 .parse()
2422 .unwrap();
2423
2424 let mut entry = wf.entries().next().unwrap();
2425 assert_eq!(
2426 entry.url(),
2427 "https://github.com/syncthing/syncthing-gtk/tags"
2428 );
2429
2430 entry.set_url("https://newurl.example.org/path");
2431 assert_eq!(entry.url(), "https://newurl.example.org/path");
2432 assert_eq!(
2433 entry.matching_pattern(),
2434 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2435 );
2436
2437 assert_eq!(
2439 entry.to_string(),
2440 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2441 );
2442 }
2443
2444 #[test]
2445 fn test_set_url_with_options() {
2446 let wf: super::WatchFile = r#"version=4
2448opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2449"#
2450 .parse()
2451 .unwrap();
2452
2453 let mut entry = wf.entries().next().unwrap();
2454 assert_eq!(entry.url(), "https://foo.com/bar");
2455 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2456
2457 entry.set_url("https://example.com/baz");
2458 assert_eq!(entry.url(), "https://example.com/baz");
2459
2460 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2462 assert_eq!(
2463 entry.matching_pattern(),
2464 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2465 );
2466
2467 assert_eq!(
2469 entry.to_string(),
2470 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2471 );
2472 }
2473
2474 #[test]
2475 fn test_set_url_complex() {
2476 let wf: super::WatchFile = r#"version=4
2478opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2479 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2480"#
2481 .parse()
2482 .unwrap();
2483
2484 let mut entry = wf.entries().next().unwrap();
2485 assert_eq!(
2486 entry.url(),
2487 "https://github.com/syncthing/syncthing-gtk/tags"
2488 );
2489
2490 entry.set_url("https://gitlab.com/newproject/tags");
2491 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2492
2493 assert!(entry.bare());
2495 assert_eq!(
2496 entry.filenamemangle(),
2497 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2498 );
2499 assert_eq!(
2500 entry.matching_pattern(),
2501 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2502 );
2503
2504 assert_eq!(
2506 entry.to_string(),
2507 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2508 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2509"#
2510 );
2511 }
2512
2513 #[test]
2514 fn test_set_url_with_all_fields() {
2515 let wf: super::WatchFile = r#"version=4
2517opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2518 https://github.com/example/example-cat/tags \
2519 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2520"#
2521 .parse()
2522 .unwrap();
2523
2524 let mut entry = wf.entries().next().unwrap();
2525 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
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 entry.set_url("https://gitlab.example.org/project/releases");
2534 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2535
2536 assert!(entry.repack());
2538 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2539 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2540 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2541 assert_eq!(
2542 entry.matching_pattern(),
2543 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2544 );
2545 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2546 assert_eq!(entry.script(), Some("uupdate".into()));
2547
2548 assert_eq!(
2550 entry.to_string(),
2551 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2552 https://gitlab.example.org/project/releases \
2553 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2554"#
2555 );
2556 }
2557
2558 #[test]
2559 fn test_set_url_quoted_options() {
2560 let wf: super::WatchFile = r#"version=4
2562opts="bare, filenamemangle=blah" \
2563 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2564"#
2565 .parse()
2566 .unwrap();
2567
2568 let mut entry = wf.entries().next().unwrap();
2569 assert_eq!(
2570 entry.url(),
2571 "https://github.com/syncthing/syncthing-gtk/tags"
2572 );
2573
2574 entry.set_url("https://example.org/new/path");
2575 assert_eq!(entry.url(), "https://example.org/new/path");
2576
2577 assert_eq!(
2579 entry.to_string(),
2580 r#"opts="bare, filenamemangle=blah" \
2581 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2582"#
2583 );
2584 }
2585
2586 #[test]
2587 fn test_set_opt_update_existing() {
2588 let wf: super::WatchFile = r#"version=4
2590opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2591"#
2592 .parse()
2593 .unwrap();
2594
2595 let mut entry = wf.entries().next().unwrap();
2596 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2597 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2598
2599 entry.set_opt("foo", "updated");
2600 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2601 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2602
2603 assert_eq!(
2605 entry.to_string(),
2606 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2607 );
2608 }
2609
2610 #[test]
2611 fn test_set_opt_add_new() {
2612 let wf: super::WatchFile = r#"version=4
2614opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2615"#
2616 .parse()
2617 .unwrap();
2618
2619 let mut entry = wf.entries().next().unwrap();
2620 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2621 assert_eq!(entry.get_option("bar"), None);
2622
2623 entry.set_opt("bar", "baz");
2624 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2625 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2626
2627 assert_eq!(
2629 entry.to_string(),
2630 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2631 );
2632 }
2633
2634 #[test]
2635 fn test_set_opt_create_options_list() {
2636 let wf: super::WatchFile = r#"version=4
2638https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2639"#
2640 .parse()
2641 .unwrap();
2642
2643 let mut entry = wf.entries().next().unwrap();
2644 assert_eq!(entry.option_list(), None);
2645
2646 entry.set_opt("compression", "xz");
2647 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2648
2649 assert_eq!(
2651 entry.to_string(),
2652 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2653 );
2654 }
2655
2656 #[test]
2657 fn test_del_opt_remove_single() {
2658 let wf: super::WatchFile = r#"version=4
2660opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2661"#
2662 .parse()
2663 .unwrap();
2664
2665 let mut entry = wf.entries().next().unwrap();
2666 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2667 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2668 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2669
2670 entry.del_opt_str("bar");
2671 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2672 assert_eq!(entry.get_option("bar"), None);
2673 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2674
2675 assert_eq!(
2677 entry.to_string(),
2678 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2679 );
2680 }
2681
2682 #[test]
2683 fn test_del_opt_remove_first() {
2684 let wf: super::WatchFile = r#"version=4
2686opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2687"#
2688 .parse()
2689 .unwrap();
2690
2691 let mut entry = wf.entries().next().unwrap();
2692 entry.del_opt_str("foo");
2693 assert_eq!(entry.get_option("foo"), None);
2694 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2695
2696 assert_eq!(
2698 entry.to_string(),
2699 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2700 );
2701 }
2702
2703 #[test]
2704 fn test_del_opt_remove_last() {
2705 let wf: super::WatchFile = r#"version=4
2707opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2708"#
2709 .parse()
2710 .unwrap();
2711
2712 let mut entry = wf.entries().next().unwrap();
2713 entry.del_opt_str("bar");
2714 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2715 assert_eq!(entry.get_option("bar"), None);
2716
2717 assert_eq!(
2719 entry.to_string(),
2720 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2721 );
2722 }
2723
2724 #[test]
2725 fn test_del_opt_remove_only_option() {
2726 let wf: super::WatchFile = r#"version=4
2728opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2729"#
2730 .parse()
2731 .unwrap();
2732
2733 let mut entry = wf.entries().next().unwrap();
2734 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2735
2736 entry.del_opt_str("foo");
2737 assert_eq!(entry.get_option("foo"), None);
2738 assert_eq!(entry.option_list(), None);
2739
2740 assert_eq!(
2742 entry.to_string(),
2743 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2744 );
2745 }
2746
2747 #[test]
2748 fn test_del_opt_nonexistent() {
2749 let wf: super::WatchFile = r#"version=4
2751opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2752"#
2753 .parse()
2754 .unwrap();
2755
2756 let mut entry = wf.entries().next().unwrap();
2757 let original = entry.to_string();
2758
2759 entry.del_opt_str("nonexistent");
2760 assert_eq!(entry.to_string(), original);
2761 }
2762
2763 #[test]
2764 fn test_set_opt_multiple_operations() {
2765 let wf: super::WatchFile = r#"version=4
2767https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2768"#
2769 .parse()
2770 .unwrap();
2771
2772 let mut entry = wf.entries().next().unwrap();
2773
2774 entry.set_opt("compression", "xz");
2775 entry.set_opt("repack", "");
2776 entry.set_opt("dversionmangle", "s/\\+ds//");
2777
2778 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2779 assert_eq!(
2780 entry.get_option("dversionmangle"),
2781 Some("s/\\+ds//".to_string())
2782 );
2783 }
2784
2785 #[test]
2786 fn test_set_matching_pattern() {
2787 let wf: super::WatchFile = r#"version=4
2789https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2790"#
2791 .parse()
2792 .unwrap();
2793
2794 let mut entry = wf.entries().next().unwrap();
2795 assert_eq!(
2796 entry.matching_pattern(),
2797 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2798 );
2799
2800 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2801 assert_eq!(
2802 entry.matching_pattern(),
2803 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2804 );
2805
2806 assert_eq!(entry.url(), "https://github.com/example/tags");
2808
2809 assert_eq!(
2811 entry.to_string(),
2812 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2813 );
2814 }
2815
2816 #[test]
2817 fn test_set_matching_pattern_with_all_fields() {
2818 let wf: super::WatchFile = r#"version=4
2820opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2821"#
2822 .parse()
2823 .unwrap();
2824
2825 let mut entry = wf.entries().next().unwrap();
2826 assert_eq!(
2827 entry.matching_pattern(),
2828 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2829 );
2830
2831 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2832 assert_eq!(
2833 entry.matching_pattern(),
2834 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2835 );
2836
2837 assert_eq!(entry.url(), "https://example.com/releases");
2839 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2840 assert_eq!(entry.script(), Some("uupdate".into()));
2841 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2842
2843 assert_eq!(
2845 entry.to_string(),
2846 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2847 );
2848 }
2849
2850 #[test]
2851 fn test_set_version_policy() {
2852 let wf: super::WatchFile = r#"version=4
2854https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2855"#
2856 .parse()
2857 .unwrap();
2858
2859 let mut entry = wf.entries().next().unwrap();
2860 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2861
2862 entry.set_version_policy("previous");
2863 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2864
2865 assert_eq!(entry.url(), "https://example.com/releases");
2867 assert_eq!(
2868 entry.matching_pattern(),
2869 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2870 );
2871 assert_eq!(entry.script(), Some("uupdate".into()));
2872
2873 assert_eq!(
2875 entry.to_string(),
2876 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2877 );
2878 }
2879
2880 #[test]
2881 fn test_set_version_policy_with_options() {
2882 let wf: super::WatchFile = r#"version=4
2884opts=repack,compression=xz \
2885 https://github.com/example/example-cat/tags \
2886 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2887"#
2888 .parse()
2889 .unwrap();
2890
2891 let mut entry = wf.entries().next().unwrap();
2892 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2893
2894 entry.set_version_policy("ignore");
2895 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2896
2897 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2899 assert_eq!(
2900 entry.matching_pattern(),
2901 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2902 );
2903 assert_eq!(entry.script(), Some("uupdate".into()));
2904 assert!(entry.repack());
2905
2906 assert_eq!(
2908 entry.to_string(),
2909 r#"opts=repack,compression=xz \
2910 https://github.com/example/example-cat/tags \
2911 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2912"#
2913 );
2914 }
2915
2916 #[test]
2917 fn test_set_script() {
2918 let wf: super::WatchFile = r#"version=4
2920https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2921"#
2922 .parse()
2923 .unwrap();
2924
2925 let mut entry = wf.entries().next().unwrap();
2926 assert_eq!(entry.script(), Some("uupdate".into()));
2927
2928 entry.set_script("uscan");
2929 assert_eq!(entry.script(), Some("uscan".into()));
2930
2931 assert_eq!(entry.url(), "https://example.com/releases");
2933 assert_eq!(
2934 entry.matching_pattern(),
2935 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2936 );
2937 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2938
2939 assert_eq!(
2941 entry.to_string(),
2942 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2943 );
2944 }
2945
2946 #[test]
2947 fn test_set_script_with_options() {
2948 let wf: super::WatchFile = r#"version=4
2950opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2951"#
2952 .parse()
2953 .unwrap();
2954
2955 let mut entry = wf.entries().next().unwrap();
2956 assert_eq!(entry.script(), Some("uupdate".into()));
2957
2958 entry.set_script("custom-script.sh");
2959 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2960
2961 assert_eq!(entry.url(), "https://example.com/releases");
2963 assert_eq!(
2964 entry.matching_pattern(),
2965 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2966 );
2967 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2968 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2969
2970 assert_eq!(
2972 entry.to_string(),
2973 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2974 );
2975 }
2976
2977 #[test]
2978 fn test_apply_dversionmangle() {
2979 let wf: super::WatchFile = r#"version=4
2981opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2982"#
2983 .parse()
2984 .unwrap();
2985 let entry = wf.entries().next().unwrap();
2986 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2987 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2988
2989 let wf: super::WatchFile = r#"version=4
2991opts=versionmangle=s/^v// https://example.com/ .*
2992"#
2993 .parse()
2994 .unwrap();
2995 let entry = wf.entries().next().unwrap();
2996 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
2997
2998 let wf: super::WatchFile = r#"version=4
3000opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
3001"#
3002 .parse()
3003 .unwrap();
3004 let entry = wf.entries().next().unwrap();
3005 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
3006
3007 let wf: super::WatchFile = r#"version=4
3009https://example.com/ .*
3010"#
3011 .parse()
3012 .unwrap();
3013 let entry = wf.entries().next().unwrap();
3014 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
3015 }
3016
3017 #[test]
3018 fn test_apply_oversionmangle() {
3019 let wf: super::WatchFile = r#"version=4
3021opts=oversionmangle=s/$/-1/ https://example.com/ .*
3022"#
3023 .parse()
3024 .unwrap();
3025 let entry = wf.entries().next().unwrap();
3026 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
3027 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
3028
3029 let wf: super::WatchFile = r#"version=4
3031opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
3032"#
3033 .parse()
3034 .unwrap();
3035 let entry = wf.entries().next().unwrap();
3036 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
3037
3038 let wf: super::WatchFile = r#"version=4
3040https://example.com/ .*
3041"#
3042 .parse()
3043 .unwrap();
3044 let entry = wf.entries().next().unwrap();
3045 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
3046 }
3047
3048 #[test]
3049 fn test_apply_dirversionmangle() {
3050 let wf: super::WatchFile = r#"version=4
3052opts=dirversionmangle=s/^v// https://example.com/ .*
3053"#
3054 .parse()
3055 .unwrap();
3056 let entry = wf.entries().next().unwrap();
3057 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3058 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
3059
3060 let wf: super::WatchFile = r#"version=4
3062opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
3063"#
3064 .parse()
3065 .unwrap();
3066 let entry = wf.entries().next().unwrap();
3067 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3068
3069 let wf: super::WatchFile = r#"version=4
3071https://example.com/ .*
3072"#
3073 .parse()
3074 .unwrap();
3075 let entry = wf.entries().next().unwrap();
3076 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
3077 }
3078
3079 #[test]
3080 fn test_apply_filenamemangle() {
3081 let wf: super::WatchFile = r#"version=4
3083opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
3084"#
3085 .parse()
3086 .unwrap();
3087 let entry = wf.entries().next().unwrap();
3088 assert_eq!(
3089 entry
3090 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
3091 .unwrap(),
3092 "mypackage-1.0.tar.gz"
3093 );
3094 assert_eq!(
3095 entry
3096 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
3097 .unwrap(),
3098 "mypackage-2.5.3.tar.gz"
3099 );
3100
3101 let wf: super::WatchFile = r#"version=4
3103opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
3104"#
3105 .parse()
3106 .unwrap();
3107 let entry = wf.entries().next().unwrap();
3108 assert_eq!(
3109 entry
3110 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
3111 .unwrap(),
3112 "file.tar.gz"
3113 );
3114
3115 let wf: super::WatchFile = r#"version=4
3117https://example.com/ .*
3118"#
3119 .parse()
3120 .unwrap();
3121 let entry = wf.entries().next().unwrap();
3122 assert_eq!(
3123 entry
3124 .apply_filenamemangle("https://example.com/file.tar.gz")
3125 .unwrap(),
3126 "https://example.com/file.tar.gz"
3127 );
3128 }
3129
3130 #[test]
3131 fn test_apply_pagemangle() {
3132 let wf: super::WatchFile = r#"version=4
3134opts=pagemangle=s/&/&/g https://example.com/ .*
3135"#
3136 .parse()
3137 .unwrap();
3138 let entry = wf.entries().next().unwrap();
3139 assert_eq!(
3140 entry.apply_pagemangle(b"foo & bar").unwrap(),
3141 b"foo & bar"
3142 );
3143 assert_eq!(
3144 entry
3145 .apply_pagemangle(b"& foo & bar &")
3146 .unwrap(),
3147 b"& foo & bar &"
3148 );
3149
3150 let wf: super::WatchFile = r#"version=4
3152opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3153"#
3154 .parse()
3155 .unwrap();
3156 let entry = wf.entries().next().unwrap();
3157 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3158
3159 let wf: super::WatchFile = r#"version=4
3161https://example.com/ .*
3162"#
3163 .parse()
3164 .unwrap();
3165 let entry = wf.entries().next().unwrap();
3166 assert_eq!(
3167 entry.apply_pagemangle(b"foo & bar").unwrap(),
3168 b"foo & bar"
3169 );
3170 }
3171
3172 #[test]
3173 fn test_apply_downloadurlmangle() {
3174 let wf: super::WatchFile = r#"version=4
3176opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3177"#
3178 .parse()
3179 .unwrap();
3180 let entry = wf.entries().next().unwrap();
3181 assert_eq!(
3182 entry
3183 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3184 .unwrap(),
3185 "https://example.com/download/file.tar.gz"
3186 );
3187
3188 let wf: super::WatchFile = r#"version=4
3190opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3191"#
3192 .parse()
3193 .unwrap();
3194 let entry = wf.entries().next().unwrap();
3195 assert_eq!(
3196 entry
3197 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3198 .unwrap(),
3199 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3200 );
3201
3202 let wf: super::WatchFile = r#"version=4
3204https://example.com/ .*
3205"#
3206 .parse()
3207 .unwrap();
3208 let entry = wf.entries().next().unwrap();
3209 assert_eq!(
3210 entry
3211 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3212 .unwrap(),
3213 "https://example.com/archive/file.tar.gz"
3214 );
3215 }
3216
3217 #[test]
3218 fn test_entry_builder_minimal() {
3219 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3221 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3222 .build();
3223
3224 assert_eq!(entry.url(), "https://github.com/example/tags");
3225 assert_eq!(
3226 entry.matching_pattern().as_deref(),
3227 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3228 );
3229 assert_eq!(entry.version(), Ok(None));
3230 assert_eq!(entry.script(), None);
3231 assert!(entry.opts().is_empty());
3232 }
3233
3234 #[test]
3235 fn test_entry_builder_url_only() {
3236 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3238
3239 assert_eq!(entry.url(), "https://example.com/releases");
3240 assert_eq!(entry.matching_pattern(), None);
3241 assert_eq!(entry.version(), Ok(None));
3242 assert_eq!(entry.script(), None);
3243 assert!(entry.opts().is_empty());
3244 }
3245
3246 #[test]
3247 fn test_entry_builder_with_all_fields() {
3248 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3250 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3251 .version_policy("debian")
3252 .script("uupdate")
3253 .opt("compression", "xz")
3254 .flag("repack")
3255 .build();
3256
3257 assert_eq!(entry.url(), "https://github.com/example/tags");
3258 assert_eq!(
3259 entry.matching_pattern().as_deref(),
3260 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3261 );
3262 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3263 assert_eq!(entry.script(), Some("uupdate".into()));
3264 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3265 assert!(entry.has_option("repack"));
3266 assert!(entry.repack());
3267 }
3268
3269 #[test]
3270 fn test_entry_builder_multiple_options() {
3271 let entry = super::EntryBuilder::new("https://example.com/tags")
3273 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3274 .opt("compression", "xz")
3275 .opt("dversionmangle", "s/\\+ds//")
3276 .opt("repacksuffix", "+ds")
3277 .build();
3278
3279 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3280 assert_eq!(
3281 entry.get_option("dversionmangle"),
3282 Some("s/\\+ds//".to_string())
3283 );
3284 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3285 }
3286
3287 #[test]
3288 fn test_entry_builder_via_entry() {
3289 let entry = super::Entry::builder("https://github.com/example/tags")
3291 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3292 .version_policy("debian")
3293 .build();
3294
3295 assert_eq!(entry.url(), "https://github.com/example/tags");
3296 assert_eq!(
3297 entry.matching_pattern().as_deref(),
3298 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3299 );
3300 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3301 }
3302
3303 #[test]
3304 fn test_watchfile_add_entry_to_empty() {
3305 let mut wf = super::WatchFile::new(Some(4));
3307
3308 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3309 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3310 .build();
3311
3312 wf.add_entry(entry);
3313
3314 assert_eq!(wf.version(), 4);
3315 assert_eq!(wf.entries().count(), 1);
3316
3317 let added_entry = wf.entries().next().unwrap();
3318 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3319 assert_eq!(
3320 added_entry.matching_pattern().as_deref(),
3321 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3322 );
3323 }
3324
3325 #[test]
3326 fn test_watchfile_add_multiple_entries() {
3327 let mut wf = super::WatchFile::new(Some(4));
3329
3330 wf.add_entry(
3331 super::EntryBuilder::new("https://github.com/example1/tags")
3332 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3333 .build(),
3334 );
3335
3336 wf.add_entry(
3337 super::EntryBuilder::new("https://github.com/example2/releases")
3338 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3339 .opt("compression", "xz")
3340 .build(),
3341 );
3342
3343 assert_eq!(wf.entries().count(), 2);
3344
3345 let entries: Vec<_> = wf.entries().collect();
3346 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3347 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3348 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3349 }
3350
3351 #[test]
3352 fn test_watchfile_add_entry_to_existing() {
3353 let mut wf: super::WatchFile = r#"version=4
3355https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3356"#
3357 .parse()
3358 .unwrap();
3359
3360 assert_eq!(wf.entries().count(), 1);
3361
3362 wf.add_entry(
3363 super::EntryBuilder::new("https://github.com/example/new")
3364 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3365 .opt("compression", "xz")
3366 .version_policy("debian")
3367 .build(),
3368 );
3369
3370 assert_eq!(wf.entries().count(), 2);
3371
3372 let entries: Vec<_> = wf.entries().collect();
3373 assert_eq!(entries[0].url(), "https://example.com/old");
3374 assert_eq!(entries[1].url(), "https://github.com/example/new");
3375 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3376 }
3377
3378 #[test]
3379 fn test_entry_builder_formatting() {
3380 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3382 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3383 .opt("compression", "xz")
3384 .flag("repack")
3385 .version_policy("debian")
3386 .script("uupdate")
3387 .build();
3388
3389 let entry_str = entry.to_string();
3390
3391 assert!(entry_str.starts_with("opts="));
3393 assert!(entry_str.contains("https://github.com/example/tags"));
3395 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3397 assert!(entry_str.contains("debian"));
3399 assert!(entry_str.contains("uupdate"));
3401 assert!(entry_str.ends_with('\n'));
3403 }
3404
3405 #[test]
3406 fn test_watchfile_add_entry_preserves_format() {
3407 let mut wf = super::WatchFile::new(Some(4));
3409
3410 wf.add_entry(
3411 super::EntryBuilder::new("https://github.com/example/tags")
3412 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3413 .build(),
3414 );
3415
3416 let wf_str = wf.to_string();
3417
3418 assert!(wf_str.starts_with("version=4\n"));
3420 assert!(wf_str.contains("https://github.com/example/tags"));
3422
3423 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3425 assert_eq!(reparsed.version(), 4);
3426 assert_eq!(reparsed.entries().count(), 1);
3427 }
3428
3429 #[test]
3430 fn test_line_col() {
3431 let text = r#"version=4
3432opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3433"#;
3434 let wf = text.parse::<super::WatchFile>().unwrap();
3435
3436 let version_node = wf.version_node().unwrap();
3438 assert_eq!(version_node.line(), 0);
3439 assert_eq!(version_node.column(), 0);
3440 assert_eq!(version_node.line_col(), (0, 0));
3441
3442 let entries: Vec<_> = wf.entries().collect();
3444 assert_eq!(entries.len(), 1);
3445
3446 assert_eq!(entries[0].line(), 1);
3448 assert_eq!(entries[0].column(), 0);
3449 assert_eq!(entries[0].line_col(), (1, 0));
3450
3451 let option_list = entries[0].option_list().unwrap();
3453 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3456 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3459 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3462 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3465 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3469 assert_eq!(options.len(), 1);
3470 assert_eq!(options[0].key(), Some("compression".to_string()));
3471 assert_eq!(options[0].value(), Some("xz".to_string()));
3472 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3476 assert_eq!(compression_opt.line(), 1);
3477 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3479 }
3480
3481 #[test]
3482 fn test_parse_str_relaxed() {
3483 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3484 r#"version=4
3485ERRORS IN THIS LINE
3486opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3487"#,
3488 );
3489 assert_eq!(wf.version(), 4);
3490 assert_eq!(wf.entries().count(), 2);
3491
3492 let entries = wf.entries().collect::<Vec<_>>();
3493
3494 let entry = &entries[0];
3495 assert_eq!(entry.url(), "ERRORS");
3496
3497 let entry = &entries[1];
3498 assert_eq!(entry.url(), "https://example.com/releases");
3499 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3500 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3501 }
3502
3503 #[test]
3504 fn test_parse_entry_with_comment_before() {
3505 let input = concat!(
3509 "version=4\n",
3510 "# try also https://pypi.debian.net/tomoscan/watch\n",
3511 "opts=uversionmangle=s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/ \\\n",
3512 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))\n"
3513 );
3514 let wf: super::WatchFile = input.parse().unwrap();
3515 assert_eq!(wf.to_string(), input);
3517 assert_eq!(wf.entries().count(), 1);
3518 let entry = wf.entries().next().unwrap();
3519 assert_eq!(
3520 entry.url(),
3521 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))"
3522 );
3523 assert_eq!(
3524 entry.get_option("uversionmangle"),
3525 Some("s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/".to_string())
3526 );
3527 }
3528
3529 #[test]
3530 fn test_parse_multiple_comments_before_entry() {
3531 let input = concat!(
3534 "version=4\n",
3535 "# first comment\n",
3536 "# second comment\n",
3537 "# third comment\n",
3538 "https://example.com/foo foo-(.*).tar.gz\n",
3539 );
3540 let wf: super::WatchFile = input.parse().unwrap();
3541 assert_eq!(wf.to_string(), input);
3542 assert_eq!(wf.entries().count(), 1);
3543 assert_eq!(wf.entries().next().unwrap().url(), "https://example.com/foo");
3544 }
3545
3546 #[test]
3547 fn test_parse_blank_lines_between_entries() {
3548 let input = concat!(
3550 "version=4\n",
3551 "https://example.com/foo .*/foo-(\\d+)\\.tar\\.gz\n",
3552 "\n",
3553 "https://example.com/bar .*/bar-(\\d+)\\.tar\\.gz\n",
3554 );
3555 let wf: super::WatchFile = input.parse().unwrap();
3556 assert_eq!(wf.to_string(), input);
3557 assert_eq!(wf.entries().count(), 2);
3558 }
3559
3560 #[test]
3561 fn test_parse_trailing_unparseable_tokens_produce_error() {
3562 let input = "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n=garbage\n";
3565 let result = input.parse::<super::WatchFile>();
3566 assert!(result.is_err(), "expected parse error for trailing garbage");
3567 let wf = super::WatchFile::from_str_relaxed(input);
3569 assert_eq!(wf.to_string(), input);
3570 }
3571
3572 #[test]
3573 fn test_parse_roundtrip_full_file() {
3574 let inputs = [
3576 "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n",
3577 "version=4\n# a comment\nhttps://example.com/foo foo-(.*).tar.gz\n",
3578 concat!(
3579 "version=4\n",
3580 "opts=uversionmangle=s/rc/~rc/ \\\n",
3581 " https://example.com/foo foo-(.*).tar.gz\n",
3582 ),
3583 concat!(
3584 "version=4\n",
3585 "# comment before entry\n",
3586 "opts=uversionmangle=s/rc/~rc/ \\\n",
3587 "https://example.com/foo foo-(.*).tar.gz\n",
3588 "# comment between entries\n",
3589 "https://example.com/bar bar-(.*).tar.gz\n",
3590 ),
3591 ];
3592 for input in &inputs {
3593 let wf: super::WatchFile = input.parse().unwrap();
3594 assert_eq!(
3595 wf.to_string(),
3596 *input,
3597 "round-trip failed for input: {:?}",
3598 input
3599 );
3600 }
3601 }
3602}