use rossi::{NamedComponent, component_filename, parse_components};
use rossi_build::ProjectComponent;
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
pub(crate) type CmdResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
pub(crate) fn is_rodin_xml_ext(ext: &str) -> bool {
ext.eq_ignore_ascii_case("buc") || ext.eq_ignore_ascii_case("bum")
}
pub(crate) fn is_text_ext(ext: &str) -> bool {
ext.eq_ignore_ascii_case("eventb") || ext.eq_ignore_ascii_case("txt")
}
pub(crate) fn is_eventb_ext(ext: &str) -> bool {
ext.eq_ignore_ascii_case("eventb")
}
pub(crate) fn is_zip_ext(ext: &str) -> bool {
ext.eq_ignore_ascii_case("zip")
}
pub(crate) fn is_stdin(p: &Path) -> bool {
p.as_os_str() == "-"
}
pub(crate) fn read_stdin_to_string() -> CmdResult<String> {
Ok(std::io::read_to_string(std::io::stdin())?)
}
pub(crate) fn stdin_is_sole_input(inputs: &[PathBuf]) -> CmdResult<bool> {
let has_stdin = inputs.iter().any(|p| is_stdin(p));
if has_stdin && inputs.len() > 1 {
return Err("'-' (stdin) must be the only input".into());
}
Ok(has_stdin)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InputFamily {
Rodin,
Text,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InputKind {
Text,
RodinXml,
RodinZip,
}
impl InputKind {
pub(crate) fn family(self) -> InputFamily {
match self {
InputKind::Text => InputFamily::Text,
InputKind::RodinXml | InputKind::RodinZip => InputFamily::Rodin,
}
}
}
pub(crate) fn classify_file(p: &Path) -> CmdResult<InputKind> {
match p.extension().and_then(|e| e.to_str()) {
Some(ext) if is_text_ext(ext) => Ok(InputKind::Text),
Some(ext) if is_rodin_xml_ext(ext) => Ok(InputKind::RodinXml),
Some(ext) if is_zip_ext(ext) => Ok(InputKind::RodinZip),
Some(ext) => Err(format!("Unsupported file extension '.{}': {}", ext, p.display()).into()),
None => Err(format!("File has no extension: {}", p.display()).into()),
}
}
pub(crate) fn ensure_input(p: &Path, want: InputFamily) -> CmdResult<()> {
if !p.exists() {
return Err(format!("Input not found: {}", p.display()).into());
}
if p.is_dir() {
return Ok(());
}
if classify_file(p)?.family() == want {
return Ok(());
}
Err(match want {
InputFamily::Rodin => format!(
"import reads Rodin inputs (.zip/.buc/.bum/dir); '{}' is Event-B text \u{2014} use `rossi export`",
p.display()
),
InputFamily::Text => format!(
"export reads Event-B text (.eventb/.txt/dir); '{}' is a Rodin file \u{2014} use `rossi import`",
p.display()
),
}
.into())
}
pub(crate) fn parse_text_components(label: &str, source: &str) -> CmdResult<Vec<NamedComponent>> {
let parsed = parse_components(source).map_err(|e| format!("Failed to parse {label}: {e}"))?;
Ok(parsed
.into_iter()
.map(|component| NamedComponent {
filename: component_filename(&component),
component,
})
.collect())
}
pub(crate) fn parse_rodin_xml_file(path: &Path) -> CmdResult<NamedComponent> {
let xml = fs::read_to_string(path)?;
let filename = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| format!("Invalid filename: {}", path.display()))?;
let component = ProjectComponent::from_xml(filename, &xml)?;
Ok(NamedComponent {
filename: component.filename,
component: component.component,
})
}
pub(crate) fn collect_eventb_files(inputs: &[PathBuf]) -> CmdResult<Vec<PathBuf>> {
let mut files = Vec::new();
for input in inputs {
if input.is_dir() {
for entry in WalkDir::new(input).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_file()
&& let Some(ext) = path.extension().and_then(|e| e.to_str())
&& is_text_ext(ext)
{
files.push(path.to_path_buf());
}
}
} else {
files.push(input.clone());
}
}
files.sort();
Ok(files)
}
pub(crate) fn collect_rodin_xml_files(inputs: &[PathBuf]) -> CmdResult<Vec<PathBuf>> {
let mut files = Vec::new();
for input in inputs {
if input.is_dir() {
for entry in WalkDir::new(input).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_file()
&& let Some(ext) = path.extension().and_then(|e| e.to_str())
&& is_rodin_xml_ext(ext)
{
files.push(path.to_path_buf());
}
}
} else {
files.push(input.clone());
}
}
files.sort();
Ok(files)
}
pub(crate) fn ensure_parent_dir(path: &Path) -> CmdResult<()> {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)?;
}
Ok(())
}
pub(crate) fn safe_path_segment(name: &str) -> String {
Path::new(name)
.file_name()
.and_then(|n| n.to_str())
.filter(|n| !n.is_empty())
.unwrap_or("project")
.to_string()
}