use rossi::NamedComponent;
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>> {
Ok(rossi::parse_named_components(source)
.map_err(|e| format!("Failed to parse {label}: {e}"))?)
}
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>> {
collect_files(inputs, is_text_ext)
}
pub(crate) fn collect_rodin_xml_files(inputs: &[PathBuf]) -> CmdResult<Vec<PathBuf>> {
collect_files(inputs, is_rodin_xml_ext)
}
fn collect_files(
inputs: &[PathBuf],
matches_extension: fn(&str) -> bool,
) -> 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_entry(|entry| entry.depth() == 0 || !eventb_lsp::walk::is_hidden_dir(entry))
{
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type();
let is_file = file_type.is_file()
|| file_type.is_symlink() && std::fs::metadata(path)?.is_file();
if is_file
&& let Some(ext) = path.extension().and_then(|e| e.to_str())
&& matches_extension(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 parent_or_cwd(path: &Path) -> std::path::PathBuf {
match path.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
_ => std::path::PathBuf::from("."),
}
}
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()
}