debian_watch/
parse.rs

1use crate::lex::lex;
2use crate::types::*;
3use crate::SyntaxKind;
4use crate::SyntaxKind::*;
5use crate::DEFAULT_VERSION;
6use std::str::FromStr;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9pub struct ParseError(Vec<String>);
10
11impl std::fmt::Display for ParseError {
12    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
13        for err in &self.0 {
14            writeln!(f, "{}", err)?;
15        }
16        Ok(())
17    }
18}
19
20impl std::error::Error for ParseError {}
21
22/// Second, implementing the `Language` trait teaches rowan to convert between
23/// these two SyntaxKind types, allowing for a nicer SyntaxNode API where
24/// "kinds" are values from our `enum SyntaxKind`, instead of plain u16 values.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26enum Lang {}
27impl rowan::Language for Lang {
28    type Kind = SyntaxKind;
29    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
30        unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
31    }
32    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
33        kind.into()
34    }
35}
36
37/// GreenNode is an immutable tree, which is cheap to change,
38/// but doesn't contain offsets and parent pointers.
39use rowan::GreenNode;
40
41/// You can construct GreenNodes by hand, but a builder
42/// is helpful for top-down parsers: it maintains a stack
43/// of currently in-progress nodes
44use rowan::GreenNodeBuilder;
45
46/// The parse results are stored as a "green tree".
47/// We'll discuss working with the results later
48struct Parse {
49    green_node: GreenNode,
50    #[allow(unused)]
51    errors: Vec<String>,
52    #[allow(unused)]
53    version: i32,
54}
55
56fn parse(text: &str) -> Parse {
57    struct Parser {
58        /// input tokens, including whitespace,
59        /// in *reverse* order.
60        tokens: Vec<(SyntaxKind, String)>,
61        /// the in-progress tree.
62        builder: GreenNodeBuilder<'static>,
63        /// the list of syntax errors we've accumulated
64        /// so far.
65        errors: Vec<String>,
66    }
67
68    impl Parser {
69        fn parse_version(&mut self) -> Option<i32> {
70            let mut version = None;
71            if self.tokens.last() == Some(&(KEY, "version".to_string())) {
72                self.builder.start_node(VERSION.into());
73                self.bump();
74                self.skip_ws();
75                if self.current() != Some(EQUALS) {
76                    self.builder.start_node(ERROR.into());
77                    self.errors.push("expected `=`".to_string());
78                    self.bump();
79                    self.builder.finish_node();
80                } else {
81                    self.bump();
82                }
83                if self.current() != Some(VALUE) {
84                    self.builder.start_node(ERROR.into());
85                    self.errors
86                        .push(format!("expected value, got {:?}", self.current()));
87                    self.bump();
88                    self.builder.finish_node();
89                } else {
90                    let version_str = self.tokens.last().unwrap().1.clone();
91                    match version_str.parse() {
92                        Ok(v) => {
93                            version = Some(v);
94                            self.bump();
95                        }
96                        Err(_) => {
97                            self.builder.start_node(ERROR.into());
98                            self.errors
99                                .push(format!("invalid version: {}", version_str));
100                            self.bump();
101                            self.builder.finish_node();
102                        }
103                    }
104                }
105                if self.current() != Some(NEWLINE) {
106                    self.builder.start_node(ERROR.into());
107                    self.errors.push("expected newline".to_string());
108                    self.bump();
109                    self.builder.finish_node();
110                } else {
111                    self.bump();
112                }
113                self.builder.finish_node();
114            }
115            version
116        }
117
118        fn parse_watch_entry(&mut self) -> bool {
119            self.skip_ws();
120            if self.current().is_none() {
121                return false;
122            }
123            if self.current() == Some(NEWLINE) {
124                self.bump();
125                return false;
126            }
127            self.builder.start_node(ENTRY.into());
128            self.parse_options_list();
129            for i in 0..4 {
130                if self.current() == Some(NEWLINE) {
131                    break;
132                }
133                if self.current() == Some(CONTINUATION) {
134                    self.bump();
135                    self.skip_ws();
136                    continue;
137                }
138                if self.current() != Some(VALUE) && self.current() != Some(KEY) {
139                    self.builder.start_node(ERROR.into());
140                    self.errors.push(format!(
141                        "expected value, got {:?} (i={})",
142                        self.current(),
143                        i
144                    ));
145                    if self.current().is_some() {
146                        self.bump();
147                    }
148                    self.builder.finish_node();
149                } else {
150                    self.bump();
151                }
152                self.skip_ws();
153            }
154            if self.current() != Some(NEWLINE) && self.current().is_some() {
155                self.builder.start_node(ERROR.into());
156                self.errors
157                    .push(format!("expected newline, not {:?}", self.current()));
158                if self.current().is_some() {
159                    self.bump();
160                }
161                self.builder.finish_node();
162            } else {
163                self.bump();
164            }
165            self.builder.finish_node();
166            true
167        }
168
169        fn parse_option(&mut self) -> bool {
170            if self.current().is_none() {
171                return false;
172            }
173            while self.current() == Some(CONTINUATION) {
174                self.bump();
175            }
176            if self.current() == Some(WHITESPACE) {
177                return false;
178            }
179            self.builder.start_node(OPTION.into());
180            if self.current() != Some(KEY) {
181                self.builder.start_node(ERROR.into());
182                self.errors.push("expected key".to_string());
183                self.bump();
184                self.builder.finish_node();
185            } else {
186                self.bump();
187            }
188            if self.current() == Some(EQUALS) {
189                self.bump();
190                if self.current() != Some(VALUE) && self.current() != Some(KEY) {
191                    self.builder.start_node(ERROR.into());
192                    self.errors
193                        .push(format!("expected value, got {:?}", self.current()));
194                    self.bump();
195                    self.builder.finish_node();
196                } else {
197                    self.bump();
198                }
199            } else if self.current() == Some(COMMA) {
200            } else {
201                self.builder.start_node(ERROR.into());
202                self.errors.push("expected `=`".to_string());
203                if self.current().is_some() {
204                    self.bump();
205                }
206                self.builder.finish_node();
207            }
208            self.builder.finish_node();
209            true
210        }
211
212        fn parse_options_list(&mut self) {
213            self.skip_ws();
214            if self.tokens.last() == Some(&(KEY, "opts".to_string()))
215                || self.tokens.last() == Some(&(KEY, "options".to_string()))
216            {
217                self.builder.start_node(OPTS_LIST.into());
218                self.bump();
219                self.skip_ws();
220                if self.current() != Some(EQUALS) {
221                    self.builder.start_node(ERROR.into());
222                    self.errors.push("expected `=`".to_string());
223                    if self.current().is_some() {
224                        self.bump();
225                    }
226                    self.builder.finish_node();
227                } else {
228                    self.bump();
229                }
230                let quoted = if self.current() == Some(QUOTE) {
231                    self.bump();
232                    true
233                } else {
234                    false
235                };
236                loop {
237                    if quoted {
238                        if self.current() == Some(QUOTE) {
239                            self.bump();
240                            break;
241                        }
242                        self.skip_ws();
243                    }
244                    if !self.parse_option() {
245                        break;
246                    }
247                    if self.current() == Some(COMMA) {
248                        self.bump();
249                    } else if !quoted {
250                        break;
251                    }
252                }
253                self.builder.finish_node();
254                self.skip_ws();
255            }
256        }
257
258        fn parse(mut self) -> Parse {
259            let mut version = 1;
260            // Make sure that the root node covers all source
261            self.builder.start_node(ROOT.into());
262            if let Some(v) = self.parse_version() {
263                version = v;
264            }
265            // TODO: use version to influence parsing
266            loop {
267                if !self.parse_watch_entry() {
268                    break;
269                }
270            }
271            // Don't forget to eat *trailing* whitespace
272            self.skip_ws();
273            // Close the root node.
274            self.builder.finish_node();
275
276            // Turn the builder into a GreenNode
277            Parse {
278                green_node: self.builder.finish(),
279                errors: self.errors,
280                version,
281            }
282        }
283        /// Advance one token, adding it to the current branch of the tree builder.
284        fn bump(&mut self) {
285            let (kind, text) = self.tokens.pop().unwrap();
286            self.builder.token(kind.into(), text.as_str());
287        }
288        /// Peek at the first unprocessed token
289        fn current(&self) -> Option<SyntaxKind> {
290            self.tokens.last().map(|(kind, _)| *kind)
291        }
292        fn skip_ws(&mut self) {
293            while self.current() == Some(WHITESPACE)
294                || self.current() == Some(CONTINUATION)
295                || self.current() == Some(COMMENT)
296            {
297                self.bump()
298            }
299        }
300    }
301
302    let mut tokens = lex(text);
303    tokens.reverse();
304    Parser {
305        tokens,
306        builder: GreenNodeBuilder::new(),
307        errors: Vec::new(),
308    }
309    .parse()
310}
311
312/// To work with the parse results we need a view into the
313/// green tree - the Syntax tree.
314/// It is also immutable, like a GreenNode,
315/// but it contains parent pointers, offsets, and
316/// has identity semantics.
317
318type SyntaxNode = rowan::SyntaxNode<Lang>;
319#[allow(unused)]
320type SyntaxToken = rowan::SyntaxToken<Lang>;
321#[allow(unused)]
322type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
323
324impl Parse {
325    fn syntax(&self) -> SyntaxNode {
326        SyntaxNode::new_root(self.green_node.clone())
327    }
328
329    fn root(&self) -> WatchFile {
330        WatchFile::cast(self.syntax()).unwrap()
331    }
332}
333
334macro_rules! ast_node {
335    ($ast:ident, $kind:ident) => {
336        #[derive(PartialEq, Eq, Hash)]
337        #[repr(transparent)]
338        /// A node in the syntax tree for $ast
339        pub struct $ast(SyntaxNode);
340        impl $ast {
341            #[allow(unused)]
342            fn cast(node: SyntaxNode) -> Option<Self> {
343                if node.kind() == $kind {
344                    Some(Self(node))
345                } else {
346                    None
347                }
348            }
349        }
350
351        impl ToString for $ast {
352            fn to_string(&self) -> String {
353                self.0.text().to_string()
354            }
355        }
356    };
357}
358
359ast_node!(WatchFile, ROOT);
360ast_node!(Version, VERSION);
361ast_node!(Entry, ENTRY);
362ast_node!(OptionList, OPTS_LIST);
363ast_node!(_Option, OPTION);
364
365impl WatchFile {
366    /// Create a new watch file with specified version
367    pub fn new(version: Option<u32>) -> WatchFile {
368        let mut builder = GreenNodeBuilder::new();
369
370        builder.start_node(ROOT.into());
371        if let Some(version) = version {
372            builder.start_node(VERSION.into());
373            builder.token(KEY.into(), "version");
374            builder.token(EQUALS.into(), "=");
375            builder.token(VALUE.into(), version.to_string().as_str());
376            builder.token(NEWLINE.into(), "\n");
377            builder.finish_node();
378        }
379        builder.finish_node();
380        WatchFile(SyntaxNode::new_root_mut(builder.finish()))
381    }
382
383    /// Returns the version of the watch file.
384    pub fn version(&self) -> u32 {
385        self.0
386            .children()
387            .find_map(Version::cast)
388            .map(|it| it.version())
389            .unwrap_or(DEFAULT_VERSION)
390    }
391
392    /// Returns an iterator over all entries in the watch file.
393    pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
394        self.0.children().filter_map(Entry::cast)
395    }
396
397    /// Set the version of the watch file.
398    pub fn set_version(&mut self, new_version: u32) {
399        // Build the new version node
400        let mut builder = GreenNodeBuilder::new();
401        builder.start_node(VERSION.into());
402        builder.token(KEY.into(), "version");
403        builder.token(EQUALS.into(), "=");
404        builder.token(VALUE.into(), new_version.to_string().as_str());
405        builder.token(NEWLINE.into(), "\n");
406        builder.finish_node();
407        let new_version_green = builder.finish();
408
409        // Create a syntax node (splice_children will detach and reattach it)
410        let new_version_node = SyntaxNode::new_root_mut(new_version_green);
411
412        // Find existing version node if any
413        let version_pos = self.0.children().position(|child| child.kind() == VERSION);
414
415        if let Some(pos) = version_pos {
416            // Replace existing version node
417            self.0
418                .splice_children(pos..pos + 1, vec![new_version_node.into()]);
419        } else {
420            // Insert version node at the beginning
421            self.0.splice_children(0..0, vec![new_version_node.into()]);
422        }
423    }
424}
425
426impl FromStr for WatchFile {
427    type Err = ParseError;
428
429    fn from_str(s: &str) -> Result<Self, Self::Err> {
430        let parsed = parse(s);
431        if parsed.errors.is_empty() {
432            Ok(parsed.root())
433        } else {
434            Err(ParseError(parsed.errors))
435        }
436    }
437}
438
439impl Version {
440    /// Returns the version of the watch file.
441    pub fn version(&self) -> u32 {
442        self.0
443            .children_with_tokens()
444            .find_map(|it| match it {
445                SyntaxElement::Token(token) => {
446                    if token.kind() == VALUE {
447                        Some(token.text().parse().unwrap())
448                    } else {
449                        None
450                    }
451                }
452                _ => None,
453            })
454            .unwrap_or(DEFAULT_VERSION)
455    }
456}
457
458impl Entry {
459    /// List of options
460    pub fn option_list(&self) -> Option<OptionList> {
461        self.0.children().find_map(OptionList::cast)
462    }
463
464    /// Get the value of an option
465    pub fn get_option(&self, key: &str) -> Option<String> {
466        self.option_list().and_then(|ol| ol.get_option(key))
467    }
468
469    /// Check if an option is set
470    pub fn has_option(&self, key: &str) -> bool {
471        self.option_list().map_or(false, |ol| ol.has_option(key))
472    }
473
474    /// The name of the secondary source tarball
475    pub fn component(&self) -> Option<String> {
476        self.get_option("component")
477    }
478
479    /// Component type
480    pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
481        self.get_option("ctype").map(|s| s.parse()).transpose()
482    }
483
484    /// Compression method
485    pub fn compression(&self) -> Result<Option<Compression>, ()> {
486        self.get_option("compression")
487            .map(|s| s.parse())
488            .transpose()
489    }
490
491    /// Repack the tarball
492    pub fn repack(&self) -> bool {
493        self.has_option("repack")
494    }
495
496    /// Repack suffix
497    pub fn repacksuffix(&self) -> Option<String> {
498        self.get_option("repacksuffix")
499    }
500
501    /// Retrieve the mode of the watch file entry.
502    pub fn mode(&self) -> Result<Mode, ()> {
503        Ok(self
504            .get_option("mode")
505            .map(|s| s.parse())
506            .transpose()?
507            .unwrap_or_default())
508    }
509
510    /// Return the git pretty mode
511    pub fn pretty(&self) -> Result<Pretty, ()> {
512        Ok(self
513            .get_option("pretty")
514            .map(|s| s.parse())
515            .transpose()?
516            .unwrap_or_default())
517    }
518
519    /// Set the date string used by the pretty option to an arbitrary format as an optional
520    /// opts argument when the matching-pattern is HEAD or heads/branch for git mode.
521    pub fn date(&self) -> String {
522        self.get_option("date")
523            .unwrap_or_else(|| "%Y%m%d".to_string())
524    }
525
526    /// Return the git export mode
527    pub fn gitexport(&self) -> Result<GitExport, ()> {
528        Ok(self
529            .get_option("gitexport")
530            .map(|s| s.parse())
531            .transpose()?
532            .unwrap_or_default())
533    }
534
535    /// Return the git mode
536    pub fn gitmode(&self) -> Result<GitMode, ()> {
537        Ok(self
538            .get_option("gitmode")
539            .map(|s| s.parse())
540            .transpose()?
541            .unwrap_or_default())
542    }
543
544    /// Return the pgp mode
545    pub fn pgpmode(&self) -> Result<PgpMode, ()> {
546        Ok(self
547            .get_option("pgpmode")
548            .map(|s| s.parse())
549            .transpose()?
550            .unwrap_or_default())
551    }
552
553    /// Return the search mode
554    pub fn searchmode(&self) -> Result<SearchMode, ()> {
555        Ok(self
556            .get_option("searchmode")
557            .map(|s| s.parse())
558            .transpose()?
559            .unwrap_or_default())
560    }
561
562    /// Return the decompression mode
563    pub fn decompress(&self) -> bool {
564        self.has_option("decompress")
565    }
566
567    /// Whether to disable all site specific special case code such as URL director uses and page
568    /// content alterations.
569    pub fn bare(&self) -> bool {
570        self.has_option("bare")
571    }
572
573    /// Set the user-agent string used to contact the HTTP(S) server as user-agent-string. (persistent)
574    pub fn user_agent(&self) -> Option<String> {
575        self.get_option("user-agent")
576    }
577
578    /// Use PASV mode for the FTP connection.
579    pub fn passive(&self) -> Option<bool> {
580        if self.has_option("passive") || self.has_option("pasv") {
581            Some(true)
582        } else if self.has_option("active") || self.has_option("nopasv") {
583            Some(false)
584        } else {
585            None
586        }
587    }
588
589    /// Add the extra options to use with the unzip command, such as -a, -aa, and -b, when executed
590    /// by mk-origtargz.
591    pub fn unzipoptions(&self) -> Option<String> {
592        self.get_option("unzipopt")
593    }
594
595    /// Normalize the downloaded web page string.
596    pub fn dversionmangle(&self) -> Option<String> {
597        self.get_option("dversionmangle")
598            .or_else(|| self.get_option("versionmangle"))
599    }
600
601    /// Normalize the directory path string matching the regex in a set of parentheses of
602    /// http://URL as the sortable version index string.  This is used
603    /// as the directory path sorting index only.
604    pub fn dirversionmangle(&self) -> Option<String> {
605        self.get_option("dirversionmangle")
606    }
607
608    /// Normalize the downloaded web page string.
609    pub fn pagemangle(&self) -> Option<String> {
610        self.get_option("pagemangle")
611    }
612
613    /// Normalize the candidate upstream version strings extracted from hrefs in the
614    /// source of the web page.  This is used as the version sorting index when selecting the
615    /// latest upstream version.
616    pub fn uversionmangle(&self) -> Option<String> {
617        self.get_option("uversionmangle")
618            .or_else(|| self.get_option("versionmangle"))
619    }
620
621    /// Syntactic shorthand for uversionmangle=rules, dversionmangle=rules
622    pub fn versionmangle(&self) -> Option<String> {
623        self.get_option("versionmangle")
624    }
625
626    /// Convert the selected upstream tarball href string from the percent-encoded hexadecimal
627    /// string to the decoded normal URL  string  for  obfuscated
628    /// web sites.  Only percent-encoding is available and it is decoded with
629    /// s/%([A-Fa-f\d]{2})/chr hex $1/eg.
630    pub fn hrefdecode(&self) -> bool {
631        self.get_option("hrefdecode").is_some()
632    }
633
634    /// Convert the selected upstream tarball href string into the accessible URL for obfuscated
635    /// web sites.  This is run after hrefdecode.
636    pub fn downloadurlmangle(&self) -> Option<String> {
637        self.get_option("downloadurlmangle")
638    }
639
640    /// Generate the upstream tarball filename from the selected href string if matching-pattern
641    /// can extract the latest upstream version <uversion> from the  selected  href  string.
642    /// Otherwise, generate the upstream tarball filename from its full URL string and set the
643    /// missing <uversion> from the generated upstream tarball filename.
644    ///
645    /// Without this option, the default upstream tarball filename is generated by taking the last
646    /// component of the URL and  removing everything  after any '?' or '#'.
647    pub fn filenamemangle(&self) -> Option<String> {
648        self.get_option("filenamemangle")
649    }
650
651    /// Generate the candidate upstream signature file URL string from the upstream tarball URL.
652    pub fn pgpsigurlmangle(&self) -> Option<String> {
653        self.get_option("pgpsigurlmangle")
654    }
655
656    /// Generate the version string <oversion> of the source tarball <spkg>_<oversion>.orig.tar.gz
657    /// from <uversion>.  This should be used to add a suffix such as +dfsg to a MUT package.
658    pub fn oversionmangle(&self) -> Option<String> {
659        self.get_option("oversionmangle")
660    }
661
662    /// Returns options set
663    pub fn opts(&self) -> std::collections::HashMap<String, String> {
664        let mut options = std::collections::HashMap::new();
665
666        if let Some(ol) = self.option_list() {
667            for opt in ol.children() {
668                let key = opt.key();
669                let value = opt.value();
670                if let (Some(key), Some(value)) = (key, value) {
671                    options.insert(key.to_string(), value.to_string());
672                }
673            }
674        }
675
676        options
677    }
678
679    fn items(&self) -> impl Iterator<Item = String> + '_ {
680        self.0.children_with_tokens().filter_map(|it| match it {
681            SyntaxElement::Token(token) => {
682                if token.kind() == VALUE || token.kind() == KEY {
683                    Some(token.text().to_string())
684                } else {
685                    None
686                }
687            }
688            _ => None,
689        })
690    }
691
692    /// Returns the URL of the entry.
693    pub fn url(&self) -> String {
694        self.items().next().unwrap()
695    }
696
697    /// Returns the matching pattern of the entry.
698    pub fn matching_pattern(&self) -> Option<String> {
699        self.items().nth(1)
700    }
701
702    /// Returns the version policy
703    pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
704        self.items().nth(2).map(|it| it.parse()).transpose()
705    }
706
707    /// Returns the script of the entry.
708    pub fn script(&self) -> Option<String> {
709        self.items().nth(3)
710    }
711
712    /// Replace all substitutions and return the resulting URL.
713    pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
714        subst(self.url().as_str(), package).parse().unwrap()
715    }
716}
717
718const SUBSTITUTIONS: &[(&str, &str)] = &[
719    // This is substituted with the source package name found in the first line
720    // of the debian/changelog file.
721    // "@PACKAGE@": None,
722    // This is substituted by the legal upstream version regex (capturing).
723    ("@ANY_VERSION@", r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"),
724    // This is substituted by the typical archive file extension regex
725    // (non-capturing).
726    (
727        "@ARCHIVE_EXT@",
728        r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)",
729    ),
730    // This is substituted by the typical signature file extension regex
731    // (non-capturing).
732    (
733        "@SIGNATURE_EXT@",
734        r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)\.(?:asc|pgp|gpg|sig|sign)",
735    ),
736    // This is substituted by the typical Debian extension regexp (capturing).
737    ("@DEB_EXT@", r"[\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$"),
738];
739
740pub fn subst(text: &str, package: impl FnOnce() -> String) -> String {
741    let mut substs = SUBSTITUTIONS.to_vec();
742    let package_name;
743    if text.contains("@PACKAGE@") {
744        package_name = Some(package());
745        substs.push(("@PACKAGE@", package_name.as_deref().unwrap()));
746    }
747
748    let mut text = text.to_string();
749
750    for (k, v) in substs {
751        text = text.replace(k, v);
752    }
753
754    text
755}
756
757#[test]
758fn test_subst() {
759    assert_eq!(
760        subst("@ANY_VERSION@", || unreachable!()),
761        r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"
762    );
763    assert_eq!(subst("@PACKAGE@", || "dulwich".to_string()), "dulwich");
764}
765
766impl OptionList {
767    fn children(&self) -> impl Iterator<Item = _Option> + '_ {
768        self.0.children().filter_map(_Option::cast)
769    }
770
771    pub fn has_option(&self, key: &str) -> bool {
772        self.children().any(|it| it.key().as_deref() == Some(key))
773    }
774
775    pub fn get_option(&self, key: &str) -> Option<String> {
776        for child in self.children() {
777            if child.key().as_deref() == Some(key) {
778                return child.value();
779            }
780        }
781        None
782    }
783}
784
785impl _Option {
786    /// Returns the key of the option.
787    pub fn key(&self) -> Option<String> {
788        self.0.children_with_tokens().find_map(|it| match it {
789            SyntaxElement::Token(token) => {
790                if token.kind() == KEY {
791                    Some(token.text().to_string())
792                } else {
793                    None
794                }
795            }
796            _ => None,
797        })
798    }
799
800    /// Returns the value of the option.
801    pub fn value(&self) -> Option<String> {
802        self.0
803            .children_with_tokens()
804            .filter_map(|it| match it {
805                SyntaxElement::Token(token) => {
806                    if token.kind() == VALUE || token.kind() == KEY {
807                        Some(token.text().to_string())
808                    } else {
809                        None
810                    }
811                }
812                _ => None,
813            })
814            .nth(1)
815    }
816}
817
818#[test]
819fn test_parse_v1() {
820    const WATCHV1: &str = r#"version=4
821opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
822  https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
823"#;
824    let parsed = parse(WATCHV1);
825    //assert_eq!(parsed.errors, Vec::<String>::new());
826    let node = parsed.syntax();
827    assert_eq!(
828        format!("{:#?}", node),
829        r#"ROOT@0..161
830  VERSION@0..10
831    KEY@0..7 "version"
832    EQUALS@7..8 "="
833    VALUE@8..9 "4"
834    NEWLINE@9..10 "\n"
835  ENTRY@10..161
836    OPTS_LIST@10..86
837      KEY@10..14 "opts"
838      EQUALS@14..15 "="
839      OPTION@15..19
840        KEY@15..19 "bare"
841      COMMA@19..20 ","
842      OPTION@20..86
843        KEY@20..34 "filenamemangle"
844        EQUALS@34..35 "="
845        VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
846    WHITESPACE@86..87 " "
847    CONTINUATION@87..89 "\\\n"
848    WHITESPACE@89..91 "  "
849    VALUE@91..138 "https://github.com/sy ..."
850    WHITESPACE@138..139 " "
851    VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
852    NEWLINE@160..161 "\n"
853"#
854    );
855
856    let root = parsed.root();
857    assert_eq!(root.version(), 4);
858    let entries = root.entries().collect::<Vec<_>>();
859    assert_eq!(entries.len(), 1);
860    let entry = &entries[0];
861    assert_eq!(
862        entry.url(),
863        "https://github.com/syncthing/syncthing-gtk/tags"
864    );
865    assert_eq!(
866        entry.matching_pattern(),
867        Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
868    );
869    assert_eq!(entry.version(), Ok(None));
870    assert_eq!(entry.script(), None);
871
872    assert_eq!(node.text(), WATCHV1);
873}
874
875#[test]
876fn test_parse_v2() {
877    let parsed = parse(
878        r#"version=4
879https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
880# comment
881"#,
882    );
883    assert_eq!(parsed.errors, Vec::<String>::new());
884    let node = parsed.syntax();
885    assert_eq!(
886        format!("{:#?}", node),
887        r###"ROOT@0..90
888  VERSION@0..10
889    KEY@0..7 "version"
890    EQUALS@7..8 "="
891    VALUE@8..9 "4"
892    NEWLINE@9..10 "\n"
893  ENTRY@10..80
894    VALUE@10..57 "https://github.com/sy ..."
895    WHITESPACE@57..58 " "
896    VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
897    NEWLINE@79..80 "\n"
898  COMMENT@80..89 "# comment"
899  NEWLINE@89..90 "\n"
900"###
901    );
902
903    let root = parsed.root();
904    assert_eq!(root.version(), 4);
905    let entries = root.entries().collect::<Vec<_>>();
906    assert_eq!(entries.len(), 1);
907    let entry = &entries[0];
908    assert_eq!(
909        entry.url(),
910        "https://github.com/syncthing/syncthing-gtk/tags"
911    );
912    assert_eq!(
913        entry.format_url(|| "syncthing-gtk".to_string()),
914        "https://github.com/syncthing/syncthing-gtk/tags"
915            .parse()
916            .unwrap()
917    );
918}
919
920#[test]
921fn test_parse_v3() {
922    let parsed = parse(
923        r#"version=4
924https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
925# comment
926"#,
927    );
928    assert_eq!(parsed.errors, Vec::<String>::new());
929    let root = parsed.root();
930    assert_eq!(root.version(), 4);
931    let entries = root.entries().collect::<Vec<_>>();
932    assert_eq!(entries.len(), 1);
933    let entry = &entries[0];
934    assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
935    assert_eq!(
936        entry.format_url(|| "syncthing-gtk".to_string()),
937        "https://github.com/syncthing/syncthing-gtk/tags"
938            .parse()
939            .unwrap()
940    );
941}
942
943#[test]
944fn test_parse_v4() {
945    let cl: super::WatchFile = r#"version=4
946opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
947    https://github.com/example/example-cat/tags \
948        (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
949"#
950    .parse()
951    .unwrap();
952    assert_eq!(cl.version(), 4);
953    let entries = cl.entries().collect::<Vec<_>>();
954    assert_eq!(entries.len(), 1);
955    let entry = &entries[0];
956    assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
957    assert_eq!(
958        entry.matching_pattern(),
959        Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
960    );
961    assert!(entry.repack());
962    assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
963    assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
964    assert_eq!(entry.repacksuffix(), Some("+ds".into()));
965    assert_eq!(entry.script(), Some("uupdate".into()));
966    assert_eq!(
967        entry.format_url(|| "example-cat".to_string()),
968        "https://github.com/example/example-cat/tags"
969            .parse()
970            .unwrap()
971    );
972    assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
973}
974
975#[test]
976fn test_git_mode() {
977    let text = r#"version=3
978opts="mode=git, gitmode=shallow, pgpmode=gittag" \
979https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
980refs/tags/(.*) debian
981"#;
982    let parsed = parse(text);
983    assert_eq!(parsed.errors, Vec::<String>::new());
984    let cl = parsed.root();
985    assert_eq!(cl.version(), 3);
986    let entries = cl.entries().collect::<Vec<_>>();
987    assert_eq!(entries.len(), 1);
988    let entry = &entries[0];
989    assert_eq!(
990        entry.url(),
991        "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
992    );
993    assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
994    assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
995    assert_eq!(entry.script(), None);
996    assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
997    assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
998    assert_eq!(entry.mode(), Ok(Mode::Git));
999}
1000
1001#[test]
1002fn test_parse_quoted() {
1003    const WATCHV1: &str = r#"version=4
1004opts="bare, filenamemangle=blah" \
1005  https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1006"#;
1007    let parsed = parse(WATCHV1);
1008    //assert_eq!(parsed.errors, Vec::<String>::new());
1009    let node = parsed.syntax();
1010
1011    let root = parsed.root();
1012    assert_eq!(root.version(), 4);
1013    let entries = root.entries().collect::<Vec<_>>();
1014    assert_eq!(entries.len(), 1);
1015    let entry = &entries[0];
1016
1017    assert_eq!(
1018        entry.url(),
1019        "https://github.com/syncthing/syncthing-gtk/tags"
1020    );
1021    assert_eq!(
1022        entry.matching_pattern(),
1023        Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1024    );
1025    assert_eq!(entry.version(), Ok(None));
1026    assert_eq!(entry.script(), None);
1027
1028    assert_eq!(node.text(), WATCHV1);
1029}