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)
}
}