view libpam-sys/libpam-sys-test/build.rs @ 171:e27c5c667a5a

Create full new types for return code and flags, separate end to end. This plumbs the ReturnCode and RawFlags types through the places where we call into or are called from PAM. Also adds Sun documentation to the project.
author Paul Fisher <paul@pfish.zone>
date Fri, 25 Jul 2025 20:52:14 -0400
parents 3a7cf05d2b5f
children 0730f5f2ee2a
line wrap: on
line source

use bindgen::MacroTypeVariation;
use libpam_sys_consts::pam_impl::PamImpl;
use libpam_sys_consts::{pam_impl, pam_impl_name};
use proc_macro2::{Group, Ident, TokenStream, TokenTree};
use quote::{format_ident, ToTokens};
use std::path::Path;
use std::process::Command;
use std::str::FromStr;
use std::{env, fs};
use syn::{Item, ItemConst};

const REDIR_FD: &str = "pam_modutil_redirect_fd";

fn main() {
    pam_impl::enable_pam_impl_cfg();
    let config = match PamImpl::CURRENT {
        PamImpl::LinuxPam => TestConfig {
            headers: vec![
                "<security/_pam_types.h>",
                "<security/pam_appl.h>",
                "<security/pam_ext.h>",
                "<security/pam_modules.h>",
                "<security/pam_modutil.h>",
            ],
            allow_types: vec![REDIR_FD],
            ignore_consts: vec![
                "__LINUX_PAM__",
                "__LINUX_PAM_MINOR__",
                "PAM_AUTHTOK_RECOVER_ERR",
            ],
        },
        PamImpl::OpenPam => TestConfig {
            headers: vec![
                "<security/pam_types.h>",
                "<security/openpam.h>",
                "<security/pam_appl.h>",
                "<security/pam_constants.h>",
            ],
            ignore_consts: vec!["OPENPAM_VERSION", "OPENPAM_RELEASE", "PAM_SOEXT"],
            ..Default::default()
        },
        PamImpl::Sun => TestConfig {
            headers: vec![
                "<security/pam_appl.h>",
                "<security/pam_modules.h>",
                "\"illumos_pam_impl.h\"",
            ],
            ..Default::default()
        },
        PamImpl::XSso => TestConfig {
            headers: vec!["\"xsso_pam_appl.h\""],
            ..Default::default()
        },
        other => panic!("PAM implementation {other:?} is not yet tested"),
    };
    generate_const_test(&config);
    generate_ctest(&config);
}

fn generate_const_test(config: &TestConfig) {
    let mut builder = bindgen::Builder::default()
        .header_contents("_.h", &config.header_contents())
        .merge_extern_blocks(true)
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .allowlist_var("(OPEN)?PAM_.*")
        .default_macro_constant_type(MacroTypeVariation::Signed);

    for &typ in config.allow_types.iter() {
        builder = builder.allowlist_type(typ);
    }

    let generated = builder.generate().unwrap();
    generated.write_to_file(test_file("bindgen.rs")).unwrap();
    let file = syn::parse_file(&generated.to_string()).unwrap();
    let mut tests = vec![
        "#[allow(deprecated, overflowing_literals)]".into(),
        "fn main() {".into(),
        format!(
            "assert_eq!(libpam_sys::pam_impl::PamImpl::CURRENT, libpam_sys::pam_impl::PamImpl::{:?});",
            PamImpl::CURRENT
        ),
    ];
    tests.extend(
        file.items
            .iter()
            .filter_map(|item| {
                if let Item::Const(item) = item {
                    Some(item)
                } else {
                    None
                }
            })
            .filter(|&item| config.should_check_const(item))
            .map(|item| {
                let name = item.ident.to_string();
                if let Some(stripped) = name.strip_prefix(&format!("{REDIR_FD}_")) {
                    format!("\
                        assert_eq!(generated::{name} as i32, libpam_sys::{REDIR_FD}::{stripped}.into());\
                        assert_eq!((generated::{name} as i32).try_into(), Ok(libpam_sys::{REDIR_FD}::{stripped}));\
                    ")
                } else {
                    format!("assert_eq!(generated::{name} as i32, libpam_sys::{name});")
                }
            }),
    );
    tests.push("}".into());
    let const_test = test_file("constant_test.rs");
    fs::write(&const_test, tests.join("\n")).unwrap();
    rustfmt(&const_test);
}

