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