zahirscan 0.3.6

Token-efficient content compression for AI analysis using probabilistic template mining
Documentation
//! `NetCDF` (`.nc`, `.cdf`) — when the `netcdf` feature is enabled, global and variable metadata is read
//! via the [`::netcdf`] crate. When the feature is off, [`extract_netcdf_metadata`] fails at runtime with a
//! build-time error (e.g. docs.rs or `--no-default-features` without `libnetcdf`).

// Use `::netcdf` in the `netcdf` feature so paths refer to the library crate, not this module.

#[cfg(not(feature = "netcdf"))]
mod not_linked {
    use anyhow::Result;
    use memmap2::Mmap;

    use crate::config::RuntimeConfig;
    use crate::parsers::ParseResult;
    use crate::results::NetCdfMetadata;

    /// Returns an error because the `netcdf` feature is disabled; rebuild with that feature and `libnetcdf`.
    ///
    /// # Errors
    ///
    /// Always: NetCDF support was not compiled in.
    pub fn extract_netcdf_metadata(
        _mmap: &Mmap,
        _stats: &ParseResult,
        _config: &RuntimeConfig,
    ) -> Result<NetCdfMetadata> {
        anyhow::bail!(
            "NetCDF support was disabled at build time. Rebuild with `--features netcdf` (and install `libnetcdf`)."
        )
    }

    crate::no_template_mining!(
        extract_netcdf_templates,
        "`NetCDF` is binary scientific data; no text template mining."
    );
}

#[cfg(not(feature = "netcdf"))]
pub use not_linked::{extract_netcdf_metadata, extract_netcdf_templates};

#[cfg(feature = "netcdf")]
mod linked {
    use ::netcdf::AttributeValue;
    use ::netcdf::Group;
    use anyhow::Context;
    use log::debug;
    use memmap2::Mmap;

    use crate::config::RuntimeConfig;
    use crate::parsers::ParseResult;
    use crate::results::{NetCdfAttributeEntry, NetCdfMetadata, NetCdfVariableSummary};
    use crate::utils::netcdf_handler::check_netcdf_available;

    const MAX_GLOBAL_ATTRS: usize = 512;
    const MAX_VARS_TOTAL: usize = 10_000;
    const MAX_ATTRS_PER_VAR: usize = 256;
    const MAX_DIMS_LISTED: usize = 512;
    const MAX_GROUP_DEPTH: usize = 128;

    fn attr_value_string(v: AttributeValue) -> String {
        match v {
            AttributeValue::Str(s) => s,
            AttributeValue::Strs(ss) => ss.join("\n"),
            AttributeValue::Uchar(x) => x.to_string(),
            AttributeValue::Uchars(v) => format!("{v:?}"),
            AttributeValue::Schar(x) => x.to_string(),
            AttributeValue::Schars(v) => format!("{v:?}"),
            AttributeValue::Ushort(x) => x.to_string(),
            AttributeValue::Ushorts(v) => format!("{v:?}"),
            AttributeValue::Short(x) => x.to_string(),
            AttributeValue::Shorts(v) => format!("{v:?}"),
            AttributeValue::Uint(x) => x.to_string(),
            AttributeValue::Uints(v) => format!("{v:?}"),
            AttributeValue::Int(x) => x.to_string(),
            AttributeValue::Ints(v) => format!("{v:?}"),
            AttributeValue::Ulonglong(x) => x.to_string(),
            AttributeValue::Ulonglongs(v) => format!("{v:?}"),
            AttributeValue::Longlong(x) => x.to_string(),
            AttributeValue::Longlongs(v) => format!("{v:?}"),
            AttributeValue::Float(x) => x.to_string(),
            AttributeValue::Floats(v) => format!("{v:?}"),
            AttributeValue::Double(x) => x.to_string(),
            AttributeValue::Doubles(v) => format!("{v:?}"),
        }
    }

    fn collect_global_attrs(file: &::netcdf::File, out: &mut Vec<NetCdfAttributeEntry>) {
        for a in file.attributes().take(MAX_GLOBAL_ATTRS) {
            let name = a.name().to_string();
            match a.value() {
                Ok(v) => out.push(NetCdfAttributeEntry {
                    name,
                    value: attr_value_string(v),
                }),
                Err(e) => out.push(NetCdfAttributeEntry {
                    name,
                    value: format!("<read error: {e}>"),
                }),
            }
        }
    }

