wsi-rs 0.5.1

wsi-rs whole-slide image reader
Documentation
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;

use crate::error::WsiError;

#[derive(Default)]
pub(crate) struct ParsedIni {
    pub(crate) groups: HashMap<String, HashMap<String, String>>,
}

pub(crate) fn parse_ini_file(
    path: &Path,
    max_size: u64,
    too_large: impl FnOnce(&Path) -> WsiError,
    strip_utf8_bom: bool,
) -> Result<ParsedIni, WsiError> {
    let metadata = std::fs::metadata(path).map_err(|source| WsiError::IoWithPath {
        source: Arc::new(source),
        path: path.to_path_buf(),
    })?;
    if metadata.len() > max_size {
        return Err(too_large(path));
    }
    let text = std::fs::read_to_string(path).map_err(|source| WsiError::IoWithPath {
        source: Arc::new(source),
        path: path.to_path_buf(),
    })?;
    let text = if strip_utf8_bom {
        text.strip_prefix('\u{feff}').unwrap_or(&text)
    } else {
        &text
    };
    Ok(parse_ini_text(text))
}

fn parse_ini_text(text: &str) -> ParsedIni {
    let mut parsed = ParsedIni::default();
    let mut current_group: Option<String> = None;
    for raw_line in text.lines() {
        let line = raw_line.trim();
        if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
            continue;
        }
        if let Some(group) = line
            .strip_prefix('[')
            .and_then(|line| line.strip_suffix(']'))
        {
            current_group = Some(group.to_string());
            parsed.groups.entry(group.to_string()).or_default();
            continue;
        }
        let Some(group) = current_group.as_ref() else {
            continue;
        };
        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        parsed
            .groups
            .entry(group.clone())
            .or_default()
            .insert(key.trim().to_string(), value.trim().to_string());
    }
    parsed
}

#[cfg(test)]
mod tests {
    use std::io::Write;

    use tempfile::NamedTempFile;

    use super::{parse_ini_file, parse_ini_text};
    use crate::error::WsiError;

    #[test]
    fn parse_ini_text_trims_groups_keys_and_values() {
        let parsed = parse_ini_text(
            r#"
            ; comment
            [ Group ]
            key = value
            padded =  spaced value
            # another comment
            ignored without equals
            "#,
        );

        let group = parsed.groups.get(" Group ").expect("group");
        assert_eq!(group.get("key").map(String::as_str), Some("value"));
        assert_eq!(
            group.get("padded").map(String::as_str),
            Some("spaced value")
        );
    }

    #[test]
    fn parse_ini_file_strips_utf8_bom_only_when_requested() {
        let mut file = NamedTempFile::new().expect("temporary INI file");
        write!(file, "\u{feff}[GENERAL]\nKEY=VALUE\n").expect("write INI");

        let stripped = parse_ini_file(
            file.path(),
            1024,
            |_| WsiError::UnsupportedFormat("too large".into()),
            true,
        )
        .expect("parse BOM-stripped INI");
        assert!(stripped.groups.contains_key("GENERAL"));

        let preserved = parse_ini_file(
            file.path(),
            1024,
            |_| WsiError::UnsupportedFormat("too large".into()),
            false,
        )
        .expect("parse BOM-preserved INI");
        assert!(!preserved.groups.contains_key("GENERAL"));
    }
}