1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
//! This library provides a standardized way for clients to parse makedeb-styled `.SRCINFO` files.
//! These are the files found on the [MPR](https://mpr.makedeb.org) that provide a method to know
//! the contents of a PKGBUILD file without having to `source` (and thefore execute) it.
//!
//! Most clients won't need to use any of the `SRCINFO_*` constants, but instead should use the
//! [`SrcInfo`] struct to read a `.SRCINFO` file.
use regex::Regex;
use std::collections::HashMap;

// Python bindings.
#[cfg(feature = "python")]
mod python;

/// A list of items that should always be strings (i.e. a maximum of one can be present) in a `.SRCINFO` file.
pub const SRCINFO_STRINGS: [&str; 10] = [
    "pkgbase", "pkgdesc", "pkgver", "pkgrel", "epoch", "url", "preinst", "postinst", "prerm",
    "postrm",
];

/// A list of items that should always be arrays (i.e. any amount can be present) in a `.SRCINFO` file.
pub const SRCINFO_ARRAYS: [&str; 19] = [
    "pkgname",
    "arch",
    "license",
    "depends",
    "makedepends",
    "checkdepends",
    "optdepends",
    "conflicts",
    "provides",
    "replaces",
    "source",
    "control_fields",
    "md5sums",
    "sha1sums",
    "sha224sums",
    "sha256sums",
    "sha384sums",
    "sha512sums",
    "b2sums",
];

/// A list of items that can be extended (e.g. prefixed with `focal_` or suffixed with `_amd64`) in
/// a `.SRCINFO` file.
pub const SRCINFO_EXTENDED: [&str; 20] = [
    // Strings
    "preinst",
    "postinst",
    "prerm",
    "postrm",
    // Arrays
    "depends",
    "makedepends",
    "checkdepends",
    "optdepends",
    "conflicts",
    "provides",
    "replaces",
    "source",
    "control_fields",
    "md5sums",
    "sha1sums",
    "sha224sums",
    "sha256sums",
    "sha384sums",
    "sha512sums",
    "b2sums",
];

/// A list of items that must always be present inside of a `.SRCINFO` file.
pub const SRCINFO_REQUIRED: [&str; 5] = ["pkgbase", "pkgname", "pkgver", "pkgrel", "arch"];

/// A struct representing the output of a parsing error.
#[derive(Debug)]
pub struct ParserError {
    /// A message describing the parsing error.
    pub msg: String,
    /// The line number the error occured on. This will always be the [`Some`] variant unless there
    /// was an issue with the file as a whole, in which case the [`None`] variant will be returned.
    pub line_num: Option<usize>,
}

type ParseMap = HashMap<String, Vec<String>>;

#[derive(Debug)]
pub struct SrcInfo {
    map: ParseMap,
}

impl SrcInfo {
    /// Parse the `.SRCINFO` file, returning a [`ParserError`] if there was an issue parsing the
    /// file.
    ///
    /// `content` should be a string representing the content of the `.SRCINFO` file.
    pub fn new(content: &str) -> Result<Self, ParserError> {
        let mut map: ParseMap = HashMap::new();

        for (_index, _line) in content.lines().enumerate() {
            let mut line = _line.to_owned();

            // We'll use the index for error reporting. Line numbers start at one in a file while
            // indexes start at zero, so increment the index by one.
            let index = _index + 1;

            // Arch Linux .SRCINFO files sometimes contain comment lines while makedeb's do not, so
            // we want to ignore those.
            if line.starts_with('#') {
                continue;
            }

            // Arch Linux .SRCINFO files also contain some blank lines which are lacking in
            // makedeb's style, so ignore those too.
            if line.is_empty() {
                continue;
            }

            // Older .SRCINFO files contain tabs in some lines. We still want to parse those lines
            // and the only problem is the tab, so just remove it.
            line = line.replace('\t', "");

            // Split the line between its key and value.
            let _parts = line.split(" = ");

            if _parts.clone().count() < 2 {
                return Err(ParserError {
                    msg: "No ' = ' delimiter found.".to_string(),
                    line_num: Some(index),
                });
            }

            let parts: Vec<&str> = _parts.collect();
            let key = parts[0].to_string();
            let value = parts[1..].join(" = ");

            if let Some(values) = map.get_mut(&key) {
                values.push(value);
            } else {
                map.insert(key, vec![value]);
            }
        }

        // Make sure we have all required keys present.
        for item in SRCINFO_REQUIRED {
            if !map.contains_key(&item.to_owned()) {
                return Err(ParserError {
                    msg: format!("Required key '{}' not found.", item),
                    line_num: None,
                });
            }
        }

        // Make sure any item that's supposed to be a string only has one item present.
        // TODO: Also do this for any SRCINFO_STRINGS also in SRCINFO_EXTENDED.
        for item in SRCINFO_STRINGS {
            if let Some(values) = map.get(&item.to_owned()) {
                if values.len() > 1 {
                    return Err(ParserError {
                        msg: format!(
                            "Key '{}' is present more than once when it is not allowed to.",
                            item
                        ),
                        line_num: None,
                    });
                }
            }
        }

        Ok(Self { map })
    }

