netcdf-reader 0.9.0

Pure-Rust NetCDF-3 classic and NetCDF-4 (HDF5-backed) file reader
Documentation
//! Property test: parsing arbitrary byte mutations of a valid classic file
//! must never panic, hang, or allocate unboundedly — it either reads
//! successfully or returns an error. A process-wide `peak_alloc` allocator
//! bounds each case's heap growth so a mutation that inflates a declared count
//! is caught as a failure rather than an OOM.

use netcdf_reader::NcFile;
use peak_alloc::PeakAlloc;
use proptest::prelude::*;

#[global_allocator]
static PEAK: PeakAlloc = PeakAlloc;

/// A valid CDF-5 file (an unlimited `time` dim, a fixed `x=3` dim, a global
/// title/int attribute, a fixed `grid` float variable, and a record `obs`
/// short variable). Produced by netcdf-writer; mutating its bytes exercises
/// every classic parse path.
const SEED: &[u8] = &[
    0x43, 0x44, 0x46, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0a,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
    0x74, 0x69, 0x6d, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x01, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
    0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x73, 0x65, 0x65, 0x64, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x02, 0x67, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03,
    0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x04, 0x67, 0x72, 0x69, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x75, 0x6e, 0x69, 0x74,
    0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
    0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
    0x6f, 0x62, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x48, 0x3f, 0x80, 0x00, 0x00,
    0x40, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x14,
    0x00, 0x15, 0x00, 0x16,
];

/// Bound on heap growth for a single mutated-open attempt. Generous enough for
/// legitimate parsing of this small file, far below what a declared-count bomb
/// would demand.
const MAX_OPEN_ALLOC: f32 = 8.0 * 1024.0 * 1024.0;

proptest! {
    #![proptest_config(ProptestConfig::with_cases(400))]

    #[test]
    fn mutated_classic_file_never_panics_or_over_allocates(
        mutations in prop::collection::vec((0usize..SEED.len(), any::<u8>()), 0..8)
    ) {
        let mut bytes = SEED.to_vec();
        for (index, value) in mutations {
            bytes[index] = value;
        }

        PEAK.reset_peak_usage();
        let before = PEAK.current_usage() as f32;

        // Opening must not panic; enumerate and read to exercise deeper paths.
        if let Ok(file) = NcFile::from_bytes(&bytes) {
            let _ = file.global_attributes();
            let _ = file.dimensions();
            if let Ok(variables) = file.variables() {
                let names: Vec<String> = variables.iter().map(|v| v.name.clone()).collect();
                for name in names {
                    let _ = file.read_variable::<f64>(&name);
                }
            }
        }

        let peak = PEAK.peak_usage() as f32 - before;
        prop_assert!(
            peak < MAX_OPEN_ALLOC,
            "mutated open allocated {peak} bytes (> {MAX_OPEN_ALLOC})"
        );
    }
}