#[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 as SystemCommand, ExitStatus, Stdio};
use std::string::FromUtf8Error;
#[derive(Debug, 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() -> Self {
Self::default()
}
pub fn new_exec() -> Self {
Self::new().exec(true)
}
pub fn from_macros(macros: impl Into<HashMap<String, String>>) -> Self {
Self {
macros: macros.into(),
..Default::default()
}
}
pub fn from_macros_iter(macros: impl IntoIterator<Item = (String, String)>) -> Self {
Self::from_macros(macros.into_iter().collect::<HashMap<_, _>>())
}
pub fn exec(mut self, allow_exec: bool) -> Self {
self.allow_exec = allow_exec;
self
}
}
#[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) -> SystemCommand {
let (shell, flag) = if cfg!(target_os = "windows") {
("cmd", "/C")
} else {
("/bin/sh", "-c")
};
let mut command = SystemCommand::new(shell);
command.args(&[flag, 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 mut parts = line.splitn(2, ' ');
let name = parts.next().unwrap();
let value = parts.next().unwrap_or("");
context.macros.insert(name.to_owned(), value.to_owned());
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())
}
#[derive(Clone, Copy)]
struct Command {
name: &'static str,
requires_exec: bool,
ignored_by_if: bool,
execute: fn(&str, &mut Context) -> Result<String, Error>,
}
const COMMANDS: &[Command] = &[
Command {
name: "exec",
requires_exec: true,
ignored_by_if: false,
execute: process_exec,
},
Command {
name: "in",
requires_exec: true,
ignored_by_if: false,
execute: process_in,
},
Command {
name: "endin",
requires_exec: true,
ignored_by_if: false,
execute: process_endin,
},
Command {
name: "include",
requires_exec: false,
ignored_by_if: false,
execute: process_include,
},
Command {
name: "define",
requires_exec: false,
ignored_by_if: false,
execute: process_define,
},
Command {
name: "undef",
requires_exec: false,
ignored_by_if: false,
execute: process_undef,
},
Command {
name: "ifdef",
requires_exec: false,
ignored_by_if: true,
execute: |line, context| process_ifdef(line, context, false),
},
Command {
name: "ifndef",
requires_exec: false,
ignored_by_if: true,
execute: |line, context| process_ifdef(line, context, true),
},
Command {
name: "elifdef",
requires_exec: false,
ignored_by_if: true,
execute: |line, context| process_elifdef(line, context, false),
},
Command {
name: "elifndef",
requires_exec: false,
ignored_by_if: true,
execute: |line, context| process_elifdef(line, context, true),
},
Command {
name: "else",
requires_exec: false,
ignored_by_if: true,
execute: process_else,
},
Command {
name: "endif",
requires_exec: false,
ignored_by_if: true,
execute: process_endif,
},
];
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn replace_next_macro(line: &str, macros: &HashMap<String, String>) -> Option<String> {
macros.iter().find_map(|(name, value)| {
let mut parts = line.splitn(2, name);
let before = parts.next().unwrap();
let after = parts.next()?;
if before.chars().next_back().map_or(false, is_word_char)
|| after.chars().next().map_or(false, is_word_char)
{
return None;
}
let mut new_line = String::with_capacity(before.len() + value.len() + after.len());
new_line.push_str(before);
new_line.push_str(value);
new_line.push_str(after);
Some(new_line)
})
}
pub fn process_line(line: &str, context: &mut Context) -> Result<String, Error> {
let line = line
.strip_suffix("\r\n")
.or_else(|| line.strip_suffix('\n'))
.unwrap_or(line);
enum Line<'a> {
Text(&'a str),
Command(Command, &'a str),
}
let line = if let Some(rest) = line.strip_prefix('#') {
if rest.starts_with('#') {
Line::Text(rest)
} else {
let mut parts = rest.trim_start().splitn(2, ' ');
let command_name = parts.next().unwrap();
let content = parts.next().unwrap_or("").trim_start();
Line::Command(
COMMANDS
.iter()
.copied()
.filter(|command| context.allow_exec || !command.requires_exec)
.find(|command| command.name == command_name)
.ok_or_else(|| Error::InvalidCommand {
command_name: command_name.to_owned(),
})?,
content,
)
}
} else {
Line::Text(line)
};
let line = match line {
Line::Text(_)
| Line::Command(
Command {
ignored_by_if: false,
..
},
_,
) if context.inactive_stack > 0 => String::new(),
Line::Text(text) => {
let mut line = format!("{}\n", text);
while let Some(s) = replace_next_macro(&line, &context.macros) {
line = s;
}
line
}
Line::Command(command, content) => (command.execute)(content, context)?,
};
Ok(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())?;
String::new()
} else {
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> {
buf.lines()
.enumerate()
.map(|(num, line)| {
Ok({
process_line(&line?, context).map_err(|e| Error::FileError {
filename: String::from(buf_name),
line: num,
error: Box::new(e),
})?
})
})
.collect()
}