palisade-errors 2.0.0

Security-conscious error handling with operational security principles
use stats_alloc::{INSTRUMENTED_SYSTEM, Region, Stats, StatsAlloc};
use std::alloc::System;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

#[global_allocator]
static GLOBAL: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;

pub(crate) struct BenchResult {
    pub(crate) name: &'static str,
    pub(crate) iterations: usize,
    pub(crate) total: Duration,
    pub(crate) min: Duration,
    pub(crate) max: Duration,
    pub(crate) stats: Stats,
    pub(crate) ct_floor: Option<Duration>,
    pub(crate) ct_ok: Option<bool>,
}

pub(crate) struct CountingWriter {
    len: usize,
}

impl CountingWriter {
    pub(crate) fn new() -> Self {
        Self { len: 0 }
    }

    pub(crate) const fn len(&self) -> usize {
        self.len
    }
}

impl fmt::Write for CountingWriter {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        self.len += s.len();
        Ok(())
    }
}

pub(crate) fn run_bench(
    name: &'static str,
    iterations: usize,
    mut scenario: impl FnMut(),
) -> BenchResult {
    let mut total = Duration::ZERO;
    let mut min = Duration::MAX;
    let mut max = Duration::ZERO;
    let mut stats = Stats::default();

    for _ in 0..iterations {
        let region = Region::new(GLOBAL);
        let start = Instant::now();
        scenario();
        let elapsed = start.elapsed();
        let sample = region.change();

        total += elapsed;
        min = min.min(elapsed);
        max = max.max(elapsed);
        accumulate(&mut stats, sample);
    }

    BenchResult {
        name,
        iterations,
        total,
        min,
        max,
        stats,
        ct_floor: None,
        ct_ok: None,
    }
}

pub(crate) fn run_bench_with_ct(
    name: &'static str,
    iterations: usize,
    ct_floor: Duration,
    scenario: impl FnMut(),
) -> BenchResult {
    let mut result = run_bench(name, iterations, scenario);
    result.ct_ok = Some(result.min >= ct_floor);
    result.ct_floor = Some(ct_floor);
    result
}

pub(crate) fn write_report(
    file_name: &str,
    title: &str,
    results: &[BenchResult],
) -> std::io::Result<PathBuf> {
    let report_dir = Path::new("target/bench-results");
    fs::create_dir_all(report_dir)?;
    let path = report_dir.join(file_name);

    let generated_at = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |duration| duration.as_secs());

    let mut out = String::new();
    out.push_str(title);
    out.push('\n');
    out.push_str(&format!("generated_at_unix={generated_at}\n\n"));

    for result in results {
        let avg_ns = result.total.as_nanos() / result.iterations as u128;
        out.push_str(&format!("scenario={}\n", result.name));
        out.push_str(&format!("iterations={}\n", result.iterations));
        out.push_str(&format!("avg_ns={avg_ns}\n"));
        out.push_str(&format!("min_ns={}\n", result.min.as_nanos()));
        out.push_str(&format!("max_ns={}\n", result.max.as_nanos()));
        out.push_str(&format!(
            "allocations_per_iter={}\n",
            result.stats.allocations / result.iterations
        ));
        out.push_str(&format!(
            "deallocations_per_iter={}\n",
            result.stats.deallocations / result.iterations
        ));
        out.push_str(&format!(
            "reallocations_per_iter={}\n",
            result.stats.reallocations / result.iterations
        ));
        out.push_str(&format!(
            "bytes_allocated_per_iter={}\n",
            result.stats.bytes_allocated / result.iterations
        ));
        out.push_str(&format!(
            "bytes_deallocated_per_iter={}\n",
            result.stats.bytes_deallocated / result.iterations
        ));
        out.push_str(&format!(
            "bytes_reallocated_per_iter={}\n\n",
            result.stats.bytes_reallocated / result.iterations as isize
        ));
        if let Some(ct_floor) = result.ct_floor {
            out.push_str(&format!("ct_floor_ns={}\n", ct_floor.as_nanos()));
            out.push_str(&format!(
                "ct_ok={}\n\n",
                result
                    .ct_ok
                    .expect("ct result must be present when ct floor is set")
            ));
        }
    }

    fs::write(&path, out)?;
    Ok(path)
}

#[cfg(feature = "log")]
pub(crate) fn temp_log_path(label: &str) -> PathBuf {
    Path::new("target/bench-results").join(format!(
        "palisade_errors_{label}_{}_{}.log",
        std::process::id(),
        monotonic_suffix(),
    ))
}

#[cfg(feature = "log")]
pub(crate) fn cleanup_log(path: &Path) {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        if let Ok(metadata) = std::fs::metadata(path) {
            let mut permissions = metadata.permissions();
            permissions.set_mode(0o600);
            let _ = std::fs::set_permissions(path, permissions);
        }
    }
    let _ = std::fs::remove_file(path);
}

#[cfg(feature = "log")]
fn monotonic_suffix() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |duration| duration.as_nanos())
}

fn accumulate(total: &mut Stats, sample: Stats) {
    total.allocations += sample.allocations;
    total.deallocations += sample.deallocations;
    total.reallocations += sample.reallocations;
    total.bytes_allocated += sample.bytes_allocated;
    total.bytes_deallocated += sample.bytes_deallocated;
    total.bytes_reallocated += sample.bytes_reallocated;
}