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}