mant-loader 0.11.0

Read-only local document discovery and bounded loading for ManT
Documentation
use std::{collections::HashMap, env, ffi::OsString, fs, path::PathBuf};

use super::{
    BsdManConfig, ManDbConfig, ManualPathPlatform, config_directive, deduplicate_manual_paths,
    developer_manual_roots, discover_manual_roots_from, discover_manual_roots_from_for,
    environment_value_for, expand_man_db_systems, parse_bsd_man_config, parse_man_db_config,
    parse_mandoc_manpaths, supplemental_manual_roots_for, wildcard_matches,
};

#[test]
fn injected_windows_discovery_uses_only_its_config_and_environment() {
    let root = temporary_root("injected-windows-config");
    fs::create_dir_all(&root).unwrap();
    let config = root.join("man.conf");
    fs::write(&config, "MANPATH C:\\isolated\\manuals\n").unwrap();
    let environment = HashMap::from([(
        OsString::from("AppData"),
        OsString::from(r"C:\isolated\roaming"),
    )]);
    let context = super::DiscoveryContext {
        environment: &environment,
        platform: ManualPathPlatform::Windows,
        mant_config: Some(&config),
    };
    let result = super::host_default_manual_roots(&context);
    assert!(result.diagnostics.is_empty());
    assert_eq!(
        result.roots,
        [
            PathBuf::from(r"C:\isolated\manuals"),
            PathBuf::from(r"C:\isolated\roaming").join("ManT/man")
        ]
    );
    let absent = super::DiscoveryContext {
        mant_config: None,
        ..context
    };
    assert_eq!(
        super::host_default_manual_roots(&absent).roots,
        [PathBuf::from(r"C:\isolated\roaming").join("ManT/man")]
    );
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn macos_man_conf_reads_paths_and_imports_port_fragments() {
    let root = temporary_root("macos-man-conf");
    let fragments = root.join("man.d");
    fs::create_dir_all(&fragments).expect("create fragment root");
    let primary = root.join("primary");
    let port = root.join("port");
    fs::create_dir_all(&primary).expect("create primary root");
    fs::create_dir_all(&port).expect("create port root");
    let fragment = fragments.join("tool.conf");
    fs::write(&fragment, format!("MANPATH {}\n", port.display())).expect("write fragment");
    let configuration = parse_bsd_man_config(&format!(
        "MANPATH {}\nMANCONFIG {}/*.conf\n",
        primary.display(),
        fragments.display()
    ));
    assert_eq!(configuration.paths, vec![primary]);
    assert_eq!(
        configuration.include_pattern,
        Some(fragments.join("*.conf"))
    );
    assert_eq!(
        super::macos_configuration_roots(&root.join("man.conf")).roots,
        Vec::<PathBuf>::new()
    );
    fs::write(
        root.join("man.conf"),
        format!(
            "MANPATH {}\nMANCONFIG {}/*.conf\n",
            root.join("primary").display(),
            fragments.display()
        ),
    )
    .expect("write man.conf");
    assert_eq!(
        super::macos_configuration_roots(&root.join("man.conf")).roots,
        vec![root.join("primary"), port]
    );
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn mandoc_man_conf_uses_lowercase_manpath_only() {
    let paths = parse_mandoc_manpaths(
        "# comment\nmanpath /usr/share/man\nMANPATH /not-mandoc\noutput style css\n",
    );
    assert_eq!(paths, vec![PathBuf::from("/usr/share/man")]);
}

#[test]
fn directive_parser_preserves_internal_path_whitespace() {
    assert_eq!(
        config_directive("manpath   C:\\Program Files\\Tool\\man   "),
        Some(("manpath", "C:\\Program Files\\Tool\\man"))
    );
    assert_eq!(config_directive("manpath   "), None);
    assert_eq!(config_directive("manpath"), None);
}

#[test]
fn active_macos_developer_tree_contributes_tool_and_sdk_manuals() {
    let developer = temporary_root("macos-developer");
    let tool = developer.join("usr/share/man");
    let sdk = developer.join("Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/share/man");
    fs::create_dir_all(&tool).expect("create tool manuals");
    fs::create_dir_all(&sdk).expect("create SDK manuals");
    assert_eq!(developer_manual_roots(&developer), vec![tool, sdk]);
    fs::remove_dir_all(developer).expect("remove fixture");
}

#[test]
fn man_db_maps_path_then_appends_mandatory_roots_and_systems() {
    let root = temporary_root("man-db-mappings");
    let binary = root.join("tool/bin");
    let manual = root.join("tool/man");
    let shared = root.join("tool/share/man");
    let mandatory = root.join("usr/share/man");
    let configuration = parse_man_db_config(&format!(
        "MANPATH_MAP {} {}\nMANPATH_MAP {} {}\nMANDATORY_MANPATH {}\n",
        binary.display(),
        manual.display(),
        binary.display(),
        shared.display(),
        mandatory.display(),
    ));
    assert_eq!(
        configuration,
        ManDbConfig {
            mappings: vec![
                (binary.clone(), manual.clone()),
                (binary.clone(), shared.clone()),
            ],
            mandatory: vec![mandatory.clone()],
        }
    );
    let environment = HashMap::from([
        (
            OsString::from("PATH"),
            env::join_paths([binary]).expect("join PATH"),
        ),
        (OsString::from("SYSTEM"), OsString::from("man")),
    ]);
    assert_eq!(
        super::man_db_manual_roots(&environment, &configuration),
        vec![manual, shared, mandatory]
    );
}

#[test]
fn empty_manpath_components_insert_one_native_default_sequence() {
    let root = temporary_root("empty-manpath");
    let first = root.join("first");
    let empty = PathBuf::new();
    let last = root.join("last");
    let environment = HashMap::from([(
        OsString::from("MANPATH"),
        env::join_paths([&first, &empty, &last]).expect("join MANPATH"),
    )]);
    let native_a = root.join("native/a");
    let native_b = root.join("native/b");
    assert_eq!(
        discover_manual_roots_from(&environment, vec![native_a.clone(), native_b.clone()],),
        vec![first, native_a, native_b, last]
    );
}

#[test]
fn windows_supplemental_roots_prefer_mant_data_before_profile_compatibility() {
    let data_root = PathBuf::from(r"C:\Users\demo\AppData\Roaming");
    let profile = PathBuf::from(r"C:\Users\demo");
    let environment = HashMap::from([
        (OsString::from("AppData"), data_root.as_os_str().to_owned()),
        (
            OsString::from("UserProfile"),
            profile.as_os_str().to_owned(),
        ),
    ]);

    assert_eq!(
        supplemental_manual_roots_for(&environment, ManualPathPlatform::Windows),
        vec![
            data_root.join("ManT").join("man"),
            profile.join(".local/share/man")
        ]
    );
}

#[test]
fn windows_environment_names_are_ascii_case_insensitive_only_on_windows() {
    let path = env::join_paths([PathBuf::from("/tools")]).expect("join PATH");
    let environment = HashMap::from([
        (OsString::from("Path"), path.clone()),
        (OsString::from("ManPath"), OsString::from("/manuals")),
        (OsString::from("Mant_ManPath"), OsString::from("/override")),
    ]);

    assert_eq!(
        environment_value_for(&environment, "PATH", ManualPathPlatform::Windows),
        Some(&path)
    );
    assert_eq!(
        environment_value_for(&environment, "PATH", ManualPathPlatform::Linux),
        None
    );
    assert_eq!(
        discover_manual_roots_from_for(
            &environment,
            vec![PathBuf::from("/default")],
            ManualPathPlatform::Windows,
        ),
        vec![PathBuf::from("/override")]
    );

    let environment = HashMap::from([(
        OsString::from("ManPath"),
        env::join_paths([PathBuf::from("/first"), PathBuf::from("/second")]).expect("join MANPATH"),
    )]);
    assert_eq!(
        discover_manual_roots_from_for(
            &environment,
            vec![PathBuf::from("/default")],
            ManualPathPlatform::Windows,
        ),
        vec![PathBuf::from("/first"), PathBuf::from("/second")]
    );
}

#[test]
fn windows_final_roots_deduplicate_case_and_separator_variants() {
    assert_eq!(
        deduplicate_manual_paths(
            [
                PathBuf::from(r"C:\Users\demo\ManT\man"),
                PathBuf::from("c:/users/DEMO/mant/man/"),
            ],
            ManualPathPlatform::Windows,
        ),
        vec![PathBuf::from(r"C:\Users\demo\ManT\man")]
    );
}

#[test]
fn windows_supplemental_roots_do_not_require_a_profile_fallback() {
    let data_root = PathBuf::from(r"D:\Roaming");
    let environment =
        HashMap::from([(OsString::from("APPDATA"), data_root.as_os_str().to_owned())]);

    assert_eq!(
        supplemental_manual_roots_for(&environment, ManualPathPlatform::Windows),
        vec![data_root.join("ManT").join("man")]
    );
}

#[test]
fn wildcard_expansion_matches_configuration_file_globs_deterministically() {
    assert!(wildcard_matches("*.conf", "perl.conf"));
    assert!(wildcard_matches("?.conf", "x.conf"));
    assert!(!wildcard_matches("?.conf", "xy.conf"));
}

#[cfg(windows)]
#[test]
fn wildcard_expansion_preserves_an_absolute_windows_root() {
    let root = temporary_root("windows-absolute-glob");
    fs::create_dir_all(&root).expect("create fragment root");
    let fragment = root.join("tool.conf");
    fs::write(&fragment, "MANPATH C:\\manuals\n").expect("write fragment");

    assert_eq!(super::expand_path_pattern(&root.join("*.conf")), [fragment]);
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn systems_without_man_omit_the_native_root() {
    let environment = HashMap::from([(OsString::from("SYSTEM"), OsString::from("other"))]);
    let root = temporary_root("man-db-system");
    fs::create_dir_all(root.join("other")).expect("create system root");
    assert_eq!(
        expand_man_db_systems(vec![root.clone()], &environment),
        vec![root.join("other")]
    );
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn bsd_config_keeps_only_path_related_directives() {
    assert_eq!(
        parse_bsd_man_config("MANPATH /usr/share/man\nMANLOCALE ja_JP\n"),
        BsdManConfig {
            paths: vec![PathBuf::from("/usr/share/man")],
            include_pattern: None,
            truncated: false,
        }
    );
}

fn temporary_root(label: &str) -> PathBuf {
    std::env::temp_dir().join(format!("mant-manual-paths-{label}-{}", std::process::id()))
}