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_types::Architecture;
13use serde::{Deserialize, Serialize};
14use winnow::Parser;
15
16pub mod merged;
17pub mod package;
18pub mod package_base;
19
20#[cfg(doc)]
21use crate::MergedPackage;
22use crate::{
23 error::{Error, SourceInfoError, SourceInfoErrors},
24 source_info::{
25 parser::SourceInfoContent,
26 v1::{merged::MergedPackagesIterator, package::Package, package_base::PackageBase},
27 },
28};
29
30/// The representation of SRCINFO data.
31///
32/// Provides access to a [`PackageBase`] which tracks all data in a `pkgbase` section and a list of
33/// [`Package`] instances that provide the accumulated data of all `pkgname` sections.
34///
35/// This is the entry point for parsing SRCINFO files. Once created,
36/// [`Self::packages_for_architecture`] can be used to create usable [`MergedPackage`]s.
37#[derive(Clone, Debug, Deserialize, Serialize)]
38pub struct SourceInfoV1 {
39 pub base: PackageBase,
40 pub packages: Vec<Package>,
41}
42
43impl SourceInfoV1 {
44 /// Reads the file at the specified path and converts it into a [`SourceInfoV1`] struct.
45 ///
46 /// # Errors
47 ///
48 /// Returns an [`Error`] if the file cannot be read or parsed.
49 /// Returns an error array with potentially un/-recoverable errors, this needs to be explicitly
50 /// handled by the user.
51 pub fn from_file(path: &Path) -> Result<SourceInfoResult, Error> {
52 let mut buffer = Vec::new();
53 let file = File::open(path)
54 .map_err(|err| Error::IoPath(path.to_path_buf(), "opening file", err))?;
55 let mut buf_reader = BufReader::new(file);
56 buf_reader
57 .read_to_end(&mut buffer)
58 .map_err(|err| Error::IoPath(path.to_path_buf(), "reading file", err))?;
59
60 let content = String::from_utf8(buffer)?.to_string();
61
62 Self::from_string(&content)
63 }
64
65 /// Parses a SRCINFO file's content into a [`SourceInfoV1`] struct.
66 ///
67 /// # Error
68 ///
69 /// This function returns two types of errors.
70 /// 1. An [`Error`] is returned if the input is, for example, invalid UTF-8 or if the input
71 /// SRCINFO file couldn't be parsed due to invalid syntax.
72 /// 2. If the parsing was successful, a [`SourceInfoResult`] is returned, which wraps a possibly
73 /// invalid [`SourceInfoV1`] and possible [`SourceInfoErrors`]. [`SourceInfoErrors`] contains
74 /// all errors and lint/deprecation warnings that're encountered while interpreting the
75 /// SRCINFO file.
76 ///
77 /// ```rust
78 /// use alpm_srcinfo::SourceInfoV1;
79 /// use alpm_types::{Architecture, Name, PackageRelation};
80 ///
81 /// # fn main() -> Result<(), alpm_srcinfo::Error> {
82 /// let source_info_data = r#"
83 /// pkgbase = example
84 /// pkgver = 1.0.0
85 /// epoch = 1
86 /// pkgrel = 1
87 /// pkgdesc = A project that does something
88 /// url = https://example.org/
89 /// arch = x86_64
90 /// depends = glibc
91 /// optdepends = python: for special-python-script.py
92 /// makedepends = cmake
93 /// checkdepends = extra-test-tool
94 ///
95 /// pkgname = example
96 /// depends = glibc
97 /// depends = gcc-libs
98 /// "#;
99 ///
100 /// // Parse the file. This might already error if the file cannot be parsed on a low level.
101 /// let source_info_result = SourceInfoV1::from_string(source_info_data)?;
102 /// // Make sure there're no other unrecoverable errors.
103 /// let source_info = source_info_result.source_info()?;
104 /// # Ok(())
105 /// # }
106 /// ```
107 pub fn from_string(content: &str) -> Result<SourceInfoResult, Error> {
108 // Parse the given srcinfo content.
109 let parsed = SourceInfoContent::parser
110 .parse(content)
111 .map_err(|err| Error::ParseError(format!("{err}")))?;
112
113 // Bring it into a proper structural representation and run linting checks.
114 let (source_info, errors) = SourceInfoV1::from_raw(parsed);
115
116 // If there're some errors, create a SourceInfoErrors to also capture the file content for
117 // context.
118 let errors = if !errors.is_empty() {
119 Some(SourceInfoErrors::new(errors, content.to_string()))
120 } else {
121 None
122 };
123
124 Ok(SourceInfoResult {
125 source_info,
126 errors,
127 })
128 }
129
130 /// Reads raw [`SourceInfoContent`] from a first parsing step and converts it into a
131 /// [`SourceInfoV1`].
132 ///
133 /// Instead of a [`Result`] this function returns a tuple of [`SourceInfoV1`] and a vector of
134 /// [`SourceInfoError`]s. The caller is expected to handle the vector of [`SourceInfoError`]s,
135 /// which may only consist of linting errors that can be ignored.
136 pub fn from_raw(content: SourceInfoContent) -> (SourceInfoV1, Vec<SourceInfoError>) {
137 // Set the line cursor for error messages to the last line before we start with the
138 // `pkgbase`
139 let mut current_line = content.preceding_lines.len();
140 let mut errors = Vec::new();
141
142 // Save the number of lines in the `pkgbase` section.
143 let package_base_length = content.package_base.properties.len();
144
145 // Account for the `pkgbase` section header, which is handled separately.
146 current_line += 1;
147 let base = PackageBase::from_parsed(current_line, content.package_base, &mut errors);
148 // Add the length of lines of the pkgbuild section.
149 current_line += package_base_length;
150
151 let mut packages = Vec::new();
152 for package in content.packages {
153 // Save the number of lines in the `pkgname` section.
154 let package_length = package.properties.len();
155
156 // Account for the `pkgname` section header, which is handled separately.
157 current_line += 1;
158 let package =
159 Package::from_parsed(current_line, &base.architectures, package, &mut errors);
160 // Add the number of lines of the pkgname section.
161 current_line += package_length;
162
163 packages.push(package);
164 }
165
166 (SourceInfoV1 { base, packages }, errors)
167 }
168
169 /// Get an iterator over all packages
170 ///
171 /// ```
172 /// use alpm_srcinfo::{MergedPackage, SourceInfoV1};
173 /// use alpm_types::{Architecture, Name, PackageRelation};
174 ///
175 /// # fn main() -> Result<(), alpm_srcinfo::Error> {
176 /// let source_info_data = r#"
177 /// pkgbase = example
178 /// pkgver = 1.0.0
179 /// epoch = 1
180 /// pkgrel = 1
181 /// arch = x86_64
182 ///
183 /// pkgname = example
184 /// pkgdesc = Example split package
185 ///
186 /// pkgname = example_other
187 /// pkgdesc = The other example split package
188 /// "#;
189 /// // Parse the file. This might already error if the file cannot be parsed on a low level.
190 /// let result = SourceInfoV1::from_string(source_info_data)?;
191 /// // Make sure there're aren't unrecoverable logic errors, such as missing values.
192 /// let source_info = result.source_info()?;
193 ///
194 /// /// Get all merged package representations for the x86_64 architecture.
195 /// let mut packages = source_info.packages_for_architecture(Architecture::X86_64);
196 ///
197 /// let example = packages.next().unwrap();
198 /// assert_eq!(
199 /// example.description,
200 /// Some("Example split package".to_string())
201 /// );
202 ///
203 /// let example_other = packages.next().unwrap();
204 /// assert_eq!(
205 /// example_other.description,
206 /// Some("The other example split package".to_string())
207 /// );
208 ///
209 /// # Ok(())
210 /// # }
211 /// ```
212 pub fn packages_for_architecture(
213 &self,
214 architecture: Architecture,
215 ) -> MergedPackagesIterator<'_> {
216 MergedPackagesIterator {
217 architecture,
218 source_info: self,
219 package_iterator: self.packages.iter(),
220 }
221 }
222}
223
224/// Wraps the outcome of [`SourceInfoV1::from_string`].
225///
226/// While building a [`SourceInfoV1`] from raw [`SourceInfoContent`], errors as well as deprecation
227/// and linter warnings may be encountered.
228///
229/// In case no errors are encountered, the resulting [`SourceInfoV1`] may be accessed via
230/// [`SourceInfoResult::source_info`].
231#[derive(Debug, Clone)]
232pub struct SourceInfoResult {
233 source_info: SourceInfoV1,
234 errors: Option<SourceInfoErrors>,
235}
236
237impl SourceInfoResult {
238 /// Returns the [`SourceInfoV1`] as long as no critical errors have been encountered.
239 ///
240 /// # Errors
241 ///
242 /// Returns an error if any kind of unrecoverable logic error is encountered, such as missing
243 /// properties
244 pub fn source_info(self) -> Result<SourceInfoV1, Error> {
245 if let Some(errors) = self.errors {
246 errors.check_unrecoverable_errors()?;
247 }
248
249 Ok(self.source_info)
250 }
251
252 /// Returns the generated [`SourceInfoV1`] regardless of whether there're any errors or not.
253 ///
254 /// # Warning
255 ///
256 /// This SourceInfoV1 struct may be incomplete, could contain invalid information and/or invalid
257 /// default values!
258 ///
259 /// Only use this if you know what you're doing and if you want to do stuff like manual
260 /// auto-correction.
261 pub fn incomplete_source_info(&self) -> &SourceInfoV1 {
262 &self.source_info
263 }
264
265 /// Returns a the [`SourceInfoV1`] as long as there're no errors, lints or warnings of any kind.
266 ///
267 /// # Errors
268 ///
269 /// Any kind of error, warning or lint is encountered.
270 pub fn lint(self) -> Result<SourceInfoV1, Error> {
271 if let Some(errors) = self.errors {
272 if !errors.errors().is_empty() {
273 return Err(Error::SourceInfoErrors(errors));
274 }
275 }
276
277 Ok(self.source_info)
278 }
279
280 /// Gets a reference to the errors of this result.
281 pub fn errors(&self) -> Option<&SourceInfoErrors> {
282 self.errors.as_ref()
283 }
284}