Skip to main content

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_parsers::prelude::*;
13use alpm_pkgbuild::bridge::BridgeOutput;
14use alpm_types::Architecture;
15use fluent_i18n::t;
16use serde::{Deserialize, Serialize};
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.parse(Input::new(content_no_tabs.as_str()))?;
178
179        // Bring it into a proper structural representation
180        let source_info = SourceInfoV1::from_raw(parsed)?;
181
182        Ok(source_info)
183    }
184
185    /// Reads raw [`SourceInfoContent`] from a first parsing step and converts it into a
186    /// [`SourceInfoV1`].
187    pub fn from_raw(content: SourceInfoContent) -> Result<SourceInfoV1, Error> {
188        let base = PackageBase::from_parsed(content.package_base)?;
189
190        let mut packages = Vec::new();
191        for package in content.packages {
192            let package = Package::from_parsed(package)?;
193            packages.push(package);
194        }
195
196        Ok(SourceInfoV1 { base, packages })
197    }
198
199    /// Get an iterator over all packages
200    ///
201    /// ```
202    /// use alpm_srcinfo::{MergedPackage, SourceInfoV1};
203    /// use alpm_types::{Name, PackageDescription, PackageRelation, SystemArchitecture};
204    ///
205    /// # fn main() -> Result<(), alpm_srcinfo::Error> {
206    /// let source_info_data = r#"
207    /// pkgbase = example
208    ///     pkgver = 1.0.0
209    ///     epoch = 1
210    ///     pkgrel = 1
211    ///     arch = x86_64
212    ///
213    /// pkgname = example
214    ///     pkgdesc = Example split package
215    ///
216    /// pkgname = example_other
217    ///     pkgdesc = The other example split package
218    /// "#;
219    /// // Parse the file. This errors if the file cannot be parsed, is missing data or contains invalid data.
220    /// let source_info = SourceInfoV1::from_string(source_info_data)?;
221    ///
222    /// /// Get all merged package representations for the x86_64 architecture.
223    /// let mut packages = source_info.packages_for_architecture(SystemArchitecture::X86_64);
224    ///
225    /// let Some(example) = packages.next() else {
226    ///     panic!("There is no next package");
227    /// };
228    /// assert_eq!(
229    ///     example.description,
230    ///     Some(PackageDescription::new("Example split package"))
231    /// );
232    ///
233    /// let Some(example) = packages.next() else {
234    ///     panic!("There is no next package");
235    /// };
236    /// assert_eq!(
237    ///     example.description,
238    ///     Some(PackageDescription::new("The other example split package"))
239    /// );
240    ///
241    /// # Ok(())
242    /// # }
243    /// ```
244    pub fn packages_for_architecture<A: Into<Architecture>>(
245        &self,
246        architecture: A,
247    ) -> MergedPackagesIterator<'_> {
248        MergedPackagesIterator {
249            architecture: architecture.into(),
250            source_info: self,
251            package_iterator: self.packages.iter(),
252        }
253    }
254}