#[cfg(test)]
mod tests;
use std::collections::HashMap;
use std::error;
use std::fmt;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::string::FromUtf8Error;
#[derive(Default)]
pub struct Context {
pub macros: HashMap<String, String>,
pub inactive_stack: u32,
pub used_if: bool,
pub allow_exec: bool,
pub in_stack: Vec<Child>,
}
impl Context {
pub fn new() -> Context {
Context {
macros: HashMap::new(),
inactive_stack: 0,
used_if: false,
allow_exec: false,
in_stack: Vec::new(),
}
}
pub fn new_exec() -> Context {
Context {
macros: HashMap::new(),
inactive_stack: 0,
used_if: false,
allow_exec: true,
in_stack: Vec::new(),
}
}
pub fn from_map(macros: HashMap<String, String>) -> Context {
Context {
macros,
inactive_stack: 0,
used_if: false,
allow_exec: false,
in_stack: Vec::new(),
}
}
pub fn from_vec(macros: Vec<(&str, &str)>) -> Context {
Context {
macros: macros
.into_iter()
.map(|(name, value)| (name.to_owned(), value.to_owned()))
.collect(),
inactive_stack: 0,
used_if: false,
allow_exec: false,
in_stack: Vec::new(),
}
}
}
#[derive(Debug)]
pub enum Error {
InvalidCommand { command_name: String },
TooManyParameters { command: &'static str },
UnexpectedCommand { command: &'static str },
ChildFailed { status: ExitStatus },
PipeFailed,
IoError(io::Error),
FromUtf8Error(FromUtf8Error),
FileError {
filename: String,
line: usize,
error: Box<Error>,
},
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidCommand { command_name } => {
write!(f, "Invalid command '{}'", command_name)
}
Error::TooManyParameters { command } => {
write!(f, "Too many parameters for #{}", command)
}
Error::UnexpectedCommand { command } => write!(f, "Unexpected command #{}", command),
Error::ChildFailed { status } => write!(f, "Child failed with exit code {}", status),
Error::PipeFailed => write!(f, "Pipe to child failed"),
Error::IoError(e) => write!(f, "I/O Error: {}", e),
Error::FromUtf8Error(e) => write!(f, "UTF-8 Error: {}", e),
Error::FileError {
filename,
line,
error,
} => write!(f, "Error in {}:{}: {}", filename, line, error),
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Error::IoError(e) => Some(e),
Error::FromUtf8Error(e) => Some(e),
Error::FileError { error: e, .. } => Some(e),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::IoError(e)
}
}
impl From<FromUtf8Error> for Error {
fn from(e: FromUtf8Error) -> Self {
Error::FromUtf8Error(e)
}
}
fn shell(cmd: &str) -> Command {
let mut command;
if cfg!(target_os = "windows") {
command = Command::new("cmd");
command.args(&["/C", cmd]);
} else {
command = Command::new("sh");
command.args(&["-c", cmd]);
}
command
}
fn process_exec(line: &str, _: &mut Context) -> Result<String, Error> {
let output = shell(line).output()?;
if !output.status.success() {
return Err(Error::ChildFailed {
status: output.status,
});
}
Ok(String::from_utf8(output.stdout)?)
}
fn process_in(line: &str, context: &mut Context) -> Result<String, Error> {
let child = shell(line)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
context.in_stack.push(child);
Ok(String::new())
}
fn process_endin(line: &str, context: &mut Context) -> Result<String, Error> {
if !line.is_empty() {
return Err(Error::TooManyParameters { command: "endin" });
}
if context.in_stack.is_empty() {
return Err(Error::UnexpectedCommand { command: "endin" });
}
let child = context.in_stack.pop().unwrap();
let output = child.wait_with_output()?;
if !output.status.success() {
return Err(Error::ChildFailed {
status: output.status,
});
}
Ok(String::from_utf8(output.stdout)?)
}
fn process_include(line: &str, context: &mut Context) -> Result<String, Error> {
process_file(line, context)
}
fn process_define(line: &str, context: &mut Context) -> Result<String, Error> {
let (name, value) = match line.find(' ') {
Some(index) => line.split_at(index),
None => (line, " "),
};
let value = &value[1..];
context
.macros
.insert(String::from(name), String::from(value));
Ok(String::new())
}
fn process_undef(line: &str, context: &mut Context) -> Result<String, Error> {
context.macros.remove(line);
Ok(String::new())
}
fn process_ifdef(line: &str, context: &mut Context, inverted: bool) -> Result<String, Error> {
if context.inactive_stack > 0 {
context.inactive_stack += 1;
} else if context.macros.contains_key(line) == inverted {
context.inactive_stack = 1;
context.used_if = false;
} else {
context.used_if = true;
}
Ok(String::new())
}
fn process_elifdef(line: &str, context: &mut Context, inverted: bool) -> Result<String, Error> {
if context.inactive_stack == 0 {
context.inactive_stack = 1;
} else if context.inactive_stack == 1
&& !context.used_if
&& context.macros.contains_key(line) != inverted
{
context.inactive_stack = 0;
}
Ok(String::new())
}
fn process_else(line: &str, context: &mut Context) -> Result<String, Error> {
if !line.is_empty() {
return Err(Error::TooManyParameters { command: "else" });
}
context.inactive_stack = match context.inactive_stack {
0 => 1,
1 if !context.used_if => 0,
val => val,
};
Ok(String::new())
}
fn process_endif(line: &str, context: &mut Context) -> Result<String, Error> {
if !line.is_empty() {
return Err(Error::TooManyParameters { command: "endif" });
}
if context.inactive_stack != 0 {
context.inactive_stack -= 1;
}
Ok(String::new())
}
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn is_word(s: &str, pos: usize, len: usize) -> bool {
let mut prev_char = pos;
if prev_char != 0 {
prev_char -= 1;
while !s.is_char_boundary(prev_char) {
prev_char -= 1;
}
}
if pos > 0 && is_word_char(s[prev_char..pos].chars().next().unwrap()) {
return false;
}
if pos + len < s.len() && is_word_char(s[pos + len..].chars().next().unwrap()) {
return false;
}
true
}
fn replace_next_macro(line: &str, macros: &HashMap<String, String>) -> Option<String> {
for (name, value) in macros {
let index = match line.find(name) {
Some(i) => i,
None => continue,
};
if !is_word(line, index, name.len()) {
continue;
}
let mut new_line = String::new();
new_line.reserve(line.len() - name.len() + value.len());
new_line.push_str(&line[..index]);
new_line.push_str(value);
new_line.push_str(&line[index + name.len()..]);
return Some(new_line);
}
None
}
pub fn process_line(line: &str, context: &mut Context) -> Result<String, Error> {
let mut chars = line.chars();
let first = chars.next();
let second = chars.next();
let line = if first == Some('#') && second != Some('#') {
let after_hash = line[1..].trim_start();
let (command, content) = match after_hash.find(' ') {
Some(index) => after_hash.split_at(index),
None => (after_hash, ""),
};
let content = content.trim_start();
match command {
"exec" if context.inactive_stack == 0 && context.allow_exec => {
process_exec(content, context)
}
"in" if context.inactive_stack == 0 && context.allow_exec => {
process_in(content, context)
}
"endin" if context.inactive_stack == 0 && context.allow_exec => {
process_endin(content, context)
}
"include" if context.inactive_stack == 0 => process_include(content, context),
"define" if context.inactive_stack == 0 => process_define(content, context),
"undef" if context.inactive_stack == 0 => process_undef(content, context),
"ifdef" => process_ifdef(content, context, false),
"ifndef" => process_ifdef(content, context, true),
"elifdef" => process_elifdef(content, context, false),
"elifndef" => process_elifdef(content, context, true),
"else" => process_else(content, context),
"endif" => process_endif(content, context),
command => Err(Error::InvalidCommand {
command_name: command.to_owned(),
}),
}?
} else {
if context.inactive_stack > 0 {
return Ok(String::new());
}
let mut line = String::from(if first == Some('#') && second == Some('#') {
&line[1..]
} else {
line
});
while let Some(s) = replace_next_macro(&line, &context.macros) {
line = s;
}
if line.chars().rev().next() != Some('\n') {
line.push('\n');
}
line
};
if let Some(child) = context.in_stack.last_mut() {
let input = child.stdin.as_mut().ok_or(Error::PipeFailed)?;
input.write_all(line.as_bytes())?;
Ok(String::new())
} else {
Ok(line)
}
}
pub fn process_str(s: &str, context: &mut Context) -> Result<String, Error> {
process_buf(s.as_bytes(), "<string>", context)
}
pub fn process_file(filename: &str, context: &mut Context) -> Result<String, Error> {
let file_raw = File::open(filename)?;
let file = BufReader::new(file_raw);
process_buf(file, filename, context)
}
pub fn process_buf<T: BufRead>(
buf: T,
buf_name: &str,
context: &mut Context,
) -> Result<String, Error> {
let mut result = String::new();
for (num, line) in buf.lines().enumerate() {
let line = line?;
let result_line = process_line(&line, context).map_err(|e| Error::FileError {
filename: String::from(buf_name),
line: num,
error: Box::new(e),
})?;
result.push_str(&result_line);
}
Ok(result)
}