use rpm_spec::ast::{BuildScriptKind, ScriptletKind, ShellBody, Span, SpecFile};
use super::tokens::{ShellToken, tokenize_line};
use super::walk::{BodyLocation, for_each_shell_body};
#[derive(Debug, Clone)]
pub struct CommandUse {
pub name: Option<String>,
pub tokens: Vec<ShellToken>,
pub location: SectionRef,
pub line_idx: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SectionRef {
BuildScript {
kind: BuildScriptKind,
section_span: Span,
},
Scriptlet {
kind: ScriptletKind,
section_span: Span,
},
Trigger {
section_span: Span,
},
FileTrigger {
section_span: Span,
},
Verify {
section_span: Span,
},
Sepolicy {
section_span: Span,
},
}
impl SectionRef {
pub fn section_span(&self) -> Span {
match *self {
Self::BuildScript { section_span, .. }
| Self::Scriptlet { section_span, .. }
| Self::Trigger { section_span }
| Self::FileTrigger { section_span }
| Self::Verify { section_span }
| Self::Sepolicy { section_span } => section_span,
}
}
}
#[derive(Debug, Default)]
pub struct CommandUseIndex {
uses: Vec<CommandUse>,
}
impl CommandUseIndex {
pub fn from_spec(spec: &SpecFile<Span>) -> Self {
let mut uses = Vec::new();
for_each_shell_body(spec, |loc, body| {
collect_body(&loc, body, &mut uses);
});
Self { uses }
}
pub fn all(&self) -> &[CommandUse] {
&self.uses
}
pub fn find<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a CommandUse> + 'a {
self.uses
.iter()
.filter(move |u| u.name.as_deref() == Some(name))
}
pub fn in_buildscript(&self, kind: BuildScriptKind) -> impl Iterator<Item = &CommandUse> + '_ {
self.uses.iter().filter(move |u| match u.location {
SectionRef::BuildScript { kind: k, .. } => k == kind,
_ => false,
})
}
}
fn collect_body(loc: &BodyLocation, body: &ShellBody, out: &mut Vec<CommandUse>) {
let section_ref = body_location_to_section_ref(loc);
for (idx, line) in body.lines.iter().enumerate() {
let tokens = tokenize_line(line);
if tokens.is_empty() {
continue;
}
let cmd_idx = control_word_skip(&tokens);
let name = tokens.get(cmd_idx).and_then(|t| t.literal_str());
out.push(CommandUse {
name,
tokens,
location: section_ref,
line_idx: idx,
});
}
}
fn control_word_skip(tokens: &[ShellToken]) -> usize {
const SHELL_CONTROL: &[&str] = &[
"if", "then", "else", "elif", "fi", "do", "done", "while", "for", "case", "esac", "until",
];
if let Some(first) = tokens.first().and_then(|t| t.literal_str())
&& SHELL_CONTROL.contains(&first.as_str())
{
return 1;
}
0
}
fn body_location_to_section_ref(loc: &BodyLocation) -> SectionRef {
match *loc {
BodyLocation::BuildScript { kind, span } => SectionRef::BuildScript {
kind,
section_span: span,
},
BodyLocation::Scriptlet { kind, span } => SectionRef::Scriptlet {
kind,
section_span: span,
},
BodyLocation::Trigger { span, .. } => SectionRef::Trigger { section_span: span },
BodyLocation::FileTrigger { span, .. } => SectionRef::FileTrigger { section_span: span },
BodyLocation::Verify { span } => SectionRef::Verify { section_span: span },
BodyLocation::Sepolicy { span } => SectionRef::Sepolicy { section_span: span },
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::parse;
#[test]
fn collects_commands_in_install() {
let src = "Name: x\n%install\nrm -rf %{buildroot}\ninstall -m 0644 foo /etc/foo\n";
let outcome = parse(src);
let idx = CommandUseIndex::from_spec(&outcome.spec);
let install: Vec<_> = idx
.in_buildscript(BuildScriptKind::Install)
.map(|u| u.name.as_deref().unwrap_or(""))
.collect();
assert_eq!(install, vec!["rm", "install"]);
}
#[test]
fn finds_commands_across_sections() {
let src = "Name: x\n%install\nsystemctl daemon-reload\n%post\nsystemctl restart foo\n";
let outcome = parse(src);
let idx = CommandUseIndex::from_spec(&outcome.spec);
let occ: Vec<_> = idx.find("systemctl").collect();
assert_eq!(occ.len(), 2, "{:?}", idx.all());
}
#[test]
fn skips_shell_control_word_to_real_command() {
let src = "Name: x\n%post\nif [ $1 = 1 ]; then systemctl enable foo; fi\n";
let outcome = parse(src);
let idx = CommandUseIndex::from_spec(&outcome.spec);
let scriptlet_uses: Vec<_> = idx
.all()
.iter()
.filter(|u| matches!(u.location, SectionRef::Scriptlet { .. }))
.collect();
assert!(!scriptlet_uses.is_empty());
let toks: Vec<_> = scriptlet_uses[0]
.tokens
.iter()
.filter_map(|t| t.literal_str())
.collect();
assert!(toks.iter().any(|s| s == "systemctl"));
}
#[test]
fn empty_spec_yields_empty_index() {
let outcome = parse("Name: x\n");
let idx = CommandUseIndex::from_spec(&outcome.spec);
assert!(idx.all().is_empty());
}
#[test]
fn macro_in_command_position_yields_no_name() {
let src = "Name: x\n%install\n%{my_install_helper} arg\n";
let outcome = parse(src);
let idx = CommandUseIndex::from_spec(&outcome.spec);
assert_eq!(idx.all().len(), 1);
assert!(idx.all()[0].name.is_none());
}
}