use crate::{parse::SpecParser, util::Consumer};
use color_eyre::eyre::eyre;
use itertools::Itertools;
use parking_lot::RwLock;
use rpmspec_common::util::handle_line_skip;
use rpmspec_common::PErr as PE;
use smartstring::alias::String;
use std::collections::HashMap;
use std::io::{Read, Write};
use std::{path::Path, sync::Arc};
type InternalMacroFn = fn(&mut SpecParser, &mut String, &mut Consumer<dyn Read + '_>) -> Result<(), PE>;
#[derive(Clone)]
pub enum MacroType {
Internal(InternalMacroFn),
Runtime {
file: Arc<Path>,
offset: usize,
len: usize,
s: Arc<RwLock<String>>,
param: bool,
},
}
impl std::fmt::Debug for MacroType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Internal(_) => f.write_str("<builtin>")?,
Self::Runtime { offset, len, s, .. } => f.write_str(&s.read()[*offset..offset + len])?,
}
Ok(())
}
}
impl From<&str> for MacroType {
fn from(value: &str) -> Self {
Self::Runtime { file: Arc::from(Path::new("unknown")), offset: 0, s: Arc::new(RwLock::new(value.into())), param: false, len: value.len() }
}
}
macro_rules! __internal_macros {
($(macro $m:ident($p:pat, $o:pat, $r:pat) $body:block )+) => {
$(
#[allow(non_snake_case, clippy::unnecessary_wraps)]
fn $m($p: &mut SpecParser, $o: &mut String, $r: &mut Consumer<dyn Read + '_>) -> Result<(), PE> $body
)+
lazy_static::lazy_static! {
pub static ref INTERNAL_MACROS: HashMap<String, Vec<MacroType>> = {
let mut ret = HashMap::new();
$({
ret.insert(stringify!($m).into(), vec![MacroType::Internal($m)]);
})+
ret
};
}
};
}
__internal_macros!(
macro define(p, _, r) {
r.until(|ch| !ch.is_whitespace());
let pos = r.pos;
let def = handle_line_skip(r.read_til_eot()?.chars());
let def = def.trim_start();
#[rustfmt::skip]
let Some((name, _)) = def.split_once(' ') else {
return Err(eyre!("%define: Expected 2 arguments").into());
};
let (name, param): (String, bool) = name.strip_suffix("()").map_or_else(|| (name.into(), false), |x| (x.into(), true));
let csm = r.range(pos + 1 + name.len()..r.pos).ok_or_else(|| eyre!("%define: cannot unwind Consumer"))?;
p.define_macro(name, &csm, param, csm.end - csm.pos);
Ok(())
}
macro global(p, o, r) {
define(p, o, r)
}
macro undefine(p, _, r) {
p.macros.remove(&*r.read_til_eot().unwrap());
Ok(())
}
macro load(p, _, r) {
let f: String = r.collect();
p.load_macro_from_file(std::path::Path::new(&*f))?;
Ok(())
}
macro expand(p, o, r) {
let new_reader = r.range(r.pos..r.end).expect("Cannot wind Consumer in %expand");
let mut new_reader = *unsafe { Box::from_raw(Box::into_raw(Box::new(new_reader)).cast()) };
p.parse_macro::<std::fs::File>(o, &mut new_reader)?;
Ok(())
}
macro expr(p, o, r) {
let mut rawexpression = String::new();
expand(p, &mut rawexpression, r)?;
let mut csm: Consumer<std::fs::File> = Consumer::new(Arc::new(RwLock::new(rawexpression)), None, Arc::from(Path::new("<expr>")));
p.parse_expr(o, &mut csm)?;
Ok(())
}
macro lua(p, o, r) {
let content: String = r.collect();
let content = content.lines().map(|line| line.trim_end().trim_end_matches('\\')).join("\n");
let parser = Arc::new(RwLock::new(std::mem::take(p)));
let out = crate::lua::run(&parser, &content)?;
std::mem::swap(p, &mut Arc::try_unwrap(parser).expect("cannot unwrap Arc rpmparser in %lua").into_inner());
o.push_str(&out);
Ok(())
}
macro macrobody(p, o, r) {
let name = r.collect();
#[rustfmt::skip]
let Some(Some(m)) = p.macros.get(&name).map(|x| x.last()) else {
return Err(PE::MacroNotFound(name));
};
match m {
MacroType::Internal(_) => o.push_str("<builtin>"),
MacroType::Runtime { file, offset, len, s, .. } => {
let mut csm: Consumer<std::fs::File> = Consumer::new(Arc::clone(s), None, Arc::clone(file));
csm.pos = *offset;
csm.end = *offset + len;
o.push_str(&csm.collect::<String>());
},
}
Ok(())
}
macro quote(_, o, r) {
o.push('"');
o.push_str(&r.collect::<String>());
o.push('"');
Ok(())
}
macro len(_, o, r) {
o.push_str(&r.collect::<Box<[char]>>().len().to_string());
Ok(())
}
macro lower(_, o, r) {
o.push_str(&r.collect::<String>().to_ascii_lowercase());
Ok(())
}
macro reverse(_, o, r) {
let mut chs = r.collect::<Box<[char]>>();
chs.reverse();
chs.iter().for_each(|ch| o.push(*ch));
Ok(())
}
macro upper(_, o, r) {
o.push_str(&r.collect::<String>().to_ascii_uppercase());
Ok(())
}
macro shescape(_, o, r) {
o.push('\'');
for ch in r {
if ch == '\'' {
o.push('\'');
o.push('\\');
o.push('\'');
}
o.push(ch);
}
o.push('\'');
Ok(())
}
macro shrink(_, o, r) {
let mut space = false;
for ch in r {
if ch.is_whitespace() {
space = true;
continue;
}
if space {
space = false;
o.push(' ');
}
o.push(ch);
}
Ok(())
}
macro basename(_, o, r) {
let s: String = r.collect();
o.push_str(s.rsplit_once('/').map_or(&s, |(_, x)| x));
Ok(())
}
macro dirname(_, o, r) {
let s: String = r.collect();
o.push_str(s.rsplit_once('/').map_or(&s, |(x, _)| x));
Ok(())
}
macro exists(_, o, r) {
o.push(if Path::new(&*r.collect::<String>()).exists() { '1' } else { '0' });
Ok(())
}
macro suffix(_, o, r) {
let s: String = r.collect();
o.push_str(s.rsplit_once('.').map_or("", |(_, x)| x));
Ok(())
}
macro url2path(_, o, r) {
let s: String = r.collect();
#[rustfmt::skip]
let Ok(url) = url::Url::parse(&s) else {
o.push_str(&s);
return Ok(());
};
if matches!(url.scheme(), "https" | "http" | "hkp" | "file" | "ftp") {
o.push_str(url.path());
} else {
o.push_str(&s);
}
Ok(())
}
macro u2p(p, o, r) {
url2path(p, o, r)
}
macro uncompress(_, o, r) {
let pathstr: String = r.collect();
let path = std::path::PathBuf::from(&*pathstr);
let Ok(fmt) = file_format::FileFormat::from_file(&path) else { return Ok(()) };
o.push_str(match fmt {
file_format::FileFormat::ArbitraryBinaryData => "cat ",
file_format::FileFormat::Gzip => "gzip -dc ",
file_format::FileFormat::Bzip2 => "bzip2 -dc ",
file_format::FileFormat::Zip => "unzip ",
file_format::FileFormat::LempelZivMarkovChainAlgorithm | file_format::FileFormat::Xz => "xz -dc ",
file_format::FileFormat::Lzip => "lzip -dc ",
file_format::FileFormat::LongRangeZip => "lrzip -dqo- ",
file_format::FileFormat::SevenZip => "7zip x ",
file_format::FileFormat::Zstandard => "zstd -dc ",
file_format::FileFormat::TapeArchive if path.extension() == Some(std::ffi::OsStr::new("gem")) => "gem unpack ",
_ => return Err(eyre!("Unexpected file format: {fmt:?}").into()),
});
o.push_str(&pathstr);
Ok(())
}
macro getncpus(_, o, _) {
o.push_str(&num_cpus::get().to_string());
Ok(())
}
macro getconfidir(_, o, _) {
let res = std::env::var("RPM_CONFIGDIR");
if let Err(std::env::VarError::NotUnicode(s)) = res {
return Err(eyre!("%{{getconfdir}} failed: While grabbing env var `RPM_CONFIGDIR`: Non-unicode OsString {s:?}").into());
}
o.push_str(res.as_ref().map(|x| &**x).unwrap_or("/usr/lib/rpm"));
Ok(())
}
macro getenv(_, o, r) {
let name: String = r.collect();
match std::env::var(&*name) {
Ok(x) => o.push_str(&x),
Err(std::env::VarError::NotPresent) => {},
Err(std::env::VarError::NotUnicode(s)) => return Err(eyre!("%{{getenv:{name}}} failed: Non-unicode OsString {s:?}").into()),
}
Ok(())
}
macro rpmversion(_, o, _) {
o.push_str(env!("CARGO_PKG_VERSION"));
Ok(())
}
macro echo(_, _, r) {
tracing::info!("{}", r.collect::<String>());
Ok(())
}
macro warn(_, _, r) {
tracing::warn!("{}", r.collect::<String>());
Ok(())
}
macro error(_, _, r) {
tracing::error!("{}", r.collect::<String>());
Ok(())
}
macro verbose(_, o, _) {
o.push('0');
Ok(())
}
macro S(p, o, r) {
expand(p, o, &mut Consumer::new(Arc::new(RwLock::new("%SOURCE".into())), None, r.file.clone()))?;
r.for_each(|c| o.push(c));
Ok(())
}
macro P(p, o, r) {
expand(p, o, &mut Consumer::new(Arc::new(RwLock::new("%PATCH".into())), None, r.file.clone()))?;
r.for_each(|c| o.push(c));
Ok(())
}
macro trace(_, _, _) {
tracing::warn!("`%trace` is not supposed by rpmspec-rs");
Ok(())
}
macro dump(p, _, r) {
let args = r.collect::<String>();
if !args.is_empty() {
tracing::warn!(?args, "Unexpected arguments to %dump");
}
let mut stdout = std::io::stdout().lock();
for (k, v) in &p.macros {
if let Some(v) = v.last() {
let MacroType::Runtime { file, offset, len, s, param } = v else {
stdout.write_fmt(format_args!("[<internal>]\t%{k}\t<builtin>\n"))?;
continue;
};
let ss = s.read();
let front = &ss[..*offset];
let nline = front.chars().filter(|c| *c == '\n').count() + 1;
let col = offset - front.find('\n').unwrap_or(0);
let f = file.display();
let p = if *param { "{}" } else { "" };
let inner = &ss[*offset..*offset + *len];
stdout.write_fmt(format_args!("[{f}:{nline}:{col}]\t%{k}{p}\t{inner}\n"))?;
}
}
Ok(())
}
);