use std::path::Path;
use std::process::ExitCode;
use trex::files::{LineIndex, Source, WalkOptions, collect};
use trex::rule_scan::{Finding, Found, Place, RuleScan, declare_rule_files};
use crate::cli_files::{Editing, across_cores, edit_sources, review_sources};
use crate::cli_window::{Part, Restrict, Windowing, read_part};
pub(crate) struct RulesRun<'a> {
pub shapes: trex::ShapeSet,
pub rules: &'a [String],
pub paths: Vec<String>,
pub text: Option<Vec<u8>>,
pub walk: WalkOptions,
pub json: bool,
pub sarif: bool,
pub github: bool,
pub format: Option<String>,
pub count: bool,
pub files_with_matches: bool,
pub files_without_match: bool,
pub max_count: Option<usize>,
pub require_match: bool,
pub binary: bool,
pub fix: bool,
pub dry_run: bool,
pub interactive: bool,
pub context: usize,
pub painter: trex::paint::Painter,
pub windowing: Windowing,
pub unit: trex::records::RecordUnit,
}
enum Outcome {
Pending,
Binary,
Failed(String),
Scanned(Found),
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Form {
Human,
Json,
Sarif,
Github,
Format,
Count,
FilesWith,
FilesWithout,
}
pub(crate) fn run(mut run: RulesRun<'_>) -> ExitCode {
if let Err(e) = declare_rule_files(&mut run.shapes, run.rules) {
eprintln!("trex: {e}");
return ExitCode::FAILURE;
}
if run.shapes.rules().is_empty() {
eprintln!("trex scan: {} holds no rule", run.rules.join(", "));
return ExitCode::FAILURE;
}
let scan = match RuleScan::new(run.shapes.clone()) {
Ok(s) => s,
Err(e) => {
eprintln!("trex: {e}");
return ExitCode::FAILURE;
}
};
let template = match &run.format {
Some(src) => match trex::Template::parse_report(src, &scan.capture_names()) {
Ok(t) => Some(t),
Err(e) => {
eprintln!("trex: --format error at byte {}: {}", e.pos, e.msg);
return ExitCode::FAILURE;
}
},
None => None,
};
let form = if run.json {
Form::Json
} else if run.sarif {
Form::Sarif
} else if run.github {
Form::Github
} else if template.is_some() {
Form::Format
} else if run.count {
Form::Count
} else if run.files_with_matches {
Form::FilesWith
} else if run.files_without_match {
Form::FilesWithout
} else {
Form::Human
};
let report = Report { scan: &scan, form, template: template.as_ref(), painter: &run.painter };
if let Some(text) = &run.text {
if !run.paths.is_empty() {
eprintln!("trex scan: --text and a path cannot both be given");
return ExitCode::FAILURE;
}
if run.fix {
eprintln!("trex scan: --fix writes files and takes no --text");
return ExitCode::FAILURE;
}
if run.files_with_matches || run.files_without_match {
eprintln!("trex scan: -l and -L name files and take no --text");
return ExitCode::FAILURE;
}
if run.windowing.follow {
eprintln!("trex scan: --follow follows files by name and takes no --text");
return ExitCode::FAILURE;
}
let part = Part::of_text(text.clone(), run.windowing.select, &run.unit);
let findings = scan.findings(None, &part.text, &part.index(), run.max_count);
let found = vec![found_of(None, part, findings)];
let (total, errors) = report.print(&found, false);
return exit(false, total, errors, run.require_match);
}
let a_directory = run.paths.iter().any(|p| Path::new(p).is_dir());
let (mut sources, walk_errors) = collect(&run.paths, &run.walk);
if sources.is_empty() && run.paths.is_empty() {
sources.push(Source::Stdin);
}
for e in &walk_errors {
eprintln!("trex: {e}");
}
let mut failed = !walk_errors.is_empty();
if run.fix {
if sources.contains(&Source::Stdin) {
eprintln!("trex scan: --fix writes files and cannot write the standard input");
return ExitCode::FAILURE;
}
let edits_of = |src: &Source, piece: crate::cli_window::Piece<'_>| {
let name = src.name();
let (edits, skipped) = scan.fix_edits(&name, piece.text, &piece.index());
for s in skipped {
eprintln!(
"trex: {name}:{}:{}: the fix of {} overlaps an earlier fix; skipped",
s.line,
s.col,
scan.rules()[s.rule].name
);
}
edits
};
let how = Editing {
failed,
a_directory,
binary: run.binary,
verb: "fixes",
dry_run: run.dry_run,
context: run.context,
restrict: Restrict::of(&run.windowing, &run.unit),
};
return if run.interactive {
review_sources(&sources, how, None, false, edits_of)
} else {
edit_sources(&sources, how, edits_of)
};
}
let numbers = !matches!(form, Form::Count | Form::FilesWith | Form::FilesWithout);
let asked =
trex::window::Asked { numbers, offsets: numbers || run.windowing.follow, binary: run.binary, units: false };
if run.windowing.follow {
if sources.contains(&Source::Stdin) {
eprintln!(
"trex scan: --follow follows files by name, and the standard input is read as it arrives already; name the file"
);
return ExitCode::FAILURE;
}
let prefixed = a_directory || sources.len() > 1;
return crate::cli_follow::follow_rules(&sources, &run, &report, asked, prefixed, failed);
}
let mut outcomes: Vec<Outcome> = sources.iter().map(|_| Outcome::Pending).collect();
across_cores(&mut outcomes, |k, slot| {
let src = &sources[k];
let part = match read_part(src, run.windowing.select, &run.unit, asked) {
Ok(part) => part,
Err(e) => {
*slot = Outcome::Failed(format!("cannot read {}: {e}", src.name()));
return;
}
};
if !run.binary && part.binary {
*slot = Outcome::Binary;
return;
}
let name = src.name();
let findings = scan.findings(Some(&name), &part.text, &part.index(), run.max_count);
*slot = Outcome::Scanned(found_of(Some(name), part, findings));
});
let mut found: Vec<Found> = Vec::new();
for (src, outcome) in sources.iter().zip(outcomes) {
match outcome {
Outcome::Pending => {}
Outcome::Binary => {
if !a_directory {
eprintln!("trex: {} holds a NUL byte and is binary; --binary scans it", src.name());
failed = true;
}
}
Outcome::Failed(e) => {
eprintln!("trex: {e}");
failed = true;
}
Outcome::Scanned(one) => found.push(one),
}
}
let prefixed = a_directory || sources.len() > 1;
let (total, errors) = report.print(&found, prefixed);
exit(failed, total, errors, run.require_match)
}
fn found_of(name: Option<String>, part: Part, findings: Vec<Finding>) -> Found {
Found { name, input: part.text, byte_base: part.byte_base, line_base: part.line_base, findings }
}
fn exit(failed: bool, total: usize, errors: usize, require_match: bool) -> ExitCode {
if failed || errors > 0 || (require_match && total == 0) { ExitCode::FAILURE } else { ExitCode::SUCCESS }
}
pub(crate) struct Report<'a> {
pub(crate) scan: &'a RuleScan,
form: Form,
template: Option<&'a trex::Template>,
painter: &'a trex::paint::Painter,
}
impl Report<'_> {
fn print(&self, found: &[Found], prefixed: bool) -> (usize, usize) {
let mut objects: Vec<String> = Vec::new();
let (total, errors) = self.print_findings(found, prefixed, &mut |object| objects.push(object));
match self.form {
Form::Json => crate::out::line(&format!("[{}]", objects.join(","))),
Form::Sarif => crate::out::line(&self.scan.sarif(found)),
Form::Human if total == 0 => crate::out::line("no finding"),
_ => {}
}
(total, errors)
}
pub(crate) fn print_findings(
&self,
found: &[Found],
prefixed: bool,
json: &mut dyn FnMut(String),
) -> (usize, usize) {
use trex::paint::Role;
let rules = self.scan.rules();
let mut total = 0usize;
let mut errors = 0usize;
for one in found {
let Found { name, input, findings, .. } = one;
total += findings.len();
errors += findings.iter().filter(|f| rules[f.rule].severity == trex::Severity::Error).count();
let index = one.index();
let name = name.as_deref();
match self.form {
Form::Count => match name {
Some(n) if prefixed => crate::out::line(&format!(
"{}{}{}",
self.painter.paint(Role::Path, n),
self.painter.paint(Role::Separator, ":"),
findings.len()
)),
_ => crate::out::line(&findings.len().to_string()),
},
Form::FilesWith => {
if let Some(n) = name
&& !findings.is_empty()
{
crate::out::line(&self.painter.paint(Role::Path, n));
}
}
Form::FilesWithout => {
if let Some(n) = name
&& findings.is_empty()
{
crate::out::line(&self.painter.paint(Role::Path, n));
}
}
Form::Json => {
for f in findings {
json(self.scan.finding_json(name, input, &index, f));
}
}
Form::Sarif => {}
Form::Github => {
for f in findings {
crate::out::line(&self.scan.github_line(name, input, &index, f));
}
}
Form::Format => {
let Some(template) = self.template else {
continue;
};
for f in findings {
let rule = &rules[f.rule];
let place = Place::of(&index, input, f.start, f.end);
let at = trex::ReportAt {
path: name.unwrap_or(""),
line: place.line,
col: place.col,
base: index.base(),
pattern: Some(&rule.name),
rule: Some(trex::ReportRule {
name: &rule.name,
severity: rule.severity.name(),
message: &f.message,
fix: f.fix.as_deref().unwrap_or(""),
}),
};
crate::out::line(&template.render_report(&f.m, input, &at));
}
}
Form::Human => {
for f in findings {
crate::out::line(&self.human_line(name, input, &index, f));
if let Some(fix) = &f.fix {
crate::out::line(&format!(" fix: {fix:?}"));
}
}
}
}
}
(total, errors)
}
fn human_line(&self, name: Option<&str>, input: &[u8], index: &LineIndex, f: &Finding) -> String {
use trex::paint::Role;
let rule = &self.scan.rules()[f.rule];
let (line, col) = index.line_col(input, f.start);
let sep = |s: &str| self.painter.paint(Role::Separator, s);
let mut out = String::new();
if let Some(n) = name {
out.push_str(&self.painter.paint(Role::Path, n));
out.push_str(&sep(":"));
}
out.push_str(&self.painter.paint(Role::Line, &line.to_string()));
out.push_str(&sep(":"));
out.push_str(&self.painter.paint(Role::Column, &col.to_string()));
out.push_str(&sep(":"));
out.push(' ');
out.push_str(rule.severity.name());
out.push_str(": ");
out.push_str(&f.message);
out.push_str(" [");
out.push_str(&rule.name);
out.push(']');
out
}
}