    /// Convert an extended string to it's base form.
    /// This returns "" if the string isn't a valid key for a `.SRCINFO` file. While this could
    /// return a [`None`] variant, this makes it easier to integrate in other places it's used
    /// in this lib.
    ///
    /// This function is also not public (!) so we can have trash design decisions like this.
    fn get_base_key(item: &str) -> &str {
        let mut keys = SRCINFO_STRINGS.to_vec();
        keys.append(&mut SRCINFO_ARRAYS.to_vec());

        if keys.contains(&item) {
            return item;
        }

        for key in keys {
            let re_key = format!("^{0}_|_{0}_|_{0}$", key);
            let re = Regex::new(&re_key).unwrap();

            if re.is_match(item) {
                return key;
            }
        }

        ""
    }

    /// Get a value for anything that's a string variable in a PKGBUILD.
    ///
    /// **Note** that you'll need to use [`SrcInfo::get_array`] if you want to get the `pkgname` variable, since that has the
    /// ability to be more than one item.
    ///
    /// This function also accepts extended variables (i.e. `focal_postrm`), though only variables that can be
    /// extended by makedeb are supported.
    ///
    /// Returns the [`Some`] variant if the variable can be found, otherwise the [`None`] variant is returned.
    pub fn get_string(&self, key: &str) -> Option<&String> {
        if !SRCINFO_STRINGS.contains(&SrcInfo::get_base_key(key)) {
            return None;
        }

        if let Some(values) = self.map.get(&key.to_owned()) {
            Some(&values[0])
        } else {
            None
        }
    }

    /// Get a value for anything that's an array variable in a PKGBUILD.
    ///
    /// This function also accepts extended variables (i.e. `focal_depends`), though only variables that can be
    /// extended by makedeb are supported.
    ///
    /// Returns the [`Some`] variant if the variable can be found, otherwise the [`None`] variant is returned.
    pub fn get_array(&self, key: &str) -> Option<&Vec<String>> {
        if !SRCINFO_ARRAYS.contains(&SrcInfo::get_base_key(key)) {
            return None;
        }

        self.map.get(&key.to_owned())
    }

    /// Get the extended names (as well as the key itself) for a variable. Use this if you need a variable as well as any
    /// same variable that contains distribution and architecture extensions.
    ///
    /// If `key` isn't a key makedeb supports for variable name extensions, this will return the [`None`] variant, regardless
    /// of if the base key is in the `.SRCINFO` file or not.
    ///
    /// This returns a vector of strings that can be then passed into [`SrcInfo.get_string`] and
    /// [`SrcInfo.get_array`].
    pub fn get_extended_values(&self, key: &str) -> Option<Vec<String>> {
        if !SRCINFO_EXTENDED.contains(&key) {
            return None;
        }

        let mut matches: Vec<String> = Vec::new();
        let re = Regex::new(&format!(".*_{0}$|.*_{0}_.*|^{0}.*|^{0}$", key)).unwrap();

        for item in self.map.keys() {
            if re.is_match(item) {
                matches.push(item.clone());
            }
        }

        // If no items are in our vector, then no variants of the key were in the `.SRCINFO` file,
        // and we want to let the client know no matches were found.
        if matches.is_empty() {
            None
        } else {
            Some(matches)
        }
    }
}

/// A Struct representing a package's name, operator, and version.
pub struct SplitPackage {
    pub pkgname: String,
    pub operator: Option<String>,
    pub version: Option<String>,
}

impl SplitPackage {
    /// Split a dependency into its name, equality operator, and version.
    /// 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.
    pub fn new(pkg_string: &str) -> Self {
        let pkg = pkg_string.to_owned();

        for operator in ["<=", ">=", "=", "<", ">"] {
            if pkg.contains(operator) {
                let (pkgname, version) = pkg.split_once(operator).unwrap();
                return Self {
                    pkgname: pkgname.to_owned(),
                    operator: Some(operator.to_owned()),
                    version: Some(version.to_owned()),
                };
            }
        }

        Self {
            pkgname: pkg_string.to_owned(),
            operator: None,
            version: None,
        }
    }
}

/// A Struct representing a dependeny string (i.e. `pkg1|pkg2>=5`).
/// Note that any prefix such as `p!` will be kept on the first package, and must be removed manually client-side.
pub struct SplitDependency {
    pub(crate) deps: Vec<SplitPackage>,
}

impl SplitDependency {
    /// Create a new [`SplitDependency`] instance.
    pub fn new(dep_string: &str) -> Self {
        let mut deps = vec![];

        for dep in dep_string.split('|') {
            deps.push(SplitPackage::new(dep));
        }

        Self { deps }
    }

    pub(crate) fn internal_as_control(deps: &Vec<SplitPackage>) -> String {
        let mut segments = vec![];

        for dep in deps {
            if dep.operator.is_some() && dep.version.is_some() {
                let mut operator = dep.operator.as_ref().unwrap().clone();
                let version = dep.version.as_ref().unwrap();

                if ["<", ">"].contains(&operator.as_str()) {
                    operator = operator.clone() + &operator;
                }

                segments.push(format!("{} ({} {})", dep.pkgname, operator, version));
            } else {
                segments.push(dep.pkgname.clone());
            }
        }

        segments.join(" | ")
    }

    /// Print a Debian control-file styled representation of this dependency.
    pub fn as_control(&self) -> String {
        Self::internal_as_control(&self.deps)
    }
}