fn generate_ctest(config: &TestConfig) {
    let mut test = ctest::TestGenerator::new();
    test.cfg("pam_impl", Some(pam_impl_name!()));

    for &header in config.headers.iter() {
        if header.starts_with('"') {
            test.include(env::var("CARGO_MANIFEST_DIR").unwrap());
        }
        test.header(&header[1..header.len() - 1]);
    }
    // These are opaque structs.
    test.skip_struct(|name| matches!(name, "pam_handle" | "AppData"));
    test.skip_type(|name| matches!(name, "ConversationCallback" | "CleanupCallback"));
    test.type_name(|name, is_struct, is_union| {
        assert!(!is_union); // we scabbin'
        match (name, is_struct) {
            ("AppData", _) => "void".into(),
            (REDIR_FD, _) => format!("enum {REDIR_FD}"),
            ("passwd", _) => "struct passwd".into(),
            ("group", _) => "struct group".into(),
            ("spwd", _) => "struct spwd".into(),
            (name, true) => format!("struct {name}"),
            (other, false) => other.into(),
        }
    });
    test.field_name(|_, name| {
        match name {
            "type_" => "type",
            other => other,
        }
        .into()
    });

    //
    // Welcome to THE HACK ZONE.
    //

    // Define away constness because the various PAM implementations
    // have different const annotations and this will surely drive you crazy.
    test.define("const", Some(""));

    // Also replace all the `const`s with `mut`s in the ffi.rs file.
    let file_contents = include_str!("../src/lib.rs");
    let deconsted_file = test_file("ffi.rs");
    remove_consts(file_contents, &deconsted_file);

    test.generate(&deconsted_file, "ctest.rs");
}

fn remove_consts(file_contents: &str, out_file: impl AsRef<Path>) {
    let lines: Vec<_> = file_contents
        .lines()
        .filter(|&l| !l.starts_with("pub mod"))
        .collect();
    let file_contents = lines.join("\n");
    let deconstified = deconstify(
        TokenStream::from_str(&file_contents).unwrap(),
        &format_ident!("mut"),
    )
    .to_string();
    let out_file = out_file.as_ref();
    fs::write(out_file, deconstified).unwrap();
    rustfmt(out_file)
}

fn rustfmt(file: impl AsRef<Path>) {
    let status = Command::new(env!("CARGO"))
        .args(["fmt", "--", file.as_ref().to_str().unwrap()])
        .status()
        .unwrap();
    assert!(status.success(), "rustfmt exited with code {status}");
}

fn deconstify(stream: TokenStream, mut_token: &Ident) -> TokenStream {
    TokenStream::from_iter(stream.into_iter().map(|token| {
        match token {
            TokenTree::Group(g) => {
                TokenTree::Group(Group::new(g.delimiter(), deconstify(g.stream(), mut_token)))
                    .into_token_stream()
            }
            // Remove all 'consts' from the file and replace them with 'mut'.
            TokenTree::Ident(id) if id == "const" => mut_token.into_token_stream(),
            other => other.into_token_stream(),
        }
    }))
}

fn test_file(name: impl AsRef<str>) -> String {
    format!("{}/{}", env::var("OUT_DIR").unwrap(), name.as_ref())
}

#[derive(Default)]
struct TestConfig {
    headers: Vec<&'static str>,
    allow_types: Vec<&'static str>,
    ignore_consts: Vec<&'static str>,
}

impl TestConfig {
    fn header_contents(&self) -> String {
        let vec: Vec<_> = self
            .headers
            .iter()
            .map(|h| format!("#include {h}\n"))
            .collect();
        vec.join("")
    }

    fn should_check_const(&self, item: &ItemConst) -> bool {
        !self
            .ignore_consts
            .contains(&item.ident.to_string().as_ref())
    }
}