makedeb_srcinfo/
lib.rs

1//! This library provides a standardized way for clients to parse makedeb-styled `.SRCINFO` files.
2//! These are the files found on the [MPR](https://mpr.makedeb.org) that provide a method to know
3//! the contents of a PKGBUILD file without having to `source` (and thefore execute) it.
4//!
5//! Most clients won't need to use any of the `SRCINFO_*` constants, but instead should use the
6//! [`SrcInfo`] struct to read a `.SRCINFO` file.
7use regex::Regex;
8use std::collections::HashMap;
9
10// Python bindings.
11#[cfg(feature = "python")]
12mod python;
13
14/// A list of items that should always be strings (i.e. a maximum of one can be present) in a `.SRCINFO` file.
15pub const SRCINFO_STRINGS: [&str; 10] = [
16    "pkgbase", "pkgdesc", "pkgver", "pkgrel", "epoch", "url", "preinst", "postinst", "prerm",
17    "postrm",
18];
19
20/// A list of items that should always be arrays (i.e. any amount can be present) in a `.SRCINFO` file.
21pub const SRCINFO_ARRAYS: [&str; 19] = [
22    "pkgname",
23    "arch",
24    "license",
25    "depends",
26    "makedepends",
27    "checkdepends",
28    "optdepends",
29    "conflicts",
30    "provides",
31    "replaces",
32    "source",
33    "control_fields",
34    "md5sums",
35    "sha1sums",
36    "sha224sums",
37    "sha256sums",
38    "sha384sums",
39    "sha512sums",
40    "b2sums",
41];
42
43/// A list of items that can be extended (e.g. prefixed with `focal_` or suffixed with `_amd64`) in
44/// a `.SRCINFO` file.
45pub const SRCINFO_EXTENDED: [&str; 20] = [
46    // Strings
47    "preinst",
48    "postinst",
49    "prerm",
50    "postrm",
51    // Arrays
52    "depends",
53    "makedepends",
54    "checkdepends",
55    "optdepends",
56    "conflicts",
57    "provides",
58    "replaces",
59    "source",
60    "control_fields",
61    "md5sums",
62    "sha1sums",
63    "sha224sums",
64    "sha256sums",
65    "sha384sums",
66    "sha512sums",
67    "b2sums",
68];
69
70/// A list of items that must always be present inside of a `.SRCINFO` file.
71pub const SRCINFO_REQUIRED: [&str; 5] = ["pkgbase", "pkgname", "pkgver", "pkgrel", "arch"];
72
73/// A struct representing the output of a parsing error.
74#[derive(Debug)]
75pub struct ParserError {
76    /// A message describing the parsing error.
77    pub msg: String,
78    /// The line number the error occured on. This will always be the [`Some`] variant unless there
79    /// was an issue with the file as a whole, in which case the [`None`] variant will be returned.
80    pub line_num: Option<usize>,
81}
82
83type ParseMap = HashMap<String, Vec<String>>;
84
85#[derive(Debug)]
86pub struct SrcInfo {
87    map: ParseMap,
88}
89
90impl SrcInfo {
91    /// Parse the `.SRCINFO` file, returning a [`ParserError`] if there was an issue parsing the
92    /// file.
93    ///
94    /// `content` should be a string representing the content of the `.SRCINFO` file.
95    pub fn new(content: &str) -> Result<Self, ParserError> {
96        let mut map: ParseMap = HashMap::new();
97
98        for (_index, _line) in content.lines().enumerate() {
99            let mut line = _line.to_owned();
100
101            // We'll use the index for error reporting. Line numbers start at one in a file while
102            // indexes start at zero, so increment the index by one.
103            let index = _index + 1;
104
105            // Arch Linux .SRCINFO files sometimes contain comment lines while makedeb's do not, so
106            // we want to ignore those.
107            if line.starts_with('#') {
108                continue;
109            }
110
111            // Arch Linux .SRCINFO files also contain some blank lines which are lacking in
112            // makedeb's style, so ignore those too.
113            if line.is_empty() {
114                continue;
115            }
116
117            // Older .SRCINFO files contain tabs in some lines. We still want to parse those lines
118            // and the only problem is the tab, so just remove it.
119            line = line.replace('\t', "");
120
121            // Split the line between its key and value.
122            let _parts = line.split(" = ");
123
124            if _parts.clone().count() < 2 {
125                return Err(ParserError {
126                    msg: "No ' = ' delimiter found.".to_string(),
127                    line_num: Some(index),
128                });
129            }
130
131            let parts: Vec<&str> = _parts.collect();
132            let key = parts[0].to_string();
133            let value = parts[1..].join(" = ");
134
135            if let Some(values) = map.get_mut(&key) {
136                values.push(value);
137            } else {
138                map.insert(key, vec![value]);
139            }
140        }
141
142        // Make sure we have all required keys present.
143        for item in SRCINFO_REQUIRED {
144            if !map.contains_key(&item.to_owned()) {
145                return Err(ParserError {
146                    msg: format!("Required key '{}' not found.", item),
147                    line_num: None,
148                });
149            }
150        }
151
152        // Make sure any item that's supposed to be a string only has one item present.
153        // TODO: Also do this for any SRCINFO_STRINGS also in SRCINFO_EXTENDED.
154        for item in SRCINFO_STRINGS {
155            if let Some(values) = map.get(&item.to_owned()) {
156                if values.len() > 1 {
157                    return Err(ParserError {
158                        msg: format!(
159                            "Key '{}' is present more than once when it is not allowed to.",
160                            item
161                        ),
162                        line_num: None,
163                    });
164                }
165            }
166        }
167
168        Ok(Self { map })
169    }
170
171    /// Convert an extended string to it's base form.
172    /// This returns "" if the string isn't a valid key for a `.SRCINFO` file. While this could
173    /// return a [`None`] variant, this makes it easier to integrate in other places it's used
174    /// in this lib.
175    ///
176    /// This function is also not public (!) so we can have trash design decisions like this.
177    fn get_base_key(item: &str) -> &str {
178        let mut keys = SRCINFO_STRINGS.to_vec();
179        keys.append(&mut SRCINFO_ARRAYS.to_vec());
180
181        if keys.contains(&item) {
182            return item;
183        }
184
185        for key in keys {
186            let re_key = format!("^{0}_|_{0}_|_{0}$", key);
187            let re = Regex::new(&re_key).unwrap();
188
189            if re.is_match(item) {
190                return key;
191            }
192        }
193
194        ""
195    }
196
197    /// Get a value for anything that's a string variable in a PKGBUILD.
198    ///
199    /// **Note** that you'll need to use [`SrcInfo::get_array`] if you want to get the `pkgname` variable, since that has the
200    /// ability to be more than one item.
201    ///
202    /// This function also accepts extended variables (i.e. `focal_postrm`), though only variables that can be
203    /// extended by makedeb are supported.
204    ///
205    /// Returns the [`Some`] variant if the variable can be found, otherwise the [`None`] variant is returned.
206    pub fn get_string(&self, key: &str) -> Option<&String> {
207        if !SRCINFO_STRINGS.contains(&SrcInfo::get_base_key(key)) {
208            return None;
209        }
210
211        if let Some(values) = self.map.get(&key.to_owned()) {
212            Some(&values[0])
213        } else {
214            None
215        }
216    }
217
218    /// Get a value for anything that's an array variable in a PKGBUILD.
219    ///
220    /// This function also accepts extended variables (i.e. `focal_depends`), though only variables that can be
221    /// extended by makedeb are supported.
222    ///
223    /// Returns the [`Some`] variant if the variable can be found, otherwise the [`None`] variant is returned.
224    pub fn get_array(&self, key: &str) -> Option<&Vec<String>> {
225        if !SRCINFO_ARRAYS.contains(&SrcInfo::get_base_key(key)) {
226            return None;
227        }
228
229        self.map.get(&key.to_owned())
230    }
231
232    /// Get the extended names (as well as the key itself) for a variable. Use this if you need a variable as well as any
233    /// same variable that contains distribution and architecture extensions.
234    ///
235    /// If `key` isn't a key makedeb supports for variable name extensions, this will return the [`None`] variant, regardless
236    /// of if the base key is in the `.SRCINFO` file or not.
237    ///
238    /// This returns a vector of strings that can be then passed into [`SrcInfo.get_string`] and
239    /// [`SrcInfo.get_array`].
240    pub fn get_extended_values(&self, key: &str) -> Option<Vec<String>> {
241        if !SRCINFO_EXTENDED.contains(&key) {
242            return None;
243        }
244
245        let mut matches: Vec<String> = Vec::new();
246        let re = Regex::new(&format!(".*_{0}$|.*_{0}_.*|^{0}.*|^{0}$", key)).unwrap();
247
248        for item in self.map.keys() {
249            if re.is_match(item) {
250                matches.push(item.clone());
251            }
252        }
253
254        // If no items are in our vector, then no variants of the key were in the `.SRCINFO` file,
255        // and we want to let the client know no matches were found.
256        if matches.is_empty() {
257            None
258        } else {
259            Some(matches)
260        }
261    }
262}
263
264/// A Struct representing a package's name, operator, and version.
265pub struct SplitPackage {
266    pub pkgname: String,
267    pub operator: Option<String>,
268    pub version: Option<String>,
269}
270
271impl SplitPackage {
272    /// Split a dependency into its name, equality operator, and version.
273    /// Note that this function simply splits on the first operator ("<<", ">=", etc etc.) found - if you pass in more than one the returned 'version' field will contain the remaining operators. Versions are also not checked to see if they're valid, if you need such behavior please check inside of your application.
274    pub fn new(pkg_string: &str) -> Self {
275        let pkg = pkg_string.to_owned();
276
277        for operator in ["<=", ">=", "=", "<", ">"] {
278            if pkg.contains(operator) {
279                let (pkgname, version) = pkg.split_once(operator).unwrap();
280                return Self {
281                    pkgname: pkgname.to_owned(),
282                    operator: Some(operator.to_owned()),
283                    version: Some(version.to_owned()),
284                };
285            }
286        }
287
288        Self {
289            pkgname: pkg_string.to_owned(),
290            operator: None,
291            version: None,
292        }
293    }
294}
295
296/// A Struct representing a dependeny string (i.e. `pkg1|pkg2>=5`).
297/// Note that any prefix such as `p!` will be kept on the first package, and must be removed manually client-side.
298pub struct SplitDependency {
299    pub(crate) deps: Vec<SplitPackage>,
300}
301
302impl SplitDependency {
303    /// Create a new [`SplitDependency`] instance.
304    pub fn new(dep_string: &str) -> Self {
305        let mut deps = vec![];
306
307        for dep in dep_string.split('|') {
308            deps.push(SplitPackage::new(dep));
309        }
310
311        Self { deps }
312    }
313
314    pub(crate) fn internal_as_control(deps: &Vec<SplitPackage>) -> String {
315        let mut segments = vec![];
316
317        for dep in deps {
318            if dep.operator.is_some() && dep.version.is_some() {
319                let mut operator = dep.operator.as_ref().unwrap().clone();
320                let version = dep.version.as_ref().unwrap();
321
322                if ["<", ">"].contains(&operator.as_str()) {
323                    operator = operator.clone() + &operator;
324                }
325
326                segments.push(format!("{} ({} {})", dep.pkgname, operator, version));
327            } else {
328                segments.push(dep.pkgname.clone());
329            }
330        }
331
332        segments.join(" | ")
333    }
334
335    /// Print a Debian control-file styled representation of this dependency.
336    pub fn as_control(&self) -> String {
337        Self::internal_as_control(&self.deps)
338    }
339}