use crate::engine::codec::EndianCodec;
use crate::engine::endian::FileEndian;
use crate::engine::stats::validate_header_stats;
use crate::{Error, Mode, Reader};
use std::fmt::Write;
use std::path::Path;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Info,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct ValidationIssue {
pub severity: Severity,
#[cfg_attr(feature = "serde", serde(skip))]
pub category: &'static str,
pub message: String,
}
impl ValidationIssue {
fn error(category: &'static str, message: String) -> Self {
Self {
severity: Severity::Error,
category,
message,
}
}
fn warning(category: &'static str, message: String) -> Self {
Self {
severity: Severity::Warning,
category,
message,
}
}
fn info(category: &'static str, message: String) -> Self {
Self {
severity: Severity::Info,
category,
message,
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct ValidationReport {
pub path: String,
pub compression: String,
pub nx: i32,
pub ny: i32,
pub nz: i32,
pub mode: i32,
pub issues: Vec<ValidationIssue>,
}
impl ValidationReport {
pub fn is_valid(&self) -> bool {
!self.issues.iter().any(|i| i.severity == Severity::Error)
}
pub fn by_severity(&self, severity: Severity) -> impl Iterator<Item = &ValidationIssue> {
self.issues.iter().filter(move |i| i.severity == severity)
}
}
pub fn validate_reader(
reader: &Reader,
path: &str,
compression: &str,
warnings: &[String],
) -> Result<ValidationReport, Error> {
let mut issues: Vec<ValidationIssue> = Vec::with_capacity(16);
let header = reader.header();
let mode_val = header.mode;
let endian = reader.endian();
for w in warnings {
issues.push(ValidationIssue::warning("Open", w.clone()));
}
match header.validate_detailed() {
Ok(()) => {
issues.push(ValidationIssue::info("Header", "Structure is valid".into()));
}
Err(e) => issues.push(ValidationIssue::error("Header", e.to_string())),
}
if let Some(data_size) = header.data_size() {
let expected_total = 1024 + header.nsymbt.max(0) as usize + data_size;
let actual_total = 1024 + reader.ext_header_bytes().len() + reader.raw_bytes().len();
if actual_total == expected_total {
issues.push(ValidationIssue::info(
"File size",
format!("{actual_total} bytes as expected (header + ext + data)"),
));
} else {
issues.push(ValidationIssue::error(
"File size",
format!(
"Size mismatch: expected {expected_total} bytes, got {actual_total} (difference: {})",
(expected_total as i64 - actual_total as i64).abs()
),
));
}
}
let machst_info = FileEndian::from_machst_with_info(&header.machst);
if !machst_info.is_standard {
issues.push(ValidationIssue::warning(
"Endianness",
format!("Non-standard MACHST stamp: {}", machst_info.description),
));
}
let host = FileEndian::native();
if endian == host {
issues.push(ValidationIssue::info(
"Endianness",
"Native byte order, fast-path available".into(),
));
} else {
issues.push(ValidationIssue::info(
"Endianness",
format!("Non-native byte order ({endian:?}), host is {host:?}"),
));
}
let raw_bytes = reader.raw_bytes();
match check_statistics(header, raw_bytes) {
Some(issue) => issues.push(issue),
None => issues.push(ValidationIssue::info(
"Statistics",
"All statistics match actual data (within 1% tolerance)".into(),
)),
}
if reader.mode().is_float() && !reader.mode().is_complex() {
let has_issues = float_mode_issues(raw_bytes, reader.mode(), endian);
if has_issues.is_empty() {
issues.push(ValidationIssue::info(
"Data integrity",
"All voxel values are finite numbers".into(),
));
} else {
for issue in has_issues {
issues.push(ValidationIssue::warning("Data integrity", issue));
}
}
} else if reader.mode() == Mode::Float32Complex {
let has_issues = complex_float_mode_issues(raw_bytes, endian);
if has_issues.is_empty() {
issues.push(ValidationIssue::info(
"Data integrity",
"All complex values are finite numbers".into(),
));
} else {
for issue in has_issues {
issues.push(ValidationIssue::warning("Data integrity", issue));
}
}
}
let vol_type = if header.is_single_image() {
"single 2D image"
} else if header.is_image_stack() {
"image stack"
} else if header.is_volume_stack() {
let nvol = if header.mz > 0 {
header.nz / header.mz
} else {
0
};
issues.push(ValidationIssue::info(
"Volume",
format!("Volume stack: {nvol} sub-volumes × {} slices", header.mz),
));
"volume stack"
} else {
"3D volume"
};
issues.push(ValidationIssue::info(
"Volume",
format!(
"{} × {} × {} voxels, {}",
header.nx, header.ny, header.nz, vol_type
),
));
Ok(ValidationReport {
path: path.to_owned(),
compression: compression.to_owned(),
nx: header.nx,
ny: header.ny,
nz: header.nz,
mode: mode_val,
issues,
})
}
pub fn validate_full<P: AsRef<Path>>(path: P, permissive: bool) -> Result<ValidationReport, Error> {
let path_str = path.as_ref().to_string_lossy().into_owned();
let compression = match crate::io::reader::detect_compression(&path)? {
crate::io::reader::CompressionType::Plain => "plain".to_string(),
#[cfg(feature = "gzip")]
crate::io::reader::CompressionType::Gzip => "gzip".to_string(),
#[cfg(feature = "bzip2")]
crate::io::reader::CompressionType::Bzip2 => "bzip2".to_string(),
};
let (reader, warnings) = if permissive {
Reader::open_permissive(&path)?
} else {
(Reader::open(&path)?, Vec::new())
};
validate_reader(&reader, &path_str, &compression, &warnings)
}
fn check_statistics(header: &crate::Header, raw_bytes: &[u8]) -> Option<ValidationIssue> {
match validate_header_stats(header, raw_bytes) {
Ok(()) => None,
Err(crate::Error::StatsMismatch {
claimed_dmin,
claimed_dmax,
claimed_dmean,
claimed_rms,
actual_dmin,
actual_dmax,
actual_dmean,
actual_rms,
}) => {
let mut detail = String::new();
if (claimed_dmin - actual_dmin).abs() > 0.01 * actual_dmin.abs().max(claimed_dmin.abs())
{
let _ = write!(detail, " dmin claimed={claimed_dmin} actual={actual_dmin}");
}
if (claimed_dmax - actual_dmax).abs() > 0.01 * actual_dmax.abs().max(claimed_dmax.abs())
{
let _ = write!(detail, " dmax claimed={claimed_dmax} actual={actual_dmax}");
}
if (claimed_dmean - actual_dmean).abs()
> 0.01 * actual_dmean.abs().max(claimed_dmean.abs())
{
let _ = write!(
detail,
" dmean claimed={claimed_dmean} actual={actual_dmean}"
);
}
if (claimed_rms - actual_rms).abs() > 0.01 * actual_rms.abs().max(claimed_rms.abs()) {
let _ = write!(detail, " rms claimed={claimed_rms} actual={actual_rms}");
}
Some(ValidationIssue::error(
"Statistics",
format!("Mismatch:{detail}"),
))
}
Err(e) => Some(ValidationIssue::error(
"Statistics",
format!("Cannot compute statistics: {e}"),
)),
}
}
struct NonFiniteCounts {
nan: usize,
pos_inf: usize,
neg_inf: usize,
}
fn format_finite_issues(counts: &NonFiniteCounts, total: usize, prefix: &str) -> Vec<String> {
let mut issues = Vec::with_capacity(3);
if counts.nan > 0 {
issues.push(format!(
"{} NaN values found{} ({:.2}%)",
counts.nan,
prefix,
counts.nan as f64 / total as f64 * 100.0
));
}
if counts.pos_inf > 0 {
issues.push(format!(
"{} +Inf values found{} ({:.2}%)",
counts.pos_inf,
prefix,
counts.pos_inf as f64 / total as f64 * 100.0
));
}
if counts.neg_inf > 0 {
issues.push(format!(
"{} -Inf values found{} ({:.2}%)",
counts.neg_inf,
prefix,
counts.neg_inf as f64 / total as f64 * 100.0
));
}
issues
}
fn float_mode_issues(raw_bytes: &[u8], mode: Mode, endian: FileEndian) -> Vec<String> {
match mode {
Mode::Float32 => {
let n = raw_bytes.len() / 4;
let mut counts = NonFiniteCounts {
nan: 0,
pos_inf: 0,
neg_inf: 0,
};
for i in 0..n {
let v = f32::from_bytes(raw_bytes, i * 4, endian);
if v.is_nan() {
counts.nan += 1;
} else if v.is_infinite() {
if v.is_sign_negative() {
counts.neg_inf += 1;
} else {
counts.pos_inf += 1;
}
}
}
format_finite_issues(&counts, n, "")
}
Mode::Float16 => {
#[cfg(feature = "f16")]
{
let n = raw_bytes.len() / 2;
let mut counts = NonFiniteCounts {
nan: 0,
pos_inf: 0,
neg_inf: 0,
};
for i in 0..n {
let v = f32::from(crate::f16::from_bytes(raw_bytes, i * 2, endian));
if v.is_nan() {
counts.nan += 1;
} else if v.is_infinite() {
if v.is_sign_negative() {
counts.neg_inf += 1;
} else {
counts.pos_inf += 1;
}
}
}
format_finite_issues(&counts, n, "")
}
#[cfg(not(feature = "f16"))]
vec!["Float16 scanning unavailable (requires `f16` feature)".into()]
}
_ => Vec::new(),
}
}
fn complex_float_mode_issues(raw_bytes: &[u8], endian: FileEndian) -> Vec<String> {
let n = raw_bytes.len() / 8;
let mut counts = NonFiniteCounts {
nan: 0,
pos_inf: 0,
neg_inf: 0,
};
for i in 0..n {
let real = f32::from_bytes(raw_bytes, i * 8, endian);
let imag = f32::from_bytes(raw_bytes, i * 8 + 4, endian);
for v in [real, imag] {
if v.is_nan() {
counts.nan += 1;
} else if v.is_infinite() {
if v.is_sign_negative() {
counts.neg_inf += 1;
} else {
counts.pos_inf += 1;
}
}
}
}
let total = n * 2;
format_finite_issues(&counts, total, " in complex components")
}