    fn push_variable(
        var_ref: &::netcdf::Variable<'_>,
        path_prefix: &str,
        out: &mut Vec<NetCdfVariableSummary>,
        truncated: &mut bool,
    ) {
        if out.len() >= MAX_VARS_TOTAL {
            *truncated = true;
            return;
        }

        let base = var_ref.name();
        let name = if path_prefix.is_empty() {
            base
        } else {
            format!("{path_prefix}/{base}")
        };

        let dims = var_ref.dimensions();
        let shape: Vec<usize> = dims.iter().map(::netcdf::Dimension::len).collect();
        let dimension_names: Vec<String> = dims.iter().map(::netcdf::Dimension::name).collect();
        let vartype = format!("{:?}", var_ref.vartype());

        let mut attrs = Vec::new();
        for a in var_ref.attributes().take(MAX_ATTRS_PER_VAR) {
            let aname = a.name().to_string();
            match a.value() {
                Ok(v) => attrs.push(NetCdfAttributeEntry {
                    name: aname,
                    value: attr_value_string(v),
                }),
                Err(e) => attrs.push(NetCdfAttributeEntry {
                    name: aname,
                    value: format!("<read error: {e}>"),
                }),
            }
        }

        out.push(NetCdfVariableSummary {
            name,
            shape: Some(shape),
            dimension_names: Some(dimension_names),
            vartype: Some(vartype),
            attributes: Some(attrs),
        });
    }

    fn walk_group(
        g: &Group<'_>,
        prefix: &str,
        out: &mut Vec<NetCdfVariableSummary>,
        truncated: &mut bool,
        depth: usize,
    ) -> anyhow::Result<()> {
        if depth > MAX_GROUP_DEPTH {
            *truncated = true;
            return Ok(());
        }

        for v in g.variables() {
            if out.len() >= MAX_VARS_TOTAL {
                *truncated = true;
                return Ok(());
            }
            push_variable(&v, prefix, out, truncated);
        }

        for child in g.groups() {
            let stem = child.name();
            let next = if prefix.is_empty() {
                stem
            } else {
                format!("{prefix}/{stem}")
            };
            walk_group(&child, &next, out, truncated, depth + 1)?;
        }
        Ok(())
    }

    /// Extract `NetCDF` metadata (attributes, variable names, shapes, types). Uses [`ParseResult::file_path`];
    /// `mmap` is unused because `netcdf` opens by path.
    ///
    /// # Errors
    ///
    /// When `NetCDF` runtime helpers are unavailable, the file cannot be opened, or walking NetCDF-4 groups fails.
    pub fn extract_netcdf_metadata(
        _mmap: &Mmap,
        stats: &ParseResult,
        _config: &RuntimeConfig,
    ) -> anyhow::Result<NetCdfMetadata> {
        check_netcdf_available()?;

        let path = stats.file_path.as_str();
        let file = ::netcdf::open(path).map_err(|e| {
            debug!("NetCDF open failed for '{path}': {e}");
            anyhow::anyhow!("{e}")
        })?;

        let netcdf4_model = Some(file.root().is_some());

        let mut global_attributes = Vec::new();
        collect_global_attrs(&file, &mut global_attributes);

        let dims: Vec<_> = file.dimensions().take(MAX_DIMS_LISTED).collect();
        let dimension_count = Some(dims.len());
        let dimensions_sample: Vec<(String, usize)> =
            dims.iter().map(|d| (d.name(), d.len())).collect();

        let mut variables = Vec::new();
        let mut metadata_truncated = false;

        for v in file.variables() {
            if variables.len() >= MAX_VARS_TOTAL {
                metadata_truncated = true;
                break;
            }
            push_variable(&v, "", &mut variables, &mut metadata_truncated);
        }

        // NetCDF-4 only: nested groups (root variables are already in `file.variables()`).
        if let Some(root) = file.root() {
            for child in root.groups() {
                if variables.len() >= MAX_VARS_TOTAL {
                    metadata_truncated = true;
                    break;
                }
                let stem = child.name();
                walk_group(&child, &stem, &mut variables, &mut metadata_truncated, 0)
                    .with_context(|| "walk NetCDF-4 groups")?;
            }
        }

        let variable_count = Some(variables.len());

        Ok(NetCdfMetadata {
            byte_count: stats.byte_count,
            netcdf4_model,
            global_attributes: Some(global_attributes),
            dimension_count,
            dimensions_sample: Some(dimensions_sample),
            variable_count,
            variables: Some(variables),
            metadata_truncated: Some(metadata_truncated),
            parse_error: None,
        })
    }

    crate::no_template_mining!(
        extract_netcdf_templates,
        "`NetCDF` is binary scientific data; no text template mining."
    );
}

#[cfg(feature = "netcdf")]
pub use linked::{extract_netcdf_metadata, extract_netcdf_templates};