use std::collections::HashMap;
use std::convert::Infallible;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::Duration;
use field_names::FieldNames;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(feature = "shell-timeout")]
use process_control::{ChildExt, Control};
use crate::dependency::Dependency;
use crate::internal::exit_status_error::{ExitStatusError, ExitStatusExt};
use crate::internal::key_value_vec_map::{self, KeyValueLike};
use crate::internal::macros::bail;
use crate::internal::serde_key_value;
use crate::internal::std_ext::{ChunksExactIterator, Tap};
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Decode(#[from] serde_key_value::Error),
#[error("shell exited unsuccessfully: '{1}'")]
Evaluate(#[source] ExitStatusError, String),
#[error("I/O error occurred when {1}")]
Io(#[source] io::Error, &'static str),
#[error("syntax error in secfixes on line {0}: '{1}'")]
MalformedSecfixes(usize, String),
#[error("missing sha512sum for: '{0}'")]
MissingChecksum(String),
#[error("failed to read file '{1}'")]
ReadFile(#[source] io::Error, PathBuf),
#[error("failed to execute shell '{1}'")]
SpawnShell(#[source] io::Error, String),
#[error("exceeded timeout {0} ms")]
Timeout(u128),
}
#[derive(Debug, Default, PartialEq, Deserialize, Serialize, FieldNames)]
pub struct Apkbuild {
#[serde(skip_serializing_if = "Option::is_none")]
#[field_names(skip)] pub maintainer: Option<String>,
#[serde(default)]
#[field_names(skip)] pub contributors: Vec<String>,
pub pkgname: String,
pub pkgver: String,
pub pkgrel: u32,
pub pkgdesc: String,
pub url: String,
#[serde(default)]
pub arch: Vec<String>,
pub license: String,
#[serde(default, with = "key_value_vec_map")]
pub depends: Vec<Dependency>,
#[serde(default, with = "key_value_vec_map")]
pub makedepends: Vec<Dependency>,
#[serde(default, with = "key_value_vec_map")]
pub makedepends_build: Vec<Dependency>,
#[serde(default, with = "key_value_vec_map")]
pub makedepends_host: Vec<Dependency>,
#[serde(default, with = "key_value_vec_map")]
pub checkdepends: Vec<Dependency>,
#[serde(default, with = "key_value_vec_map")]
pub install_if: Vec<Dependency>,
#[serde(default)]
pub pkgusers: Vec<String>,
#[serde(default)]
pub pkggroups: Vec<String>,
#[serde(default, with = "key_value_vec_map")]
pub provides: Vec<Dependency>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider_priority: Option<u32>,
#[serde(default, with = "key_value_vec_map")]
pub replaces: Vec<Dependency>,
#[serde(skip_serializing_if = "Option::is_none")]
pub replaces_priority: Option<u32>,
#[serde(default)]
pub install: Vec<String>,
#[serde(default)]
pub triggers: Vec<String>,
#[serde(default)]
pub subpackages: Vec<String>,
#[serde(default, rename = "sources")]
pub source: Vec<Source>,
#[serde(default)]
pub options: Vec<String>,
#[serde(default, with = "key_value_vec_map")]
#[field_names(skip)] pub secfixes: Vec<Secfix>,
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct Source {
pub name: String,
pub uri: String,
pub checksum: String,
}
impl Source {
pub fn new<N, U, C>(name: N, uri: U, checksum: C) -> Self
where
N: ToString,
U: ToString,
C: ToString,
{
Source {
name: name.to_string(),
uri: uri.to_string(),
checksum: checksum.to_string(),
}
}
}
#[derive(Debug, PartialEq, Deserialize)]
pub struct Secfix {
pub version: String,
pub fixes: Vec<String>,
}
impl Secfix {
pub fn new<S: ToString>(version: S, fixes: Vec<String>) -> Self {
Secfix {
version: version.to_string(),
fixes,
}
}
}
impl<'a> KeyValueLike<'a> for Secfix {
type Key = &'a str;
type Value = Vec<String>;
type Err = Infallible;
fn from_key_value(key: Self::Key, value: Self::Value) -> Result<Self, Self::Err> {
Ok(Secfix::new(key, value))
}
fn to_key_value(&'a self) -> (Self::Key, Self::Value) {
(&self.version, self.fixes.clone())
}
}
pub struct ApkbuildReader {
env: HashMap<OsString, OsString>,
inherit_env: bool,
shell_cmd: OsString,
#[allow(unused)]
time_limit: Duration,
eval_fields: Vec<&'static str>,
eval_script: Vec<u8>,
}
impl ApkbuildReader {
pub fn new() -> Self {
Self::default()
}
pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.env.insert(OsString::from(&key), OsString::from(&val));
self
}
pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
for (ref key, ref val) in vars {
self.env.insert(OsString::from(&key), OsString::from(&val));
}
self
}
pub fn inherit_env(&mut self, cond: bool) -> &mut Self {
self.inherit_env = cond;
self
}
pub fn shell_cmd<S: AsRef<OsStr>>(&mut self, cmd: S) -> &mut Self {
self.shell_cmd = OsString::from(&cmd);
self
}
#[cfg(feature = "shell-timeout")]
pub fn time_limit(&mut self, limit: Duration) -> &mut Self {
self.time_limit = limit;
self
}
pub fn read_apkbuild<P: AsRef<Path>>(&self, filepath: P) -> Result<Apkbuild, Error> {
let filepath = filepath.as_ref();
let apkbuild_str =
fs::read_to_string(filepath).map_err(|e| Error::ReadFile(e, filepath.to_owned()))?;
let values = self.evaluate(filepath)?;
let mut sha512sums: Option<&str> = None;
let mut source: Option<&str> = None;
let parsed = self
.eval_fields
.iter()
.zip(values.trim_end().split_terminator('\x1E'))
.fold(Vec::with_capacity(64), |mut acc, (key, val)| {
match *key {
"source" => source = Some(val),
"sha512sums" => sha512sums = Some(val),
"license" | "pkgdesc" | "pkgver" | "url" => {
acc.push((*key, val));
}
_ => {
for mut word in val.split_ascii_whitespace() {
if *key == "subpackages" {
word = word.split(':').next().unwrap(); }
acc.push((*key, word));
}
}
};
acc
});
let mut apkbuild: Apkbuild = serde_key_value::from_ordered_pairs(parsed)?;
if let Some(source) = source {
apkbuild.source = decode_source_and_sha512sums(source, sha512sums.unwrap_or(""))?;
}
apkbuild.maintainer = parse_maintainer(&apkbuild_str).map(|s| s.to_owned());
apkbuild.contributors = parse_contributors(&apkbuild_str)
.map(|s| s.to_owned())
.collect();
apkbuild.secfixes = parse_secfixes(&apkbuild_str)?;
Ok(apkbuild)
}
fn evaluate(&self, filepath: &Path) -> Result<String, Error> {
let startdir = filepath
.parent()
.unwrap_or_else(|| panic!("invalid APKBUILD path: `{:?}`", filepath));
let filename = filepath
.file_name()
.unwrap_or_else(|| panic!("invalid APKBUILD path: `{:?}`", filepath));
let mut child = Command::new(&self.shell_cmd)
.tap_mut_if(!self.inherit_env, |cmd| {
cmd.env_clear();
})
.envs(self.env.iter())
.env("APKBUILD", filename)
.tap_mut_if(!startdir.as_os_str().is_empty(), |cmd| {
cmd.current_dir(startdir);
})
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| Error::SpawnShell(e, self.shell_cmd.to_string_lossy().into_owned()))?;
let mut stdin = child.stdin.take().unwrap(); stdin
.write_all(&self.eval_script)
.map_err(|e| Error::Io(e, "writing data to stdin of shell"))?;
drop(stdin);
#[cfg(feature = "shell-timeout")]
let output = child
.controlled_with_output()
.pipe_if(!self.time_limit.is_zero(), |ctrl| {
ctrl.terminate_for_timeout().time_limit(self.time_limit)
})
.wait()
.map_err(|e| Error::Io(e, "waiting on shell process"))?
.ok_or(Error::Timeout(self.time_limit.as_millis()))?;
#[cfg(not(feature = "shell-timeout"))]
let output = child
.wait_with_output()
.map_err(|e| Error::Io(e, "waiting on shell process"))?;
output
.status
.exit_ok()
.map_err(|e| Error::Evaluate(e, String::from_utf8_lossy(&output.stderr).into()))?;
String::from_utf8(output.stdout).map_err(|e| {
Error::Io(
io::Error::new(io::ErrorKind::InvalidData, e),
"reading shell stdout",
)
})
}
}
impl Default for ApkbuildReader {
fn default() -> Self {
let path = std::env::var_os("PATH").unwrap_or_else(|| "/usr/bin:/bin".into());
let eval_fields: Vec<_> = Apkbuild::FIELDS.into_iter().chain(["sha512sums"]).collect();
let eval_script = eval_fields
.iter()
.fold(
r#". ./"$APKBUILD" >/dev/null; echo "#.to_owned(),
|acc, field| acc + "$" + field + "\x1E",
)
.into_bytes();
Self {
shell_cmd: "/bin/sh".into(),
env: HashMap::from([("PATH".into(), path)]),
inherit_env: false,
time_limit: Duration::from_millis(500),
eval_fields,
eval_script,
}
}
}
fn parse_comment_attribute<'a>(name: &str, line: &'a str) -> Option<&'a str> {
line.trim()
.strip_prefix("# ")
.and_then(|s| s.trim_start().strip_prefix(name))
.map(str::trim_start)
.and_then(|s| (!s.is_empty()).then_some(s))
}
fn parse_maintainer(apkbuild: &str) -> Option<&str> {
apkbuild
.lines()
.find_map(|s| parse_comment_attribute("Maintainer:", s))
}
fn parse_contributors(apkbuild: &str) -> impl Iterator<Item = &str> {
apkbuild
.lines()
.take(10)
.filter_map(|s| parse_comment_attribute("Contributor:", s))
}
fn parse_secfixes(apkbuild: &str) -> Result<Vec<Secfix>, Error> {
let mut lines = apkbuild.lines().enumerate();
let mut secfixes: Vec<Secfix> = vec![];
if !lines.any(|(_, s)| s.starts_with("# secfixes:")) {
return Ok(secfixes);
}
for pair in lines.map_while(|(i, s)| s.strip_prefix("# ").map(|s| (i, s))) {
let line_no = pair.0 + 1;
let line = pair.1.split(" #").next().unwrap().trim();
if let Some(line) = line.strip_prefix("- ") {
if let Some(Secfix { fixes, .. }) = secfixes.last_mut() {
fixes.push(line.trim_start().to_string());
} else {
bail!(Error::MalformedSecfixes(line_no, pair.1.to_owned()));
}
} else if let Some(key) = line.strip_suffix(':') {
secfixes.push(Secfix {
version: key.to_owned(),
fixes: Vec::with_capacity(3),
});
} else {
bail!(Error::MalformedSecfixes(line_no, pair.1.to_owned()));
}
}
Ok(secfixes)
}
fn decode_source_and_sha512sums(source: &str, sha512sums: &str) -> Result<Vec<Source>, Error> {
let mut sha512sums: HashMap<&str, &str> = sha512sums
.split_ascii_whitespace()
.chunks_exact()
.map(|[a, b]| (b, a))
.collect();
source
.split_ascii_whitespace()
.map(|item| {
let (name, uri) = if let Some((name, uri)) = item.split_once("::") {
(name, uri)
} else if let Some((_, name)) = item.rsplit_once('/') {
(name, item)
} else {
(item, item)
};
sha512sums
.remove(name)
.map(|checksum| Source::new(name, uri, checksum))
.ok_or_else(|| Error::MissingChecksum(name.to_owned()))
})
.collect()
}
#[cfg(test)]
#[path = "apkbuild.test.rs"]
mod test;