alpm_repo_db/desc/
parser.rs

1//! Parser for [alpm-repo-desc] files.
2//!
3//! [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
4
5use std::{fmt::Display, str::FromStr};
6
7use alpm_parsers::iter_str_context;
8use alpm_types::{
9    Architecture,
10    Base64OpenPGPSignature,
11    BuildDate,
12    CompressedSize,
13    FullVersion,
14    Group,
15    InstalledSize,
16    License,
17    Md5Checksum,
18    Name,
19    OptionalDependency,
20    PackageBaseName,
21    PackageDescription,
22    PackageFileName,
23    PackageRelation,
24    Packager,
25    RelationOrSoname,
26    Sha256Checksum,
27    Url,
28};
29use strum::{Display, EnumString, VariantNames};
30use winnow::{
31    ModalResult,
32    Parser,
33    ascii::{line_ending, newline, space0, till_line_ending},
34    combinator::{
35        alt,
36        cut_err,
37        delimited,
38        eof,
39        opt,
40        peek,
41        preceded,
42        repeat,
43        repeat_till,
44        terminated,
45    },
46    error::{StrContext, StrContextValue},
47    token::take_while,
48};
49
50/// A known section name in an [alpm-repo-desc] file.
51///
52/// Section names are e.g. `%NAME%` or `%VERSION%`.
53///
54/// [alpm-repo-desc]: https://alpm.archlinux.page/specifications/alpm-repo-desc.5.html
55#[derive(Clone, Debug, Display, EnumString, Eq, Hash, PartialEq, VariantNames)]
56#[strum(serialize_all = "UPPERCASE")]
57pub enum SectionKeyword {
58    /// %FILENAME%
59    Filename,
60    /// %Name%
61    Name,
62    /// %BASE%
63    Base,
64    /// %VERSION%
65    Version,
66    /// %DESC%
67    Desc,
68    /// %GROUPS%
69    Groups,
70    /// %CSIZE%
71    CSize,
72    /// %ISIZE%
73    ISize,
74    /// %MD5SUM%
75    Md5Sum,
76    /// %SHA256SUM%
77    Sha256Sum,
78    /// %PGPSIG%
79    PgpSig,
80    /// %URL%
81    Url,
82    /// %LICENSE%
83    License,
84    /// %ARCH%
85    Arch,
86    /// %BUILDDATE%
87    BuildDate,
88    /// %PACKAGER%
89    Packager,
90    /// %REPLACES%
91    Replaces,
92    /// %CONFLICTS%
93    Conflicts,
94    /// %PROVIDES%
95    Provides,
96    /// %DEPENDS%
97    Depends,
98    /// %OPTDEPENDS%
99    OptDepends,
100    /// %MAKEDEPENDS%
101    MakeDepends,
102    /// %CHECKDEPENDS%
103    CheckDepends,
104}
105
106impl SectionKeyword {
107    /// Recognizes a [`SectionKeyword`] in an input string slice.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use alpm_repo_db::desc::SectionKeyword;
113    ///
114    /// # fn main() -> winnow::ModalResult<()> {
115    /// let mut input = "%NAME%\nfoo\n";
116    /// let kw = SectionKeyword::parser(&mut input)?;
117    /// assert_eq!(kw, SectionKeyword::Name);
118    /// # Ok(())
119    /// # }
120    /// ```
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if the input does not start with a valid
125    /// `%SECTION%` header followed by a newline.
126    pub fn parser(input: &mut &str) -> ModalResult<Self> {
127        let section = delimited("%", take_while(1.., |c| c != '%'), "%");
128        terminated(
129            preceded(space0, section.try_map(Self::from_str)),
130            line_ending,
131        )
132        .parse_next(input)
133    }
134}
135
136/// A single logical section from a repo desc file.
137#[derive(Clone, Debug)]
138pub enum Section {
139    /// %FILENAME%
140    Filename(PackageFileName),
141    /// %NAME%
142    Name(Name),
143    /// %BASE%
144    Base(PackageBaseName),
145    /// %VERSION%
146    Version(FullVersion),
147    /// %DESC%
148    Desc(PackageDescription),
149    /// %GROUPS%
150    Groups(Vec<Group>),
151    /// %CSIZE%
152    CSize(CompressedSize),
153    /// %ISIZE%
154    ISize(InstalledSize),
155    /// %MD5SUM%
156    Md5Sum(Md5Checksum),
157    /// %SHA256SUM%
158    Sha256Sum(Sha256Checksum),
159    /// %PGPSIG%
160    PgpSig(Base64OpenPGPSignature),
161    /// %URL%
162    Url(Option<Url>),
163    /// %LICENSE%
164    License(Vec<License>),
165    /// %ARCH%
166    Arch(Architecture),
167    /// %BUILDDATE%
168    BuildDate(BuildDate),
169    /// %PACKAGER%
170    Packager(Packager),
171    /// %REPLACES%
172    Replaces(Vec<PackageRelation>),
173    /// %CONFLICTS%
174    Conflicts(Vec<PackageRelation>),
175    /// %PROVIDES%
176    Provides(Vec<RelationOrSoname>),
177    /// %DEPENDS%
178    Depends(Vec<RelationOrSoname>),
179    /// %OPTDEPENDS%
180    OptDepends(Vec<OptionalDependency>),
181    /// %MAKEDEPENDS%
182    MakeDepends(Vec<PackageRelation>),
183    /// %CHECKDEPENDS%
184    CheckDepends(Vec<PackageRelation>),
185}
186
187/// One or multiple newlines.
188///
189/// This also handles the case where there might be multiple lines with spaces.
190fn newlines(input: &mut &str) -> ModalResult<()> {
191    repeat(0.., line_ending).parse_next(input)
192}
193
194/// Parses a single value from the input.
195///
196/// Consumes text until the end of the current line.
197///
198/// # Errors
199///
200/// Returns an error if the next token cannot be parsed into `T`.
201fn value<T>(input: &mut &str) -> ModalResult<T>
202where
203    T: FromStr + Display,
204    T::Err: Display,
205{
206    // Parse until the end of the line and attempt conversion to `T`.
207    let value = till_line_ending.parse_to().parse_next(input)?;
208
209    // Consume the newline or handle end-of-file gracefully.
210    alt((line_ending, eof)).parse_next(input)?;
211
212    Ok(value)
213}
214
215fn opt_value<T>(input: &mut &str) -> ModalResult<Option<T>>
216where
217    T: FromStr + Display,
218    T::Err: Display,
219{
220    // Parse until the end of the line and attempt conversion to `Option<T>`.
221    let value = opt(till_line_ending.parse_to()).parse_next(input)?;
222
223    // Consume the newline or handle end-of-file gracefully.
224    alt((line_ending, eof)).parse_next(input)?;
225
226    Ok(value)
227}
228
229/// Parses a list of values from the input.
230///
231/// Repeats `value()` until the next section header (`%...%`)
232/// or the end of the file.
233///
234/// # Errors
235///
236/// Returns an error if a value cannot be parsed into `T` or if the
237/// section layout does not match expectations.
238fn values<T>(input: &mut &str) -> ModalResult<Vec<T>>
239where
240    T: FromStr + Display,
241    T::Err: Display,
242{
243    let next_section = peek(preceded(newline, SectionKeyword::parser)).map(|_| ());
244
245    // Consume blank lines
246    let blank_line = terminated(space0, newline).map(|_| ());
247
248    repeat_till(0.., value, alt((next_section, blank_line, eof.map(|_| ()))))
249        .context(StrContext::Label("values"))
250        .context(StrContext::Expected(StrContextValue::Description(
251            "a list of values in the database desc file",
252        )))
253        .parse_next(input)
254        .map(|(outs, _)| outs)
255}
256
257/// Parses a single `%SECTION%` block and returns a [`Section`] variant.
258///
259/// # Errors
260///
261/// Returns an error if:
262///
263/// - the section name is invalid or not recognized,
264/// - the section body contains malformed values,
265/// - or the section does not terminate properly.
266fn section(input: &mut &str) -> ModalResult<Section> {
267    // Parse and validate the header keyword first.
268    let section_keyword = cut_err(SectionKeyword::parser)
269        .context(StrContext::Label("section name"))
270        .context(StrContext::Expected(StrContextValue::Description(
271            "a section name that is enclosed in `%` characters",
272        )))
273        .context_with(iter_str_context!([SectionKeyword::VARIANTS]))
274        .parse_next(input)?;
275
276    // Delegate to the corresponding value or values parser.
277    let section = match section_keyword {
278        SectionKeyword::Filename => Section::Filename(value(input)?),
279        SectionKeyword::Name => Section::Name(value(input)?),
280        SectionKeyword::Base => Section::Base(value(input)?),
281        SectionKeyword::Version => Section::Version(value(input)?),
282        SectionKeyword::Desc => Section::Desc(value(input)?),
283        SectionKeyword::Groups => Section::Groups(values(input)?),
284        SectionKeyword::CSize => Section::CSize(value(input)?),
285        SectionKeyword::ISize => Section::ISize(value(input)?),
286        SectionKeyword::Md5Sum => Section::Md5Sum(value(input)?),
287        SectionKeyword::Sha256Sum => Section::Sha256Sum(value(input)?),
288        SectionKeyword::PgpSig => Section::PgpSig(value(input)?),
289        SectionKeyword::Url => Section::Url(opt_value(input)?),
290        SectionKeyword::License => Section::License(values(input)?),
291        SectionKeyword::Arch => Section::Arch(value(input)?),
292        SectionKeyword::BuildDate => Section::BuildDate(value(input)?),
293        SectionKeyword::Packager => Section::Packager(value(input)?),
294        SectionKeyword::Replaces => Section::Replaces(values(input)?),
295        SectionKeyword::Conflicts => Section::Conflicts(values(input)?),
296        SectionKeyword::Provides => Section::Provides(values(input)?),
297        SectionKeyword::Depends => Section::Depends(values(input)?),
298        SectionKeyword::OptDepends => Section::OptDepends(values(input)?),
299        SectionKeyword::MakeDepends => Section::MakeDepends(values(input)?),
300        SectionKeyword::CheckDepends => Section::CheckDepends(values(input)?),
301    };
302
303    Ok(section)
304}
305
306/// Parses all `%SECTION%` blocks from the given input into a list of [`Section`]s.
307///
308/// This is the top-level parser used by the higher-level file constructors.
309///
310/// # Errors
311///
312/// Returns an error if:
313///
314/// - any section header is missing or malformed,
315/// - a section value list fails to parse,
316/// - or the overall structure of the file is inconsistent.
317pub(crate) fn sections(input: &mut &str) -> ModalResult<Vec<Section>> {
318    cut_err(repeat_till(
319        0..,
320        preceded(opt(newline), section),
321        terminated(opt(newlines), eof),
322    ))
323    .context(StrContext::Label("sections"))
324    .context(StrContext::Expected(StrContextValue::Description(
325        "a section in the database desc file",
326    )))
327    .parse_next(input)
328    .map(|(sections, _)| sections)
329}