alpm_db/desc/v2.rs
1//! Representation of the database desc file v2 ([alpm-db-descv2]).
2//!
3//! [alpm-db-descv2]: https://alpm.archlinux.page/specifications/alpm-db-descv2.5.html
4
5use std::{
6 fmt::{Display, Formatter, Result as FmtResult},
7 str::FromStr,
8};
9
10use alpm_types::{
11 Architecture,
12 BuildDate,
13 ExtraData,
14 ExtraDataEntry,
15 Group,
16 InstalledSize,
17 License,
18 Name,
19 OptionalDependency,
20 PackageBaseName,
21 PackageDescription,
22 PackageInstallReason,
23 PackageRelation,
24 PackageValidation,
25 Packager,
26 Url,
27 Version,
28};
29use serde_with::{TryFromInto, serde_as};
30use winnow::Parser;
31
32use crate::{
33 Error,
34 desc::{
35 DbDescFileV1,
36 Section,
37 parser::{SectionKeyword, sections},
38 },
39};
40
41/// DB desc version 2
42///
43/// `DbDescFileV2` extends [`DbDescFileV1`] according to the second revision of the
44/// [alpm-db-desc] specification. It introduces an additional `%XDATA%` section, which allows
45/// storing structured, implementation-defined metadata.
46///
47/// ## Examples
48///
49/// ```
50/// use std::str::FromStr;
51///
52/// use alpm_db::desc::DbDescFileV2;
53///
54/// # fn main() -> Result<(), alpm_db::Error> {
55/// let desc_data = r#"%NAME%
56/// foo
57///
58/// %VERSION%
59/// 1.0.0-1
60///
61/// %BASE%
62/// foo
63///
64/// %DESC%
65/// An example package
66///
67/// %URL%
68/// https://example.org/
69///
70/// %ARCH%
71/// x86_64
72///
73/// %BUILDDATE%
74/// 1733737242
75///
76/// %INSTALLDATE%
77/// 1733737243
78///
79/// %PACKAGER%
80/// Foobar McFooface <foobar@mcfooface.org>
81///
82/// %SIZE%
83/// 123
84///
85/// %GROUPS%
86/// utils
87/// cli
88///
89/// %REASON%
90/// 1
91///
92/// %LICENSE%
93/// MIT
94/// Apache-2.0
95///
96/// %VALIDATION%
97/// pgp
98///
99/// %REPLACES%
100/// pkg-old
101///
102/// %DEPENDS%
103/// glibc
104///
105/// %OPTDEPENDS%
106/// optpkg
107///
108/// %CONFLICTS%
109/// foo-old
110///
111/// %PROVIDES%
112/// foo-virtual
113///
114/// %XDATA%
115/// pkgtype=pkg
116///
117/// "#;
118///
119/// // Parse a DB DESC file in version 2 format.
120/// let db_desc = DbDescFileV2::from_str(desc_data)?;
121/// // Convert back to its canonical string representation.
122/// assert_eq!(db_desc.to_string(), desc_data);
123/// # Ok(())
124/// # }
125/// ```
126///
127/// [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
128#[serde_as]
129#[derive(Clone, Debug, serde::Deserialize, PartialEq, serde::Serialize)]
130#[serde(deny_unknown_fields)]
131#[serde(rename_all = "lowercase")]
132pub struct DbDescFileV2 {
133 /// The name of the package.
134 pub name: Name,
135
136 /// The version of the package.
137 pub version: Version,
138
139 /// The base name of the package (used in split packages).
140 pub base: PackageBaseName,
141
142 /// The description of the package.
143 pub description: PackageDescription,
144
145 /// The URL for the project of the package.
146 pub url: Option<Url>,
147
148 /// The architecture of the package.
149 pub arch: Architecture,
150
151 /// The date at which the build of the package started.
152 pub builddate: BuildDate,
153
154 /// The date at which the package has been installed on the system.
155 pub installdate: BuildDate,
156
157 /// The User ID of the entity, that built the package.
158 pub packager: Packager,
159
160 /// The optional size of the (uncompressed and unpacked) package contents in bytes.
161 pub size: InstalledSize,
162
163 /// Groups the package belongs to.
164 pub groups: Vec<Group>,
165
166 /// Optional install reason.
167 pub reason: PackageInstallReason,
168
169 /// Licenses that apply to the package.
170 pub license: Vec<License>,
171
172 /// Validation methods used for the package archive.
173 pub validation: PackageValidation,
174
175 /// Packages this one replaces.
176 pub replaces: Vec<PackageRelation>,
177
178 /// Required runtime dependencies.
179 pub depends: Vec<PackageRelation>,
180
181 /// Optional dependencies that enhance the package.
182 pub optdepends: Vec<OptionalDependency>,
183
184 /// Conflicting packages that cannot be installed together.
185 pub conflicts: Vec<PackageRelation>,
186
187 /// Virtual packages or capabilities provided by this one.
188 pub provides: Vec<PackageRelation>,
189
190 /// Structured extra metadata, implementation-defined.
191 #[serde_as(as = "TryFromInto<Vec<ExtraDataEntry>>")]
192 pub xdata: ExtraData,
193}
194
195impl Display for DbDescFileV2 {
196 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
197 // Reuse v1 formatting
198 let base: DbDescFileV1 = self.clone().into();
199 write!(f, "{base}")?;
200
201 // Write xdata section
202 writeln!(f, "%XDATA%")?;
203 for v in self.xdata.clone() {
204 writeln!(f, "{v}")?;
205 }
206
207 writeln!(f)
208 }
209}
210
211impl FromStr for DbDescFileV2 {
212 type Err = Error;
213
214 /// Creates a [`DbDescFileV2`] from a string slice.
215 ///
216 /// Parses the input according to the [alpm-db-descv2] specification (version 2) and constructs
217 /// a structured [`DbDescFileV2`] representation including the `%XDATA%` section.
218 ///
219 /// # Examples
220 ///
221 /// ```
222 /// use std::str::FromStr;
223 ///
224 /// use alpm_db::desc::DbDescFileV2;
225 ///
226 /// # fn main() -> Result<(), alpm_db::Error> {
227 /// let desc_data = r#"%NAME%
228 /// foo
229 ///
230 /// %VERSION%
231 /// 1.0.0-1
232 ///
233 /// %BASE%
234 /// foo
235 ///
236 /// %DESC%
237 /// An example package
238 ///
239 /// %URL%
240 /// https://example.org
241 ///
242 /// %ARCH%
243 /// x86_64
244 ///
245 /// %BUILDDATE%
246 /// 1733737242
247 ///
248 /// %INSTALLDATE%
249 /// 1733737243
250 ///
251 /// %PACKAGER%
252 /// Foobar McFooface <foobar@mcfooface.org>
253 ///
254 /// %SIZE%
255 /// 123
256 ///
257 /// %VALIDATION%
258 /// pgp
259 ///
260 /// %XDATA%
261 /// pkgtype=pkg
262 ///
263 /// "#;
264 ///
265 /// let db_desc = DbDescFileV2::from_str(desc_data)?;
266 /// assert_eq!(db_desc.name.to_string(), "foo");
267 /// # Ok(())
268 /// # }
269 /// ```
270 ///
271 /// # Errors
272 ///
273 /// Returns an error if:
274 ///
275 /// - the input cannot be parsed into valid sections,
276 /// - or required fields are missing or malformed.
277 ///
278 /// [alpm-db-descv2]: https://alpm.archlinux.page/specifications/alpm-db-descv2.5.html
279 fn from_str(s: &str) -> Result<Self, Self::Err> {
280 let sections = sections.parse(s)?;
281 Self::try_from(sections)
282 }
283}
284
285impl TryFrom<Vec<Section>> for DbDescFileV2 {
286 type Error = Error;
287
288 /// Tries to create a [`DbDescFileV2`] from a list of parsed [`Section`]s.
289 ///
290 /// Reuses the parsing logic from [`DbDescFileV1`] for all common fields, and adds support for
291 /// the `%XDATA%` section introduced in the [alpm-db-descv2] specification.
292 ///
293 /// # Errors
294 ///
295 /// Returns an error if:
296 ///
297 /// - any required field is missing,
298 /// - a section appears more than once,
299 /// - or the `%XDATA%` section is missing or malformed.
300 ///
301 /// [alpm-db-descv2]: https://alpm.archlinux.page/specifications/alpm-db-descv2.5.html
302 fn try_from(sections: Vec<Section>) -> Result<Self, Self::Error> {
303 // Reuse v1 fields
304 let v1 = DbDescFileV1::try_from(sections.clone())?;
305
306 // Find xdata section
307 let xdata = if let Some(Section::XData(v)) =
308 sections.iter().find(|s| matches!(s, Section::XData(_)))
309 {
310 v.clone()
311 } else {
312 return Err(Error::MissingSection(SectionKeyword::XData));
313 };
314
315 Ok(Self {
316 name: v1.name,
317 version: v1.version,
318 base: v1.base,
319 description: v1.description,
320 url: v1.url,
321 arch: v1.arch,
322 builddate: v1.builddate,
323 installdate: v1.installdate,
324 packager: v1.packager,
325 size: v1.size,
326 groups: v1.groups,
327 reason: v1.reason,
328 license: v1.license,
329 validation: v1.validation,
330 replaces: v1.replaces,
331 depends: v1.depends,
332 optdepends: v1.optdepends,
333 conflicts: v1.conflicts,
334 provides: v1.provides,
335 xdata,
336 })
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use pretty_assertions::assert_eq;
343 use rstest::*;
344 use testresult::TestResult;
345
346 use super::*;
347
348 const VALID_DESC_FILE: &str = r#"%NAME%
349foo
350
351%VERSION%
3521.0.0-1
353
354%BASE%
355foo
356
357%DESC%
358An example package
359
360%URL%
361https://example.org/
362
363%ARCH%
364x86_64
365
366%BUILDDATE%
3671733737242
368
369%INSTALLDATE%
3701733737243
371
372%PACKAGER%
373Foobar McFooface <foobar@mcfooface.org>
374
375%SIZE%
376123
377
378%GROUPS%
379utils
380cli
381
382%REASON%
3831
384
385%LICENSE%
386MIT
387Apache-2.0
388
389%VALIDATION%
390pgp
391
392%REPLACES%
393pkg-old
394
395%DEPENDS%
396glibc
397
398%OPTDEPENDS%
399optpkg
400
401%CONFLICTS%
402foo-old
403
404%PROVIDES%
405foo-virtual
406
407%XDATA%
408pkgtype=pkg
409
410"#;
411
412 #[test]
413 fn parse_valid_v2_desc() -> TestResult {
414 let actual = DbDescFileV2::from_str(VALID_DESC_FILE)?;
415 let expected = DbDescFileV2 {
416 name: Name::new("foo")?,
417 version: Version::from_str("1.0.0-1")?,
418 base: PackageBaseName::new("foo")?,
419 description: PackageDescription::from("An example package"),
420 url: Some(Url::from_str("https://example.org")?),
421 arch: Architecture::from_str("x86_64")?,
422 builddate: BuildDate::from(1733737242),
423 installdate: BuildDate::from(1733737243),
424 packager: Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
425 size: 123,
426 groups: vec!["utils".into(), "cli".into()],
427 reason: PackageInstallReason::Depend,
428 license: vec![License::from_str("MIT")?, License::from_str("Apache-2.0")?],
429 validation: PackageValidation::from_str("pgp")?,
430 replaces: vec![PackageRelation::from_str("pkg-old")?],
431 depends: vec![PackageRelation::from_str("glibc")?],
432 optdepends: vec![OptionalDependency::from_str("optpkg")?],
433 conflicts: vec![PackageRelation::from_str("foo-old")?],
434 provides: vec![PackageRelation::from_str("foo-virtual")?],
435 xdata: ExtraDataEntry::from_str("pkgtype=pkg")?.try_into()?,
436 };
437 assert_eq!(actual, expected);
438 assert_eq!(VALID_DESC_FILE, actual.to_string());
439 Ok(())
440 }
441
442 #[rstest]
443 #[case("%UNKNOWN%\nvalue", "invalid section name")]
444 #[case("%VERSION%\n1.0.0-1\n", "Missing section: %NAME%")]
445 fn invalid_desc_parser(#[case] input: &str, #[case] error_snippet: &str) {
446 let result = DbDescFileV2::from_str(input);
447 assert!(result.is_err());
448 let err = result.unwrap_err();
449 let pretty_error = err.to_string();
450 assert!(
451 pretty_error.contains(error_snippet),
452 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
453 );
454 }
455}