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}