alpm_srcinfo/source_info/v1/
mod.rs

1//! Contains the second parsing and linting pass.
2//!
3//! The raw representation from the [`parser`](crate::source_info::parser) module is brought into a
4//! proper struct-based representation that fully represents the SRCINFO data (apart from comments
5//! and empty lines).
6use std::{
7    fs::File,
8    io::{BufReader, Read},
9    path::Path,
10};
11
12use alpm_pkgbuild::bridge::BridgeOutput;
13use alpm_types::Architecture;
14use fluent_i18n::t;
15use serde::{Deserialize, Serialize};
16use winnow::Parser;
17use writer::{pkgbase_section, pkgname_section};
18
19pub mod merged;
20pub mod package;
21pub mod package_base;
22pub mod writer;
23
24#[cfg(doc)]
25use crate::MergedPackage;
26use crate::{
27    error::Error,
28    source_info::{
29        parser::SourceInfoContent,
30        v1::{merged::MergedPackagesIterator, package::Package, package_base::PackageBase},
31    },
32};
33
34/// The representation of SRCINFO data.
35///
36/// Provides access to a [`PackageBase`] which tracks all data in a `pkgbase` section and a list of
37/// [`Package`] instances that provide the accumulated data of all `pkgname` sections.
38///
39/// This is the entry point for parsing SRCINFO files. Once created,
40/// [`Self::packages_for_architecture`] can be used to create usable [`MergedPackage`]s.
41#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
42pub struct SourceInfoV1 {
43    /// The information of the `pkgbase` section.
44    pub base: PackageBase,
45    /// The information of the `pkgname` sections.
46    pub packages: Vec<Package>,
47}
48
49impl SourceInfoV1 {
50    /// Returns the [SRCINFO] representation.
51    ///
52    /// ```
53    /// use std::{env::var, path::PathBuf};
54    ///
55    /// use alpm_srcinfo::SourceInfoV1;
56    ///
57    /// const TEST_FILE: &str = include_str!("../../../tests/unit_test_files/normal.srcinfo");
58    ///
59    /// # fn main() -> testresult::TestResult {
60    /// // Read a .SRCINFO file and bring it into the `SourceInfoV1` representation.
61    /// let source_info = SourceInfoV1::from_string(TEST_FILE)?;
62    /// // Convert the `SourceInfoV1` back into it's alpm `.SRCINFO` format.
63    /// println!("{}", source_info.as_srcinfo());
64    /// # Ok(())
65    /// # }
66    /// ```
67    ///
68    /// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
69    pub fn as_srcinfo(&self) -> String {
70        let mut srcinfo = String::new();
71
72        pkgbase_section(&self.base, &mut srcinfo);
73        for package in &self.packages {
74            srcinfo.push('\n');
75            pkgname_section(package, &self.base.architectures, &mut srcinfo);
76        }
77
78        srcinfo
79    }
80
81    /// Reads the file at the specified path and converts it into a [`SourceInfoV1`] struct.
82    ///
83    /// # Errors
84    ///
85    /// Returns an [`Error`] if the file cannot be read or parsed.
86    pub fn from_file(path: &Path) -> Result<SourceInfoV1, Error> {
87        let mut buffer = Vec::new();
88        let file = File::open(path).map_err(|source| Error::IoPath {
89            path: path.to_path_buf(),
90            context: t!("error-io-path-opening-file"),
91            source,
92        })?;
93        let mut buf_reader = BufReader::new(file);
94        buf_reader
95            .read_to_end(&mut buffer)
96            .map_err(|source| Error::IoPath {
97                path: path.to_path_buf(),
98                context: t!("error-io-path-reading-file"),
99                source,
100            })?;
101
102        let content = String::from_utf8(buffer)?.to_string();
103
104        Self::from_string(&content)
105    }
106
107    /// Creates a [`SourceInfoV1`] from a [`PKGBUILD`] file.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if
112    ///
113    /// - The `PKGBUILD` cannot be read.
114    /// - a required field is not set,
115    /// - a `package` functions exists, but does not correspond to a declared [alpm-split-package],
116    /// - a `package` function without an [alpm-package-name] suffix exists in an
117    ///   [alpm-split-package] setup,
118    /// - a value cannot be turned into its [`alpm_types`] equivalent,
119    /// - multiple values exist for a field that only accepts a singular value,
120    /// - an [alpm-architecture] is duplicated,
121    /// - an [alpm-architecture] is cleared in `package` function,
122    /// - or an [alpm-architecture] suffix is set on a keyword that does not support it.
123    ///
124    /// [`PKGBUILD`]: https://man.archlinux.org/man/PKGBUILD.5
125    /// [alpm-architecture]: https://alpm.archlinux.page/specifications/alpm-architecture.7.html
126    /// [alpm-package-name]: https://alpm.archlinux.page/specifications/alpm-package-name.7.html
127    /// [alpm-split-package]: https://alpm.archlinux.page/specifications/alpm-split-package.7.html
128    pub fn from_pkgbuild(pkgbuild_path: &Path) -> Result<SourceInfoV1, Error> {
129        let output = BridgeOutput::from_file(pkgbuild_path)?;
130        let source_info: SourceInfoV1 = output.try_into()?;
131
132        Ok(source_info)
133    }
134
135    /// Parses a SRCINFO file's content into a [`SourceInfoV1`] struct.
136    ///
137    /// # Error
138    ///
139    /// This function returns two types of errors.
140    /// 1. An [`Error`] is returned if the input is, for example, invalid UTF-8 or if the input
141    ///    SRCINFO file couldn't be parsed due to invalid syntax.
142    /// 2. An [`Error`] is returned if the parsed data is incomplete or otherwise invalid.
143    ///
144    /// ```rust
145    /// use alpm_srcinfo::SourceInfoV1;
146    /// use alpm_types::{Architecture, Name, PackageRelation};
147    ///
148    /// # fn main() -> Result<(), alpm_srcinfo::Error> {
149    /// let source_info_data = r#"
150    /// pkgbase = example
151    ///     pkgver = 1.0.0
152    ///     epoch = 1
153    ///     pkgrel = 1
154    ///     pkgdesc = A project that does something
155    ///     url = https://example.org/
156    ///     arch = x86_64
157    ///     depends = glibc
158    ///     optdepends = python: for special-python-script.py
159    ///     makedepends = cmake
160    ///     checkdepends = extra-test-tool
161    ///
162    /// pkgname = example
163    ///     depends = glibc
164    ///     depends = gcc-libs
165    /// "#;
166    ///
167    /// // Parse the file. This errors if the file cannot be parsed, is missing data or contains invalid data.
168    /// let source_info = SourceInfoV1::from_string(source_info_data)?;
169    /// # Ok(())
170    /// # }
171    /// ```
172    pub fn from_string(content: &str) -> Result<SourceInfoV1, Error> {
173        // A temporary fix for <https://github.com/winnow-rs/winnow/issues/847>
174        let content_no_tabs = content.replace('\t', " ");
175
176        // Parse the given srcinfo content.
177        let parsed = SourceInfoContent::parser
178            .parse(content_no_tabs.as_str())
179            .map_err(|err| Error::ParseError(format!("{err}")))?;
180
181        // Bring it into a proper structural representation
182        let source_info = SourceInfoV1::from_raw(parsed)?;
183
184        Ok(source_info)
185    }
186
187    /// Reads raw [`SourceInfoContent`] from a first parsing step and converts it into a
188    /// [`SourceInfoV1`].
189    pub fn from_raw(content: SourceInfoContent) -> Result<SourceInfoV1, Error> {
190        let base = PackageBase::from_parsed(content.package_base)?;
191
192        let mut packages = Vec::new();
193        for package in content.packages {
194            let package = Package::from_parsed(package)?;
195            packages.push(package);
196        }
197
198        Ok(SourceInfoV1 { base, packages })
199    }
200
201    /// Get an iterator over all packages
202    ///
203    /// ```
204    /// use alpm_srcinfo::{MergedPackage, SourceInfoV1};
205    /// use alpm_types::{Name, PackageDescription, PackageRelation, SystemArchitecture};
206    ///
207    /// # fn main() -> Result<(), alpm_srcinfo::Error> {
208    /// let source_info_data = r#"
209    /// pkgbase = example
210    ///     pkgver = 1.0.0
211    ///     epoch = 1
212    ///     pkgrel = 1
213    ///     arch = x86_64
214    ///
215    /// pkgname = example
216    ///     pkgdesc = Example split package
217    ///
218    /// pkgname = example_other
219    ///     pkgdesc = The other example split package
220    /// "#;
221    /// // Parse the file. This errors if the file cannot be parsed, is missing data or contains invalid data.
222    /// let source_info = SourceInfoV1::from_string(source_info_data)?;
223    ///
224    /// /// Get all merged package representations for the x86_64 architecture.
225    /// let mut packages = source_info.packages_for_architecture(SystemArchitecture::X86_64);
226    ///
227    /// let example = packages.next().unwrap();
228    /// assert_eq!(
229    ///     example.description,
230    ///     Some(PackageDescription::new("Example split package"))
231    /// );
232    ///
233    /// let example_other = packages.next().unwrap();
234    /// assert_eq!(
235    ///     example_other.description,
236    ///     Some(PackageDescription::new("The other example split package"))
237    /// );
238    ///
239    /// # Ok(())
240    /// # }
241    /// ```
242    pub fn packages_for_architecture<A: Into<Architecture>>(
243        &self,
244        architecture: A,
245    ) -> MergedPackagesIterator<'_> {
246        MergedPackagesIterator {
247            architecture: architecture.into(),
248            source_info: self,
249            package_iterator: self.packages.iter(),
250        }
251    }
252}