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. Load more
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) => {