pub mod download;
use std::borrow::Cow;
use std::collections::HashSet;
use std::error;
use std::fmt::{Display, Formatter, Result as FResult};
use std::fs::{self, File, Permissions};
use std::io::{BufRead as _, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, ChildStdout, Command, ExitStatus, Stdio};
use console::Style;
use log::{debug, info};
use tokio::fs::{self as tfs, File as TFile};
use tokio::io::{AsyncBufReadExt as _, AsyncReadExt, AsyncWriteExt as _, BufReader as TBufReader};
use tokio::process::{Child as TChild, ChildStdin as TChildStdin, ChildStdout as TChildStdout, Command as TCommand};
use crate::download::{DownloadSecurity, download_file, download_file_async};
const COMPILER_URL: &str = "https://github.com/Olaf-Erkemeij/eflint-server/raw/bd3997df89441f13cbc82bd114223646df41540d/eflint-to-json";
const COMPILER_CHECKSUM: [u8; 32] = hex_literal::hex!("4e4e59b158ca31e532ec0a22079951788696ffa5d020b36790b4461dbadec83d");
#[derive(Debug)]
pub struct ChildStreams(Vec<ChildStream>);
impl Display for ChildStreams {
fn fmt(&self, f: &mut Formatter<'_>) -> FResult {
for stream in &self.0 {
writeln!(f, "{stream}")?;
writeln!(f)?;
}
Ok(())
}
}
impl error::Error for ChildStreams {}
#[derive(Debug)]
pub struct ChildStream(&'static str, String);
impl ChildStream {
fn new(what: &'static str, mut stream: impl Read) -> Self {
let mut buf: String = String::new();
match stream.read_to_string(&mut buf) {
Ok(_) => Self(what, buf),
Err(err) => Self(what, format!("<failed to read stream: {err}>")),
}
}
async fn new_async(what: &'static str, mut stream: impl AsyncReadExt + Unpin) -> Self {
let mut buf: String = String::new();
match stream.read_to_string(&mut buf).await {
Ok(_) => Self(what, buf),
Err(err) => Self(what, format!("<failed to read stream: {err}>")),
}
}
}
impl Display for ChildStream {
fn fmt(&self, f: &mut Formatter<'_>) -> FResult {
writeln!(f, "{}:", self.0)?;
writeln!(f, "{}", (0..80).map(|_| '-').collect::<String>())?;
writeln!(f, "{}", self.1)?;
writeln!(f, "{}", (0..80).map(|_| '-').collect::<String>())?;
Ok(())
}
}
impl error::Error for ChildStream {}
#[derive(Debug)]
pub enum Error {
ChildFailed { cmd: String, status: ExitStatus, output: ChildStreams },
ChildRead { err: std::io::Error },
ChildWait { err: std::io::Error },
ChildWrite { err: std::io::Error },
CompilerDownload { from: String, to: PathBuf, err: Box<crate::download::Error> },
FileCreate { path: PathBuf, err: std::io::Error },
FileMetadata { path: PathBuf, err: std::io::Error },
FileOpen { path: PathBuf, err: std::io::Error },
FilePermissions { path: PathBuf, err: std::io::Error },
FileRead { path: PathBuf, err: std::io::Error },
IncludeOpen { parent: PathBuf, path: PathBuf, err: std::io::Error },
MissingQuote { parent: PathBuf, raw: String },
PathCanonicalize { parent: PathBuf, path: PathBuf, err: std::io::Error },
Spawn { cmd: String, err: std::io::Error },
WriterWrite { err: std::io::Error },
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> FResult {
use Error::*;
match self {
ChildFailed { cmd, status, .. } => write!(f, "Child process {cmd:?} failed with exit status {status}"),
ChildRead { .. } => write!(f, "Failed to read from child stdin"),
ChildWait { .. } => write!(f, "Failed to wait for child"),
ChildWrite { .. } => write!(f, "Failed to write to child stdin"),
CompilerDownload { from, to, .. } => write!(f, "Failed to download 'eflint-to-json' compiler from '{}' to '{}'", from, to.display()),
FileCreate { path, .. } => write!(f, "Failed to create output file '{}'", path.display()),
FileMetadata { path, .. } => write!(f, "Failed to get metadata of file '{}'", path.display()),
FileOpen { path, .. } => write!(f, "Failed to open input file '{}'", path.display()),
FilePermissions { path, .. } => write!(f, "Failed to set permissions of file '{}'", path.display()),
FileRead { path, .. } => write!(f, "Failed to read from input file '{}'", path.display()),
IncludeOpen { parent, path, .. } => write!(f, "Failed to open included file '{}' (in file '{}')", path.display(), parent.display()),
MissingQuote { parent, raw } => write!(f, "Missing quotes (\") in '{}' (in file '{}')", raw, parent.display()),
PathCanonicalize { parent, path, .. } => write!(f, "Failed to canonicalize path '{}' (in file '{}')", path.display(), parent.display()),
Spawn { cmd, .. } => write!(f, "Failed to spawn command {cmd:?}"),
WriterWrite { .. } => write!(f, "Failed to write to output writer"),
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
use Error::*;
match self {
ChildFailed { output, .. } => Some(output),
ChildRead { err, .. } => Some(err),
ChildWait { err, .. } => Some(err),
ChildWrite { err, .. } => Some(err),
CompilerDownload { err, .. } => Some(err),
FileCreate { err, .. } => Some(err),
FileMetadata { err, .. } => Some(err),
FileOpen { err, .. } => Some(err),
FilePermissions { err, .. } => Some(err),
FileRead { err, .. } => Some(err),
IncludeOpen { err, .. } => Some(err),
MissingQuote { .. } => None,
PathCanonicalize { err, .. } => Some(err),
Spawn { err, .. } => Some(err),
WriterWrite { err, .. } => Some(err),
}
}
}
fn potentially_include(imported: &mut HashSet<PathBuf>, path: &Path, line: &str) -> Result<Option<Option<(PathBuf, File)>>, Error> {
let line: &str = line.trim();
if line.len() < 8 || (&line[..8] != "#include" && &line[..8] != "#require") || line.chars().last().map(|c| c != '.').unwrap_or(true) {
return Ok(None);
}
let squote: usize = match line.find('"') {
Some(pos) => pos,
None => return Err(Error::MissingQuote { parent: path.into(), raw: line.into() }),
};
let equote: usize = match line.rfind('"') {
Some(pos) => pos,
None => return Err(Error::MissingQuote { parent: path.into(), raw: line.into() }),
};
let incl_path: PathBuf = PathBuf::from(&line[squote + 1..equote]);
let parent: Option<&Path> = path.parent();
#[allow(clippy::unnecessary_unwrap)]
let incl_path: PathBuf = if incl_path.is_absolute() || parent.is_none() { incl_path } else { parent.unwrap().join(incl_path) };
let incl_path: PathBuf = match incl_path.canonicalize() {
Ok(path) => path,
Err(err) => return Err(Error::PathCanonicalize { parent: path.into(), path: incl_path, err }),
};
if &line[..8] == "#require" && imported.contains(&incl_path) {
return Ok(Some(None));
}
imported.insert(incl_path.clone());
let handle: File = match File::open(&incl_path) {
Ok(handle) => handle,
Err(err) => return Err(Error::IncludeOpen { parent: path.into(), path: incl_path, err }),
};
Ok(Some(Some((incl_path, handle))))
}
async fn potentially_include_async(imported: &mut HashSet<PathBuf>, path: &Path, line: &str) -> Result<Option<Option<(PathBuf, TFile)>>, Error> {
let line: &str = line.trim();
if line.len() < 8 || (&line[..8] != "#include" && &line[..8] != "#require") || line.chars().last().map(|c| c != '.').unwrap_or(true) {
return Ok(None);
}
let squote: usize = match line.find('"') {
Some(pos) => pos,
None => return Err(Error::MissingQuote { parent: path.into(), raw: line.into() }),
};
let equote: usize = match line.rfind('"') {
Some(pos) => pos,
None => return Err(Error::MissingQuote { parent: path.into(), raw: line.into() }),
};
let incl_path: PathBuf = PathBuf::from(&line[squote + 1..equote]);
let parent: Option<&Path> = path.parent();
#[allow(clippy::unnecessary_unwrap)]
let incl_path: PathBuf = if incl_path.is_absolute() || parent.is_none() { incl_path } else { parent.unwrap().join(incl_path) };
let incl_path: PathBuf = match tfs::canonicalize(&incl_path).await {
Ok(path) => path,
Err(err) => return Err(Error::PathCanonicalize { parent: path.into(), path: incl_path, err }),
};
if &line[..8] == "#require" && imported.contains(&incl_path) {
return Ok(Some(None));
}
imported.insert(incl_path.clone());
let handle: TFile = match TFile::open(&incl_path).await {
Ok(handle) => handle,
Err(err) => return Err(Error::IncludeOpen { parent: path.into(), path: incl_path, err }),
};
Ok(Some(Some((incl_path, handle))))
}
fn load_input(imported: &mut HashSet<PathBuf>, path: &Path, handle: BufReader<File>, child: &mut ChildStdin) -> Result<(), Error> {
debug!("Importing file '{}'", path.display());
for line in handle.lines() {
let line: String = match line {
Ok(line) => line,
Err(err) => return Err(Error::FileRead { path: path.into(), err }),
};
match potentially_include(imported, path, &line)? {
Some(Some((child_path, child_handle))) => {
load_input(imported, &child_path, BufReader::new(child_handle), child)?;
},
Some(None) => {},
None => {
if let Err(err) = child.write_all(line.as_bytes()) {
return Err(Error::ChildWrite { err });
}
if let Err(err) = child.write_all(b"\n") {
return Err(Error::ChildWrite { err });
}
},
}
}
Ok(())
}
#[async_recursion::async_recursion]
async fn load_input_async(imported: &mut HashSet<PathBuf>, path: &Path, handle: TBufReader<TFile>, child: &mut TChildStdin) -> Result<(), Error> {
debug!("Importing file '{}'", path.display());
let mut lines = handle.lines();
while let Some(line) = lines.next_line().await.transpose() {
let line: String = match line {
Ok(line) => line,
Err(err) => return Err(Error::FileRead { path: path.into(), err }),
};
match potentially_include_async(imported, path, &line).await? {
Some(Some((child_path, child_handle))) => {
load_input_async(imported, &child_path, TBufReader::new(child_handle), child).await?;
},
Some(None) => {},
None => {
if let Err(err) = child.write_all(line.as_bytes()).await {
return Err(Error::ChildWrite { err });
}
if let Err(err) = child.write_all(b"\n").await {
return Err(Error::ChildWrite { err });
}
},
}
}
Ok(())
}
pub fn compile(input_path: &Path, mut output: impl Write, compiler_path: Option<&Path>) -> Result<(), Error> {
info!("Compiling input at '{}'", input_path.display());
let compiler_path: Cow<Path> = match compiler_path {
Some(path) => Cow::Borrowed(path),
None => {
let compiler_path: PathBuf = std::env::temp_dir().join("eflint-to-json");
if !compiler_path.exists() {
if let Err(err) = download_file(
COMPILER_URL,
&compiler_path,
DownloadSecurity { checksum: Some(&COMPILER_CHECKSUM), https: true },
Some(Style::new().bold().green()),
) {
return Err(Error::CompilerDownload { from: COMPILER_URL.into(), to: compiler_path, err: Box::new(err) });
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mut perms: Permissions = match fs::metadata(&compiler_path) {
Ok(mdata) => mdata.permissions(),
Err(err) => return Err(Error::FileMetadata { path: compiler_path, err }),
};
perms.set_mode(perms.mode() | 0o500);
if let Err(err) = fs::set_permissions(&compiler_path, perms) {
return Err(Error::FilePermissions { path: compiler_path, err });
}
}
}
Cow::Owned(compiler_path)
},
};
debug!("Using compiler at: '{}'", compiler_path.display());
debug!("Opening input file '{}'", input_path.display());
let input: File = match File::open(input_path) {
Ok(input) => input,
Err(err) => return Err(Error::FileOpen { path: input_path.into(), err }),
};
debug!("Spawning compiler '{}'", compiler_path.display());
let mut cmd: Command = Command::new(compiler_path.to_string_lossy().as_ref());
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut handle: Child = match cmd.spawn() {
Ok(handle) => handle,
Err(err) => return Err(Error::Spawn { cmd: format!("{cmd:?}"), err }),
};
debug!("Reading input to child process...");
let mut stdin: ChildStdin = handle.stdin.take().unwrap();
let mut included: HashSet<PathBuf> = HashSet::new();
load_input(&mut included, input_path, BufReader::new(input), &mut stdin)?;
drop(stdin);
debug!("Waiting for child process to complete...");
let status: ExitStatus = match handle.wait() {
Ok(status) => status,
Err(err) => return Err(Error::ChildWait { err }),
};
if !status.success() {
return Err(Error::ChildFailed {
cmd: format!("{cmd:?}"),
status,
output: ChildStreams(vec![
ChildStream::new("stdout", handle.stdout.take().unwrap()),
ChildStream::new("stderr", handle.stderr.take().unwrap()),
]),
});
}
debug!("Writing child process output to given output...");
let mut chunk: [u8; 65535] = [0; 65535];
let mut stdout: ChildStdout = handle.stdout.take().unwrap();
loop {
let chunk_len: usize = match stdout.read(&mut chunk) {
Ok(len) => len,
Err(err) => return Err(Error::ChildRead { err }),
};
if chunk_len == 0 {
break;
}
if let Err(err) = output.write_all(&chunk[..chunk_len]) {
return Err(Error::WriterWrite { err });
}
}
Ok(())
}
pub async fn compile_async(input_path: &Path, mut output: impl Write, compiler_path: Option<&Path>) -> Result<(), Error> {
info!("Compiling input at '{}'", input_path.display());
let compiler_path: Cow<Path> = match compiler_path {
Some(path) => Cow::Borrowed(path),
None => {
let compiler_path: PathBuf = std::env::temp_dir().join("eflint-to-json");
if !compiler_path.exists() {
if let Err(err) = download_file_async(
COMPILER_URL,
&compiler_path,
DownloadSecurity { checksum: Some(&COMPILER_CHECKSUM), https: true },
Some(Style::new().bold().green()),
)
.await
{
return Err(Error::CompilerDownload { from: COMPILER_URL.into(), to: compiler_path, err: Box::new(err) });
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mut perms: Permissions = match tfs::metadata(&compiler_path).await {
Ok(mdata) => mdata.permissions(),
Err(err) => return Err(Error::FileMetadata { path: compiler_path, err }),
};
perms.set_mode(perms.mode() | 0o500);
if let Err(err) = tfs::set_permissions(&compiler_path, perms).await {
return Err(Error::FilePermissions { path: compiler_path, err });
}
}
}
Cow::Owned(compiler_path)
},
};
debug!("Using compiler at: '{}'", compiler_path.display());
debug!("Opening input file '{}'", input_path.display());
let input: TFile = match TFile::open(input_path).await {
Ok(input) => input,
Err(err) => return Err(Error::FileOpen { path: input_path.into(), err }),
};
debug!("Spawning compiler '{}'", compiler_path.display());
let mut cmd: TCommand = TCommand::new(compiler_path.to_string_lossy().as_ref());
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut handle: TChild = match cmd.spawn() {
Ok(handle) => handle,
Err(err) => return Err(Error::Spawn { cmd: format!("{cmd:?}"), err }),
};
debug!("Reading input to child process...");
let mut stdin: TChildStdin = handle.stdin.take().unwrap();
let mut included: HashSet<PathBuf> = HashSet::new();
load_input_async(&mut included, input_path, TBufReader::new(input), &mut stdin).await?;
drop(stdin);
debug!("Waiting for child process to complete...");
let status: ExitStatus = match handle.wait().await {
Ok(status) => status,
Err(err) => return Err(Error::ChildWait { err }),
};
if !status.success() {
return Err(Error::ChildFailed {
cmd: format!("{cmd:?}"),
status,
output: ChildStreams(vec![
ChildStream::new_async("stdout", handle.stdout.take().unwrap()).await,
ChildStream::new_async("stderr", handle.stderr.take().unwrap()).await,
]),
});
}
debug!("Writing child process output to given output...");
let mut chunk: [u8; 65535] = [0; 65535];
let mut stdout: TChildStdout = handle.stdout.take().unwrap();
loop {
let chunk_len: usize = match stdout.read(&mut chunk).await {
Ok(len) => len,
Err(err) => return Err(Error::ChildRead { err }),
};
if chunk_len == 0 {
break;
}
if let Err(err) = output.write_all(&chunk[..chunk_len]) {
return Err(Error::WriterWrite { err });
}
}
Ok(())
}