yara-x-cli 1.14.0

A command-line interface for YARA-X.
use std::fs::File;
use std::io::{Cursor, Seek, SeekFrom};
use std::path::PathBuf;
use std::{fs, io, process};

use clap::{ArgAction, ArgMatches, Command, arg, value_parser};
use yara_x_fmt::{Formatter, Indentation};

use crate::config::Config;
use crate::help;

pub fn fmt() -> Command {
    super::command("fmt")
        .about("Format YARA source files")
        .arg(
            arg!(<FILE>)
                .help("Path to YARA source file")
                .required(true)
                .value_parser(value_parser!(PathBuf))
                .action(ArgAction::Append),
        )
        .arg(
            arg!(-c --check  "Run in 'check' mode")
                .long_help(help::FMT_CHECK_MODE),
        )
        .arg(
            arg!(-t - -"tab-size" <NUM_SPACES>)
                .help("Tab size (in spaces) used in source files")
                .long_help(help::FMT_TAB_SIZE)
                .default_value("4")
                .value_parser(value_parser!(usize)),
        )
}

pub fn exec_fmt(args: &ArgMatches, config: &Config) -> anyhow::Result<()> {
    let files = args.get_many::<PathBuf>("FILE").unwrap();
    let check = args.get_flag("check");
    let tab_size = args.get_one::<usize>("tab-size").unwrap();

    let formatter = Formatter::new()
        .input_tab_size(*tab_size)
        .align_metadata(config.fmt.meta.align_values)
        .align_patterns(config.fmt.patterns.align_values)
        .indent_section_headers(config.fmt.rule.indent_section_headers)
        .indent_section_contents(config.fmt.rule.indent_section_contents)
        .indentation(if config.fmt.rule.indent_spaces == 0 {
            Indentation::Tabs
        } else {
            Indentation::Spaces(config.fmt.rule.indent_spaces as usize)
        })
        .newline_before_curly_brace(config.fmt.rule.newline_before_curly_brace)
        .empty_line_before_section_header(
            config.fmt.rule.empty_line_before_section_header,
        )
        .empty_line_after_section_header(
            config.fmt.rule.empty_line_after_section_header,
        );

    let mut modified_files: Vec<&PathBuf> = Vec::new();

    for file in files {
        let input = fs::read(file.as_path())?;
        let file_modified = if check {
            formatter.format(input.as_slice(), io::sink())?
        } else {
            let mut formatted = Cursor::new(Vec::with_capacity(input.len()));
            if formatter.format(input.as_slice(), &mut formatted)? {
                formatted.seek(SeekFrom::Start(0))?;
                let mut output_file = File::create(file.as_path())?;
                io::copy(&mut formatted, &mut output_file)?;
                true
            } else {
                false
            }
        };

        if file_modified {
            modified_files.push(file);
        }
    }

    if !modified_files.is_empty() {
        if check {
            for file in &modified_files {
                eprintln!("{}", file.display());
            }
        }
        process::exit(1)
    }

    Ok(())
}