use std::io::{self, Read};
use std::path::Path;
use assura_config::{CompilerConfig, ProjectConfig};
pub(crate) fn load_project_config(
start_path: &Path,
) -> Option<(ProjectConfig, std::path::PathBuf)> {
assura_config::load_project_config(start_path, assura_resolve::find_project_root)
}
pub(crate) const MAX_SOURCE_BYTES: u64 = 16 * 1024 * 1024;
fn source_too_large(max: u64) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!("source exceeds maximum size of {max} bytes"),
)
}
pub(crate) fn read_source_limited(path: impl AsRef<Path>, max: u64) -> io::Result<String> {
let file = std::fs::File::open(path)?;
let len = file.metadata()?.len();
if len > max {
return Err(source_too_large(max));
}
let mut buf = String::new();
file.take(max.saturating_add(1)).read_to_string(&mut buf)?;
if buf.len() as u64 > max {
return Err(source_too_large(max));
}
Ok(buf)
}
pub(crate) fn read_source_arg(path: &str) -> io::Result<(String, String)> {
if path == "-" {
let mut buf = String::new();
io::stdin()
.take(MAX_SOURCE_BYTES.saturating_add(1))
.read_to_string(&mut buf)?;
if buf.len() as u64 > MAX_SOURCE_BYTES {
return Err(source_too_large(MAX_SOURCE_BYTES));
}
Ok((buf, "<stdin>".to_string()))
} else {
let source = read_source_limited(path, MAX_SOURCE_BYTES)?;
Ok((source, path.to_string()))
}
}
pub(crate) fn is_stdin_arg(path: &str) -> bool {
path == "-"
}
pub(crate) fn validate_human_json_format(format: &str, cmd: &str, as_json: bool) {
match format {
"human" | "json" => {}
other => {
if as_json {
let report = serde_json::json!({
"ok": false,
"command": cmd,
"error": "invalid_format",
"format": other,
"message": format!(
"invalid --format '{other}' for {cmd} (expected human or json)"
),
});
println!("{}", serde_json::to_string_pretty(&report).unwrap());
} else {
eprintln!("Error: invalid --format '{other}' for {cmd} (expected human or json)");
}
std::process::exit(2);
}
}
}
pub(crate) type CompilationResult = assura_pipeline::CompilationOutput;
pub(crate) fn format_counterexample_summary(
counter_model: &Option<assura_smt::CounterexampleModel>,
raw_model: &str,
) -> String {
let lines = assura_smt::display::format_counterexample_lines(counter_model, raw_model);
let pairs: Vec<&str> = lines
.iter()
.map(|l| l.strip_prefix("| ").unwrap_or(l.as_str()))
.collect();
if pairs.is_empty() {
return "counterexample found".to_string();
}
format!("counterexample: {}", pairs.join("; "))
}
pub(crate) fn load_project_deps(
project_dir: &Path,
) -> (
std::path::PathBuf,
assura_resolve::DependencyMap,
Vec<String>,
) {
let project_root = if project_dir.join("assura.toml").exists() {
project_dir.to_path_buf()
} else {
assura_resolve::find_project_root(project_dir).unwrap_or_else(|| project_dir.to_path_buf())
};
let config = load_project_config(&project_root);
let (dep_map, dep_warnings) = if let Some((ref cfg, ref root)) = config {
assura_resolve::resolve_dependency_map(root, cfg)
} else {
(assura_resolve::DependencyMap::new(), vec![])
};
(project_root, dep_map, dep_warnings)
}
pub(crate) fn compile(source: &str, filename: &str) -> CompilationResult {
assura_pipeline::compile(source, filename, &CompilerConfig::default())
}
pub(crate) fn compile_with_config(
source: &str,
filename: &str,
config: &CompilerConfig,
) -> CompilationResult {
assura_pipeline::compile(source, filename, config)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn max_source_bytes_is_16_mib() {
assert_eq!(MAX_SOURCE_BYTES, 16 * 1024 * 1024);
}
#[test]
fn read_source_limited_ok_under_cap() {
let mut f = NamedTempFile::new().unwrap();
write!(f, "hello").unwrap();
f.flush().unwrap();
let s = read_source_limited(f.path(), 16).unwrap();
assert_eq!(s, "hello");
}
#[test]
fn read_source_limited_rejects_over_cap() {
let mut f = NamedTempFile::new().unwrap();
write!(f, "hello world").unwrap();
f.flush().unwrap();
let err = read_source_limited(f.path(), 4).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
let msg = err.to_string();
assert!(
msg.contains("maximum size"),
"expected size-cap message, got {msg}"
);
assert!(
!msg.contains("hello world"),
"error must not include file body: {msg}"
);
}
#[test]
fn read_source_arg_file_uses_limited_reader() {
let mut f = NamedTempFile::new().unwrap();
write!(f, "contract Foo {{}}\n").unwrap();
f.flush().unwrap();
let path = f.path().to_string_lossy().to_string();
let (source, display) = read_source_arg(&path).unwrap();
assert_eq!(source, "contract Foo {}\n");
assert_eq!(display, path);
}
}