use super::diagnostic::{Diagnostic, DiagnosticCode, Location, Severity};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub fn check_filesystem(preset_path: &Path) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let toml_str = match std::fs::read_to_string(preset_path) {
Ok(s) => s,
Err(_) => return diagnostics,
};
let raw: crate::preset::PresetRaw = match toml::from_str(&toml_str) {
Ok(r) => r,
Err(_) => return diagnostics, };
let base_dir = preset_path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
if let Some(lut_path) = raw.lut.path.as_ref() {
let resolved = base_dir.join(lut_path);
if !resolved.exists() {
let (line, column) = super::structural::find_position_by_path(&toml_str, "lut.path");
diagnostics.push(Diagnostic {
severity: Severity::Error,
code: DiagnosticCode::LutNotFound,
message: format!(
"LUT file not found: `{}` (resolved to {})",
lut_path,
resolved.display()
),
location: Location {
line,
column,
field: "lut.path".to_string(),
},
});
}
}
diagnostics.extend(check_extends_chain(preset_path, &base_dir, &toml_str));
diagnostics
}
fn check_extends_chain(start_path: &Path, start_dir: &Path, start_toml: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let mut visited: HashSet<PathBuf> = HashSet::new();
let canonical = start_path
.canonicalize()
.unwrap_or_else(|_| start_path.to_path_buf());
visited.insert(canonical);
let mut current_path = start_path.to_path_buf();
let mut current_dir = start_dir.to_path_buf();
let mut current_toml = start_toml.to_string();
while let Ok(raw) = toml::from_str::<crate::preset::PresetRaw>(¤t_toml) {
let extends = match raw.metadata.extends.as_ref() {
Some(e) => e.clone(),
None => break, };
let next_path = current_dir.join(&extends);
if !next_path.exists() {
let (line, column) =
super::structural::find_position_by_path(¤t_toml, "metadata.extends");
let via_suffix = via_suffix(¤t_path, start_path);
diagnostics.push(Diagnostic {
severity: Severity::Error,
code: DiagnosticCode::ExtendsNotFound,
message: format!(
"extends references missing file: `{}` (resolved to {}){}",
extends,
next_path.display(),
via_suffix,
),
location: Location {
line,
column,
field: "metadata.extends".to_string(),
},
});
break;
}
let canonical = next_path
.canonicalize()
.unwrap_or_else(|_| next_path.clone());
if !visited.insert(canonical) {
let (line, column) =
super::structural::find_position_by_path(¤t_toml, "metadata.extends");
let via_suffix = via_suffix(¤t_path, start_path);
diagnostics.push(Diagnostic {
severity: Severity::Error,
code: DiagnosticCode::ExtendsCycle,
message: format!(
"extends chain has a cycle: `{}` already visited{}",
next_path.display(),
via_suffix,
),
location: Location {
line,
column,
field: "metadata.extends".to_string(),
},
});
break;
}
let next_toml = match std::fs::read_to_string(&next_path) {
Ok(s) => s,
Err(_) => break,
};
if let Err(e) = toml::from_str::<crate::preset::PresetRaw>(&next_toml) {
let (line, column) =
super::structural::find_position_by_path(¤t_toml, "metadata.extends");
let via_suffix = via_suffix(¤t_path, start_path);
diagnostics.push(Diagnostic {
severity: Severity::Error,
code: DiagnosticCode::ExtendsNotFound,
message: format!(
"extends references unparseable file: `{}` ({}){}",
extends, e, via_suffix
),
location: Location {
line,
column,
field: "metadata.extends".to_string(),
},
});
break;
}
current_toml = next_toml;
current_dir = next_path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
current_path = next_path;
}
diagnostics
}
fn via_suffix(current_path: &Path, start_path: &Path) -> String {
let current_canon = current_path
.canonicalize()
.unwrap_or_else(|_| current_path.to_path_buf());
let start_canon = start_path
.canonicalize()
.unwrap_or_else(|_| start_path.to_path_buf());
if current_canon == start_canon {
String::new()
} else {
let name = current_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?");
format!(" (via {})", name)
}
}