use std::ffi::OsStr;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub enum PreprocessError {
Io(std::io::Error),
RecursionLimit,
Other(String),
}
pub type Result<T> = std::result::Result<T, PreprocessError>;
macro_rules! debug {
($($arg:tt)*) => {
if env::var("CHOCOLATAL_DEBUG")
.map(|v| v == "1")
.unwrap_or(true) {
eprintln!("[chocolatal:debug] {}", format!($($arg)*));
}
}
}
pub fn preprocess(input: &str, path: &str, root_dir: &PathBuf) -> Result<String> {
let mut output = String::new();
let mut stack = Vec::new(); let mut lambda_counter = 0;
let mut token = String::new();
let mut sep = String::new();
let mut chars = input.chars().peekable();
let mut tokens = Vec::new();
while let Some(&c) = chars.peek() {
if c.is_whitespace() {
sep.push(c);
chars.next();
} else {
if !sep.is_empty() || !token.is_empty() {
if !token.is_empty() {
tokens.push((token.clone(), sep.clone()));
token.clear();
sep.clear();
} else if !sep.is_empty() {
tokens.push((String::new(), sep.clone()));
sep.clear();
}
}
if token.is_empty() && c == '~' {
let mut incl = String::new();
incl.push('~');
chars.next();
while let Some(&nc) = chars.peek() {
if nc.is_whitespace() {
break;
}
incl.push(nc);
chars.next();
}
tokens.push((incl, sep.clone()));
sep.clear();
continue;
}
while let Some(&nc) = chars.peek() {
if nc.is_whitespace() {
break;
}
token.push(nc);
chars.next();
}
tokens.push((token.clone(), sep.clone()));
token.clear();
sep.clear();
}
}
if !token.is_empty() || !sep.is_empty() {
tokens.push((token, sep));
}
let mut i = 0;
let mut prefix_stack: Vec<String> = Vec::new();
let mut current_prefix: String = String::new();
while i < tokens.len() {
let (ref tok, ref sep) = tokens[i];
if let Some(idx) = tok.find('~') {
let (prefix, rest) = tok.split_at(idx);
if !prefix.is_empty() && rest.len() > 1 {
let path_part = &rest[1..];
let label = std::path::Path::new(path_part)
.file_name()
.and_then(|f| f.to_str())
.map(|f| {
if f.ends_with(".tal") {
&f[..f.len() - 4]
} else {
f
}
})
.unwrap_or("");
let prefix_label = format!("{}{}", prefix, label);
prefix_stack.push(current_prefix.clone());
let input_dir = std::path::Path::new(path)
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| std::path::PathBuf::from("."));
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let path_part = path_part.trim_start_matches("./").trim_start_matches('/');
let incl_pattern_cwd = cwd.join(path_part);
let incl_pattern_cwd_str = incl_pattern_cwd.to_str().unwrap_or("");
if incl_pattern_cwd.exists() {
process_include_pattern(
incl_pattern_cwd_str,
&mut output,
sep,
&prefix_label,
root_dir,
)?;
} else {
let incl_pattern = input_dir.join(path_part);
let incl_pattern_str = incl_pattern.to_str().unwrap_or("");
process_include_pattern(
incl_pattern_str,
&mut output,
sep,
&prefix_label,
root_dir,
)?;
}
current_prefix = prefix_stack.pop().unwrap_or_default();
i += 1;
continue;
} else if prefix.is_empty() && rest.len() > 1 {
let path_part = &rest[1..];
let input_dir = std::path::Path::new(path)
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| std::path::PathBuf::from("."));
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let path_part = path_part.trim_start_matches("./").trim_start_matches('/');
let incl_pattern_cwd = cwd.join(path_part);
let incl_pattern_cwd_str = incl_pattern_cwd.to_str().unwrap_or("");
if incl_pattern_cwd.exists() {
process_include_pattern(
incl_pattern_cwd_str,
&mut output,
sep,
path_part,
root_dir,
)?;
} else {
let incl_pattern = input_dir.join(path_part);
let incl_pattern_str = incl_pattern.to_str().unwrap_or("");
process_include_pattern(
incl_pattern_str,
&mut output,
sep,
path_part,
root_dir,
)?;
}
i += 1;
continue;
}
}
if let Some(idx) = tok.find('~') {
let (prefix, rest) = tok.split_at(idx);
if !prefix.is_empty() && rest.len() > 1 {
let path_part = &rest[1..];
if path_part.trim().is_empty() {
return Err(PreprocessError::Other(format!(
"Syntax error at {}:{}: Empty include path\nSource: ~",
path,
input[..input.find('~').unwrap_or(0)].lines().count() + 1
)));
}
let label = Path::new(path_part)
.file_name()
.and_then(|f| f.to_str())
.map(|f| {
if f.ends_with(".tal") {
&f[..f.len() - 4]
} else {
f
}
})
.unwrap_or("");
let prefix_label = format!("{}{}", prefix, label);
let input_dir = Path::new(path)
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let path_part = path_part.trim_start_matches("./").trim_start_matches('/');
debug!(
"Prefix+include token: '{}', resolved path part: '{}', prefix_label: '{}'",
tok, path_part, prefix_label
);
let incl_pattern_cwd = cwd.join(path_part);
let incl_pattern_cwd_str = incl_pattern_cwd.to_str().unwrap_or("");
debug!(
"Including file(s) with pattern1: {}",
incl_pattern_cwd.display()
);
if incl_pattern_cwd.exists() {
process_include_pattern(
incl_pattern_cwd_str,
&mut output,
sep,
&prefix_label,
root_dir,
)?;
} else {
let incl_pattern = input_dir.join(path_part);
let incl_pattern_str = incl_pattern.to_str().unwrap_or("");
process_include_pattern(
incl_pattern_str,
&mut output,
sep,
&prefix_label,
root_dir,
)?;
}
i += 1;
continue;
}
}
if tok.starts_with('~') {
let path_part = &tok[1..];
if path_part.trim().is_empty() {
return Err(PreprocessError::Other(format!(
"Syntax error at {}:{}: Empty include path\nSource: ~",
path,
input[..input.find('~').unwrap_or(0)].lines().count() + 1
)));
}
let input_dir = Path::new(path)
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let path_part = path_part.trim_start_matches("./").trim_start_matches('/');
debug!(
"Include token: '{}', resolved path part: '{}'",
tok, path_part
);
let incl_pattern_cwd = cwd.join(path_part);
let incl_pattern_cwd_str = incl_pattern_cwd.to_str().unwrap_or("");
debug!(
"root:{} Including file(s) with pattern2: {}",
root_dir.display(), incl_pattern_cwd.display()
);
if incl_pattern_cwd.exists() {
process_include_pattern(
incl_pattern_cwd_str,
&mut output,
sep,
path_part,
root_dir,
)?;
} else {
let incl_pattern = input_dir.join(path_part);
let incl_pattern_str = incl_pattern.to_str().unwrap_or("");
process_include_pattern(
incl_pattern_str,
&mut output,
sep,
path_part,
root_dir,
)?;
}
i += 1;
continue;
}
if tok == ">{" {
lambda_counter += 1;
stack.push(lambda_counter);
debug!("Lambda open: ÊŽ{} (stack: {:?})", lambda_counter, stack);
output.push_str(&format!("&ÊŽ{}{}", lambda_counter, sep));
i += 1;
continue;
}
if (tok == "|}" || tok == "?}" || tok == "}") && !stack.is_empty() {
let n = stack.pop().unwrap();
debug!("Lambda close: ÊŽ{} (stack: {:?})", n, stack);
output.push_str(&format!("&ÊŽ{}{}", n, sep));
i += 1;
continue;
}
if tok == "@." {
let parent_name = Path::new(path)
.parent()
.and_then(|p| p.file_name())
.and_then(|s| s.to_str())
.unwrap_or("");
debug!(
"@. token found, rewriting to parent directory name: tok='{}', parent_name='{}'",
tok, parent_name
);
output.push_str(&format!("@{}{}", parent_name, sep));
i += 1;
continue;
}
if tok.starts_with("/.") {
output.push_str(&format!("STH2kr {}{}", &tok[2..], sep));
i += 1;
continue;
}
if let Some(idx) = tok.find("&&") {
let newtok = format!("{}&{}", &tok[..idx], &tok[idx + 2..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
if let Some(idx) = tok.find("/&") {
let newtok = format!("{}&{}{}", &tok[..idx], current_prefix, &tok[idx + 2..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
if let Some(idx) = tok.find('&') {
if idx == 0 && tok.len() > 1 {
let newtok = format!("&{}{}", current_prefix, &tok[idx + 1..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
}
if let Some(idx) = tok.find("//") {
let newtok = format!("{}/{}", &tok[..idx], &tok[idx + 2..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
if let Some(idx) = tok.find('/') {
if idx > 0 && !tok[idx + 1..].starts_with('.') {
let newtok = format!("{}/{}{}", &tok[..idx], current_prefix, &tok[idx + 1..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
}
if tok.starts_with('/') && tok.len() > 1 && !tok[1..].starts_with('.') {
let newtok = format!("/{}{}", current_prefix, &tok[1..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
if let Some(idx) = tok.find("&&") {
let newtok = format!("{}&{}", &tok[..idx], &tok[idx + 2..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
if let Some(idx) = tok.find("/&") {
let prefix = ""; let newtok = format!("{}&{}{}", &tok[..idx], prefix, &tok[idx + 2..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
if let Some(idx) = tok.find('&') {
if idx == 0 && tok.len() > 1 {
let prefix = ""; let newtok = format!("&{}{}", prefix, &tok[idx + 1..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
}
if let Some(idx) = tok.find("//") {
let newtok = format!("{}/{}", &tok[..idx], &tok[idx + 2..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
if let Some(idx) = tok.find('/') {
if idx > 0 && !tok[idx + 1..].starts_with('.') {
let prefix = ""; let newtok = format!("{}/{}{}", &tok[..idx], prefix, &tok[idx + 1..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
}
if tok.starts_with('/') && tok.len() > 1 && !tok[1..].starts_with('.') {
let prefix = ""; let newtok = format!("/{}{}", prefix, &tok[1..]);
output.push_str(&format!("{}{}", newtok, sep));
i += 1;
continue;
}
output.push_str(tok);
output.push_str(sep);
i += 1;
}
Ok(output)
}
fn process_include_pattern(
incl_pattern_str: &str,
output: &mut String,
sep: &String,
_path_part: &str,
root_dir: &PathBuf,
) -> Result<()> {
let input_path = Path::new(incl_pattern_str).to_path_buf();
debug!("root: {} Including file(s) with pattern3: {}", root_dir.display(), incl_pattern_str);
if incl_pattern_str.contains('*')
|| incl_pattern_str.contains('?')
|| incl_pattern_str.contains('[')
{
let (dir, file_pat) = match incl_pattern_str.rfind(std::path::MAIN_SEPARATOR) {
Some(idx) => (&incl_pattern_str[..=idx], &incl_pattern_str[idx + 1..]),
None => ("./", incl_pattern_str),
};
let dir_trimmed = if dir.ends_with(std::path::MAIN_SEPARATOR) && dir.len() > 1 {
&dir[..dir.len() - 1]
} else {
dir
};
let dir_path = Path::new(dir_trimmed);
let read_dir = match fs::read_dir(dir_path) {
Ok(rd) => rd,
Err(e) => {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let file_pat_path = cwd.join(file_pat);
if file_pat_path.exists() && file_pat_path.is_file() {
debug!("Including file (from cwd): {}", file_pat_path.display());
if file_pat_path.extension().and_then(OsStr::to_str) == Some("tal") {
let incl_pre = preprocess_include_file(&cwd, &file_pat_path)?;
output.push_str(&incl_pre);
output.push_str(sep);
} else {
hexdump(output, sep, file_pat_path)?;
}
return Ok(());
}
return Err(PreprocessError::Other(format!(
"Failed to read directory '{}': {}",
dir_path.display(),
e
)));
}
};
Ok(for entry in read_dir {
match entry {
Ok(entry) => {
let path = entry.path();
let fname = entry.file_name();
let fname_str = fname.to_str().unwrap_or("");
if matches_pattern(fname_str, file_pat) && path.is_file() {
debug!("Including file: {}", path.display());
if path.extension().and_then(OsStr::to_str) == Some("tal") {
let incl_pre = match preprocess_include_file(&PathBuf::from("."), &path)
{
Ok(s) => s,
Err(_) => match preprocess_include_file(&input_path, &path) {
Ok(s) => s,
Err(_) => preprocess_include_file(root_dir, &path)?,
}
};
output.push_str(&incl_pre);
output.push_str(sep);
} else {
hexdump(output, sep, input_path.clone())?;
}
}
}
Err(e) => {
debug!("Glob error: {}", e);
return Err(PreprocessError::Other(e.to_string()));
}
}
})
} else {
process_single_file(incl_pattern_str, output, sep, input_path, &root_dir)?;
Ok(())
}
}
fn process_single_file(
incl_pattern_str: &str,
output: &mut String,
sep: &String,
input_path: PathBuf,
root_dir: &PathBuf,
) -> Result<()> {
let debug_enabled = env::var("CHOCOLATAL_DEBUG")
.map(|v| v == "1")
.unwrap_or(true);
macro_rules! debug {
($($arg:tt)*) => {
if debug_enabled {
eprintln!("[chocolatal:debug] {}", format!($($arg)*));
}
}
}
debug!("Including file: {}", incl_pattern_str);
Ok(
if input_path.extension().map(|e| e == "tal").unwrap_or(false) {
let filename = input_path.file_name().map(|f| f.to_os_string());
let mut tried_paths = Vec::new();
let mut try_path = input_path.clone();
let incl_str = loop {
match fs::read_to_string(&try_path) {
Ok(s) => break s,
Err(e) => {
if let Some(ref fname) = filename {
if !tried_paths.contains(&root_dir.join(fname)) {
let root_path = root_dir.join(fname);
if !tried_paths.contains(&root_path) && root_path != try_path {
debug!("Trying filename in root_dir fallback: {}", root_path.display());
debug!("(was trying: {})", try_path.display());
try_path = root_dir.join(try_path.file_name().unwrap_or(&OsStr::new("")));
debug!("(was trying: {})", try_path.display());
continue;
}
}
}
tried_paths.push(try_path.clone());
if let Some(fname) = &filename {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let mut tried_cwd = false;
loop {
let mut parent_opt = match try_path.parent() {
Some(parent) => Some(parent.to_path_buf()),
None => {
if try_path.is_dir() {
Some(try_path.clone())
} else {
None
}
}
};
if let Some(ref parent) = parent_opt {
debug!("Trying parent directory: {}", parent.display());
if parent.display().to_string().is_empty() {
debug!("Reached root directory (empty path).");
parent_opt = Some(root_dir.clone());
}
} else {
debug!("No parent directory found.");
}
if let Some(parent) = parent_opt {
if parent == try_path || tried_paths.contains(&parent) {
break;
}
let parent_path = parent.join(fname);
if tried_paths.contains(&parent_path) {
break;
}
debug!("Trying parent directory: {}", parent_path.display());
try_path = parent_path.clone();
tried_paths.push(try_path.clone());
if parent.canonicalize().ok() == cwd.canonicalize().ok() {
tried_cwd = true;
break;
}
continue;
} else {
eprintln!("Reached root directory without finding file.");
break;
}
}
if !tried_cwd {
let cwd_path = cwd.join(fname);
if !tried_paths.contains(&cwd_path) && cwd_path != try_path {
debug!("Trying filename in cwd last resort: {}", cwd_path.display());
try_path = cwd_path;
continue;
}
}
}
return Err(PreprocessError::Other(format!(
"chocolatal::Failed to read file '{}': {}",
input_path.display(),
e
)));
}
}
};
let incl_pre = preprocess(&incl_str, input_path.to_str().unwrap_or(""), &root_dir)?;
let incl_pre = incl_pre.trim_end();
output.push_str(&incl_pre);
output.push_str(sep);
} else {
hexdump(output, sep, input_path)?;
},
)
}
fn hexdump(output: &mut String, sep: &String, input_path: PathBuf) -> Result<()> {
let bytes = match fs::read(&input_path) {
Ok(b) => b,
Err(e) => {
return Err(PreprocessError::Other(format!(
"Failed to read file '{}': {}",
input_path.display(),
e
)));
}
};
let mut line = String::new();
for chunk in bytes.chunks(16) {
line.clear();
for (j, group) in chunk.chunks(2).enumerate() {
if j > 0 {
line.push(' ');
}
for b in group {
line.push_str(&format!("{:02x}", b));
}
}
output.push_str(&line);
output.push('\n');
}
output.push_str(sep);
Ok(())
}
fn preprocess_include_file(current_dir: &PathBuf, path: &PathBuf) -> Result<String> {
let incl_str = match fs::read_to_string(&path) { Ok(s) => s,
Err(e) => {
return Err(PreprocessError::Other(format!(
"chocolatal: Failed to read file '{}': {}",
path.display(), e
)));
}
};
let incl_str = incl_str.trim_end();
let incl_pre = preprocess(&incl_str, &path.display().to_string(), current_dir)?;
Ok(incl_pre)
}
pub fn deluge_preprocess(_input_path: &str) -> std::io::Result<String> {
use std::env;
use std::process::Command;
let cwd = env::current_dir()?;
let volume = format!("{}:/workspace", cwd.display());
let args = [
"run",
"--rm",
"-v",
&volume,
"-w",
"/workspace",
"alpine",
"sh",
"./preprocess-tal.sh",
"deluge/main.tal",
];
eprintln!(
"[deluge_preprocess:debug] Running: docker {:?}\n cwd: {}",
args,
cwd.display()
);
let output = Command::new("docker").args(&args).output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn matches_pattern(name: &str, pattern: &str) -> bool {
let (mut np, mut pp) = (0, 0);
let (nlen, plen) = (name.len(), pattern.len());
let (name, pattern) = (name.as_bytes(), pattern.as_bytes());
let mut star = None;
while np < nlen {
if pp < plen && (pattern[pp] == b'?' || pattern[pp] == name[np]) {
np += 1;
pp += 1;
} else if pp < plen && pattern[pp] == b'*' {
star = Some((np, pp));
pp += 1;
} else if let Some((snp, spp)) = star {
np = snp + 1;
pp = spp + 1;
star = Some((np, spp));
} else {
return false;
}
}
while pp < plen && pattern[pp] == b'*' {
pp += 1;
}
pp == plen
}
#[cfg(not(test))]
#[allow(dead_code)]
fn main() {
use std::env;
use std::fs;
use std::io::{self, Read, Write};
use std::process;
let root_dir = &std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!(
"Usage: {} <input1.tal> [input2.tal ...] [output.tal|-]",
args[0]
);
eprintln!(" If no input files or '-' is given, reads from stdin. If no output is given or '-' is given, writes to stdout.");
process::exit(1);
}
let (input_files, output_path): (Vec<&str>, Option<&str>) = {
if args.len() == 2 {
if args[1] == "-" {
(vec!["-"], None)
} else if args[1].ends_with(".tal") {
(vec![&args[1]], None)
} else {
(vec!["-"], Some(&args[1]))
}
} else {
let last = &args[args.len() - 1];
if last == "-" || last.ends_with(".tal") {
(args[1..].iter().map(|s| s.as_str()).collect(), None)
} else {
(
args[1..args.len() - 1].iter().map(|s| s.as_str()).collect(),
Some(last.as_str()),
)
}
}
};
let mut input = String::new();
let mut input_path = String::new();
for (i, file) in input_files.iter().enumerate() {
let (content, path) = if *file == "-" {
let mut buf = String::new();
if let Err(e) = io::stdin().read_to_string(&mut buf) {
eprintln!("Failed to read from stdin: {}", e);
process::exit(1);
}
(buf, "<stdin>".to_string())
} else {
let content = match fs::read_to_string(file) {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to read {}: {}", file, e);
process::exit(1);
}
};
(content, file.to_string())
};
if i > 0 {
input.push_str("\n"); }
input.push_str(&content);
if input_path.is_empty() {
input_path = path;
}
}
match preprocess(&input, &input_path, root_dir) {
Ok(result) => {
match output_path {
Some(out) if out != "-" => {
if let Err(e) = fs::write(out, &result) {
eprintln!("Failed to write {}: {}", out, e);
process::exit(1);
}
}
_ => {
if let Err(e) = io::stdout().write_all(result.as_bytes()) {
eprintln!("Failed to write to stdout: {}", e);
process::exit(1);
}
}
}
}
Err(e) => {
eprintln!("Preprocess error: {:?}", e);
process::exit(1);
}
}
}