1928 lines · 68.6 KB · Rust
RawDownload
use brush_parser::ast;use brush_parser::ast::SourceLocation;use brush_parser::word::WordPiece;use brush_parser::{Parser, ParserOptions, SourceInfo};use std::io::BufReader; #[derive(Debug, Clone, PartialEq, Eq)]pub struct TerminalCommandPrefix {    pub normalized: String,    pub display: String,    pub tokens: Vec<String>,    pub command: String,    pub subcommand: Option<String>,} #[derive(Debug, Clone, PartialEq, Eq)]pub enum TerminalCommandValidation {    Safe,    Unsafe,    Unsupported,} pub fn extract_commands(command: &str) -> Option<Vec<String>> {    let reader = BufReader::new(command.as_bytes());    let options = ParserOptions::default();    let source_info = SourceInfo::default();    let mut parser = Parser::new(reader, &options, &source_info);     let program = parser.parse_program().ok()?;     let mut commands = Vec::new();    extract_commands_from_program(&program, &mut commands)?;     Some(commands)} pub fn extract_terminal_command_prefix(command: &str) -> Option<TerminalCommandPrefix> {    let reader = BufReader::new(command.as_bytes());    let options = ParserOptions::default();    let source_info = SourceInfo::default();    let mut parser = Parser::new(reader, &options, &source_info);     let program = parser.parse_program().ok()?;    let simple_command = first_simple_command(&program)?;     let mut normalized_tokens = Vec::new();    let mut display_start = None;    let mut display_end = None;     if let Some(prefix) = &simple_command.prefix {        for item in &prefix.0 {            if let ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, word) = item {                match normalize_assignment_for_command_prefix(assignment, word)? {                    NormalizedAssignment::Included(normalized_assignment) => {                        normalized_tokens.push(normalized_assignment);                        update_display_bounds(&mut display_start, &mut display_end, word);                    }                    NormalizedAssignment::Skipped => {}                }            }        }    }     let command_word = simple_command.word_or_name.as_ref()?;    let command_name = normalize_word(command_word)?;    normalized_tokens.push(command_name.clone());    update_display_bounds(&mut display_start, &mut display_end, command_word);     let mut subcommand = None;    if let Some(suffix) = &simple_command.suffix {        for item in &suffix.0 {            match item {                ast::CommandPrefixOrSuffixItem::IoRedirect(_) => continue,                ast::CommandPrefixOrSuffixItem::Word(word) => {                    let normalized_word = normalize_word(word)?;                    if !normalized_word.starts_with('-') {                        subcommand = Some(normalized_word.clone());                        normalized_tokens.push(normalized_word);                        update_display_bounds(&mut display_start, &mut display_end, word);                    }                    break;                }                _ => break,            }        }    }     let start = display_start?;    let end = display_end?;    let display = command.get(start..end)?.to_string();     Some(TerminalCommandPrefix {        normalized: normalized_tokens.join(" "),        display,        tokens: normalized_tokens,        command: command_name,        subcommand,    })} pub fn validate_terminal_command(command: &str) -> TerminalCommandValidation {    let reader = BufReader::new(command.as_bytes());    let options = ParserOptions::default();    let source_info = SourceInfo::default();    let mut parser = Parser::new(reader, &options, &source_info);     let program = match parser.parse_program() {        Ok(program) => program,        Err(_) => return TerminalCommandValidation::Unsupported,    };     match program_validation(&program) {        TerminalProgramValidation::Safe => TerminalCommandValidation::Safe,        TerminalProgramValidation::Unsafe => TerminalCommandValidation::Unsafe,        TerminalProgramValidation::Unsupported => TerminalCommandValidation::Unsupported,    }} #[derive(Debug, Clone, Copy, PartialEq, Eq)]enum TerminalProgramValidation {    Safe,    Unsafe,    Unsupported,} fn first_simple_command(program: &ast::Program) -> Option<&ast::SimpleCommand> {    let complete_command = program.complete_commands.first()?;    let compound_list_item = complete_command.0.first()?;    let command = compound_list_item.0.first.seq.first()?;     match command {        ast::Command::Simple(simple_command) => Some(simple_command),        _ => None,    }} fn update_display_bounds(start: &mut Option<usize>, end: &mut Option<usize>, word: &ast::Word) {    if let Some(location) = word.location() {        let word_start = location.start.index;        let word_end = location.end.index;        *start = Some(start.map_or(word_start, |current| current.min(word_start)));        *end = Some(end.map_or(word_end, |current| current.max(word_end)));    }} enum NormalizedAssignment {    Included(String),    Skipped,} fn normalize_assignment_for_command_prefix(    assignment: &ast::Assignment,    word: &ast::Word,) -> Option<NormalizedAssignment> {    let operator = if assignment.append { "+=" } else { "=" };    let assignment_prefix = format!("{}{}", assignment.name, operator);     match &assignment.value {        ast::AssignmentValue::Scalar(value) => {            let normalized_value = normalize_word(value)?;            let raw_value = word.value.strip_prefix(&assignment_prefix)?;            let rendered_value = if shell_value_requires_quoting(&normalized_value) {                raw_value.to_string()            } else {                normalized_value            };             Some(NormalizedAssignment::Included(format!(                "{assignment_prefix}{rendered_value}"            )))        }        ast::AssignmentValue::Array(_) => Some(NormalizedAssignment::Skipped),    }} fn shell_value_requires_quoting(value: &str) -> bool {    value.chars().any(|character| {        character.is_whitespace()            || !matches!(                character,                'a'..='z'                    | 'A'..='Z'                    | '0'..='9'                    | '_'                    | '@'                    | '%'                    | '+'                    | '='                    | ':'                    | ','                    | '.'                    | '/'                    | '-'            )    })} fn program_validation(program: &ast::Program) -> TerminalProgramValidation {    combine_validations(        program            .complete_commands            .iter()            .map(compound_list_validation),    )} fn compound_list_validation(compound_list: &ast::CompoundList) -> TerminalProgramValidation {    combine_validations(        compound_list            .0            .iter()            .map(|item| and_or_list_validation(&item.0)),    )} fn and_or_list_validation(and_or_list: &ast::AndOrList) -> TerminalProgramValidation {    combine_validations(        std::iter::once(pipeline_validation(&and_or_list.first)).chain(            and_or_list.additional.iter().map(|and_or| match and_or {                ast::AndOr::And(pipeline) | ast::AndOr::Or(pipeline) => {                    pipeline_validation(pipeline)                }            }),        ),    )} fn pipeline_validation(pipeline: &ast::Pipeline) -> TerminalProgramValidation {    combine_validations(pipeline.seq.iter().map(command_validation))} fn command_validation(command: &ast::Command) -> TerminalProgramValidation {    match command {        ast::Command::Simple(simple_command) => simple_command_validation(simple_command),        ast::Command::Compound(compound_command, redirect_list) => combine_validations(            std::iter::once(compound_command_validation(compound_command))                .chain(redirect_list.iter().map(redirect_list_validation)),        ),        ast::Command::Function(function_definition) => {            function_body_validation(&function_definition.body)        }        ast::Command::ExtendedTest(test_expr) => extended_test_expr_validation(test_expr),    }} fn simple_command_validation(simple_command: &ast::SimpleCommand) -> TerminalProgramValidation {    combine_validations(        simple_command            .prefix            .iter()            .map(command_prefix_validation)            .chain(simple_command.word_or_name.iter().map(word_validation))            .chain(simple_command.suffix.iter().map(command_suffix_validation)),    )} fn command_prefix_validation(prefix: &ast::CommandPrefix) -> TerminalProgramValidation {    combine_validations(prefix.0.iter().map(prefix_or_suffix_item_validation))} fn command_suffix_validation(suffix: &ast::CommandSuffix) -> TerminalProgramValidation {    combine_validations(suffix.0.iter().map(prefix_or_suffix_item_validation))} fn prefix_or_suffix_item_validation(    item: &ast::CommandPrefixOrSuffixItem,) -> TerminalProgramValidation {    match item {        ast::CommandPrefixOrSuffixItem::IoRedirect(redirect) => io_redirect_validation(redirect),        ast::CommandPrefixOrSuffixItem::Word(word) => word_validation(word),        ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, word) => {            combine_validations([assignment_validation(assignment), word_validation(word)])        }        ast::CommandPrefixOrSuffixItem::ProcessSubstitution(_, _) => {            TerminalProgramValidation::Unsafe        }    }} fn io_redirect_validation(redirect: &ast::IoRedirect) -> TerminalProgramValidation {    match redirect {        ast::IoRedirect::File(_, _, target) => match target {            ast::IoFileRedirectTarget::Filename(word) => word_validation(word),            ast::IoFileRedirectTarget::ProcessSubstitution(_, _) => {                TerminalProgramValidation::Unsafe            }            _ => TerminalProgramValidation::Safe,        },        ast::IoRedirect::HereDocument(_, here_doc) => {            if here_doc.requires_expansion {                word_validation(&here_doc.doc)            } else {                TerminalProgramValidation::Safe            }        }        ast::IoRedirect::HereString(_, word) | ast::IoRedirect::OutputAndError(word, _) => {            word_validation(word)        }    }} fn assignment_validation(assignment: &ast::Assignment) -> TerminalProgramValidation {    match &assignment.value {        ast::AssignmentValue::Scalar(word) => word_validation(word),        ast::AssignmentValue::Array(words) => {            combine_validations(words.iter().flat_map(|(key, value)| {                key.iter()                    .map(word_validation)                    .chain(std::iter::once(word_validation(value)))            }))        }    }} fn word_validation(word: &ast::Word) -> TerminalProgramValidation {    let options = ParserOptions::default();    let pieces = match brush_parser::word::parse(&word.value, &options) {        Ok(pieces) => pieces,        Err(_) => return TerminalProgramValidation::Unsupported,    };     combine_validations(        pieces            .iter()            .map(|piece_with_source| word_piece_validation(&piece_with_source.piece)),    )} fn word_piece_validation(piece: &WordPiece) -> TerminalProgramValidation {    match piece {        WordPiece::Text(_)        | WordPiece::SingleQuotedText(_)        | WordPiece::AnsiCQuotedText(_)        | WordPiece::EscapeSequence(_)        | WordPiece::TildePrefix(_) => TerminalProgramValidation::Safe,        WordPiece::DoubleQuotedSequence(pieces)        | WordPiece::GettextDoubleQuotedSequence(pieces) => combine_validations(            pieces                .iter()                .map(|inner| word_piece_validation(&inner.piece)),        ),        WordPiece::ParameterExpansion(_) | WordPiece::ArithmeticExpression(_) => {            TerminalProgramValidation::Unsafe        }        WordPiece::CommandSubstitution(command)        | WordPiece::BackquotedCommandSubstitution(command) => {            let reader = BufReader::new(command.as_bytes());            let options = ParserOptions::default();            let source_info = SourceInfo::default();            let mut parser = Parser::new(reader, &options, &source_info);             match parser.parse_program() {                Ok(_) => TerminalProgramValidation::Unsafe,                Err(_) => TerminalProgramValidation::Unsupported,            }        }    }} fn compound_command_validation(    compound_command: &ast::CompoundCommand,) -> TerminalProgramValidation {    match compound_command {        ast::CompoundCommand::BraceGroup(brace_group) => {            compound_list_validation(&brace_group.list)        }        ast::CompoundCommand::Subshell(subshell) => compound_list_validation(&subshell.list),        ast::CompoundCommand::ForClause(for_clause) => combine_validations(            for_clause                .values                .iter()                .flat_map(|values| values.iter().map(word_validation))                .chain(std::iter::once(do_group_validation(&for_clause.body))),        ),        ast::CompoundCommand::CaseClause(case_clause) => combine_validations(            std::iter::once(word_validation(&case_clause.value))                .chain(                    case_clause                        .cases                        .iter()                        .flat_map(|item| item.cmd.iter().map(compound_list_validation)),                )                .chain(                    case_clause                        .cases                        .iter()                        .flat_map(|item| item.patterns.iter().map(word_validation)),                ),        ),        ast::CompoundCommand::IfClause(if_clause) => combine_validations(            std::iter::once(compound_list_validation(&if_clause.condition))                .chain(std::iter::once(compound_list_validation(&if_clause.then)))                .chain(if_clause.elses.iter().flat_map(|elses| {                    elses.iter().flat_map(|else_item| {                        else_item                            .condition                            .iter()                            .map(compound_list_validation)                            .chain(std::iter::once(compound_list_validation(&else_item.body)))                    })                })),        ),        ast::CompoundCommand::WhileClause(while_clause)        | ast::CompoundCommand::UntilClause(while_clause) => combine_validations([            compound_list_validation(&while_clause.0),            do_group_validation(&while_clause.1),        ]),        ast::CompoundCommand::ArithmeticForClause(_) => TerminalProgramValidation::Unsafe,        ast::CompoundCommand::Arithmetic(_) => TerminalProgramValidation::Unsafe,    }} fn do_group_validation(do_group: &ast::DoGroupCommand) -> TerminalProgramValidation {    compound_list_validation(&do_group.list)} fn function_body_validation(function_body: &ast::FunctionBody) -> TerminalProgramValidation {    combine_validations(        std::iter::once(compound_command_validation(&function_body.0))            .chain(function_body.1.iter().map(redirect_list_validation)),    )} fn redirect_list_validation(redirect_list: &ast::RedirectList) -> TerminalProgramValidation {    combine_validations(redirect_list.0.iter().map(io_redirect_validation))} fn extended_test_expr_validation(    test_expr: &ast::ExtendedTestExprCommand,) -> TerminalProgramValidation {    extended_test_expr_inner_validation(&test_expr.expr)} fn extended_test_expr_inner_validation(expr: &ast::ExtendedTestExpr) -> TerminalProgramValidation {    match expr {        ast::ExtendedTestExpr::Not(inner) | ast::ExtendedTestExpr::Parenthesized(inner) => {            extended_test_expr_inner_validation(inner)        }        ast::ExtendedTestExpr::And(left, right) | ast::ExtendedTestExpr::Or(left, right) => {            combine_validations([                extended_test_expr_inner_validation(left),                extended_test_expr_inner_validation(right),            ])        }        ast::ExtendedTestExpr::UnaryTest(_, word) => word_validation(word),        ast::ExtendedTestExpr::BinaryTest(_, left, right) => {            combine_validations([word_validation(left), word_validation(right)])        }    }} fn combine_validations(    validations: impl IntoIterator<Item = TerminalProgramValidation>,) -> TerminalProgramValidation {    let mut saw_unsafe = false;    let mut saw_unsupported = false;     for validation in validations {        match validation {            TerminalProgramValidation::Unsupported => saw_unsupported = true,            TerminalProgramValidation::Unsafe => saw_unsafe = true,            TerminalProgramValidation::Safe => {}        }    }     if saw_unsafe {        TerminalProgramValidation::Unsafe    } else if saw_unsupported {        TerminalProgramValidation::Unsupported    } else {        TerminalProgramValidation::Safe    }} fn extract_commands_from_program(program: &ast::Program, commands: &mut Vec<String>) -> Option<()> {    for complete_command in &program.complete_commands {        extract_commands_from_compound_list(complete_command, commands)?;    }    Some(())} fn extract_commands_from_compound_list(    compound_list: &ast::CompoundList,    commands: &mut Vec<String>,) -> Option<()> {    for item in &compound_list.0 {        extract_commands_from_and_or_list(&item.0, commands)?;    }    Some(())} fn extract_commands_from_and_or_list(    and_or_list: &ast::AndOrList,    commands: &mut Vec<String>,) -> Option<()> {    extract_commands_from_pipeline(&and_or_list.first, commands)?;     for and_or in &and_or_list.additional {        match and_or {            ast::AndOr::And(pipeline) | ast::AndOr::Or(pipeline) => {

Lines 1–500 of 1928.