changeset 108:e97534be35e3

Make some proc macros for doing cfg-like stuff for PAM impls.
author Paul Fisher <paul@pfish.zone>
date Sat, 28 Jun 2025 00:34:45 -0400
parents 49c6633f6fd2
children bb465393621f
files Cargo.lock Cargo.toml build.rs libpam-sys/Cargo.toml libpam-sys/README.md libpam-sys/build.rs libpam-sys/libpam-sys-impls/Cargo.toml libpam-sys/libpam-sys-impls/build.rs libpam-sys/libpam-sys-impls/src/lib.rs libpam-sys/src/constants.rs libpam-sys/src/lib.rs src/constants.rs src/libpam/answer.rs src/libpam/pam_ffi.rs src/libpam/question.rs src/logging.rs testharness/Cargo.toml testharness/container/Containerfile.orig
diffstat 18 files changed, 542 insertions(+), 234 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.lock	Thu Jun 26 22:42:32 2025 -0400
+++ b/Cargo.lock	Sat Jun 28 00:34:45 2025 -0400
@@ -145,6 +145,12 @@
 checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
 
 [[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
 name = "home"
 version = "0.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -217,9 +223,21 @@
 
 [[package]]
 name = "libpam-sys"
-version = "0.0.8-alpha0"
+version = "0.1.0"
 dependencies = [
  "bindgen 0.69.5",
+ "libpam-sys-impls",
+]
+
+[[package]]
+name = "libpam-sys-impls"
+version = "0.0.1"
+dependencies = [
+ "bindgen 0.72.0",
+ "proc-macro2",
+ "quote",
+ "strum",
+ "syn",
 ]
 
 [[package]]
@@ -272,6 +290,7 @@
  "bindgen 0.72.0",
  "bitflags",
  "libc",
+ "libpam-sys",
  "memoffset",
  "num_enum",
  "regex",
@@ -473,6 +492,28 @@
 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
 [[package]]
+name = "strum"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
+[[package]]
 name = "syn"
 version = "2.0.104"
 source = "registry+https://github.com/rust-lang/crates.io-index"
--- a/Cargo.toml	Thu Jun 26 22:42:32 2025 -0400
+++ b/Cargo.toml	Sat Jun 28 00:34:45 2025 -0400
@@ -1,5 +1,5 @@
 [workspace]
-members = ["libpam-sys", "testharness"]
+members = ["libpam-sys", "libpam-sys/libpam-sys-impls", "testharness"]
 resolver = "2"
 
 [workspace.package]
@@ -29,10 +29,10 @@
 # with your system's PAM.
 link = []
 
-# Enable this to get access to Linux-PAM extensions.
-linux-pam-extensions = []
-# Enable this to get access to OpenPAM extensions.
-openpam-extensions = []
+basic-ext = []
+illumos-ext = []
+linux-pam-ext = []
+openpam-ext = []
 
 # This feature exists only for testing.
 test-install = []
@@ -42,6 +42,7 @@
 libc = "0.2.97"
 memoffset = "0.9.1"
 num_enum = "0.7.3"
+libpam-sys = { path = "libpam-sys" }
 
 [dev-dependencies]
 regex = "1.11.1"
--- a/build.rs	Thu Jun 26 22:42:32 2025 -0400
+++ b/build.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -11,7 +11,7 @@
             .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
             .blocklist_type("pam_handle")
             .blocklist_type("pam_conv")
-            .allowlist_var(".*")
+            .blocklist_var(".*")
             .allowlist_function("pam_start")
             .allowlist_function("pam_[gs]et_item")
             .allowlist_function("pam_get_(user|authtok)")
--- a/libpam-sys/Cargo.toml	Thu Jun 26 22:42:32 2025 -0400
+++ b/libpam-sys/Cargo.toml	Sat Jun 28 00:34:45 2025 -0400
@@ -2,18 +2,14 @@
 name = "libpam-sys"
 description = "Low-level bindings for PAM (Pluggable Authentication Modules)"
 links = "pam"
-version.workspace = true
+version = "0.1.0"
 authors.workspace = true
 repository.workspace = true
 edition.workspace = true
 rust-version.workspace = true
 
-[features]
-default = ["basic-ext"]
-use-system-headers = []
-basic-ext = []
-linux-pam-ext = []
-openpam-ext = []
+[dependencies]
+libpam-sys-impls = { path = "libpam-sys-impls" }
 
 [build-dependencies]
-bindgen = "0.69.5"
\ No newline at end of file
+bindgen = "0.69.5"
--- a/libpam-sys/README.md	Thu Jun 26 22:42:32 2025 -0400
+++ b/libpam-sys/README.md	Sat Jun 28 00:34:45 2025 -0400
@@ -2,17 +2,17 @@
 
 This crate provides low-level access to PAM.
 
-## Options
+## Configuration
 
-With no features at all enabled, only the functions specified by the [X/SSO PAM specification][xsso] are available.
+By default, this crate guesses your system's PAM implementation based upon your OS.
 
-- `extras` (enabled by default): Common extensions to PAM, shared by both OpenPAM and Linux-PAM.
-  This includes, most notably, `pam_get_authtok`.
-  *Illumos does not provide this.*
-- `illumos-ext`: Extensions provided only by Illumos.
-- `linux-pam-ext`: Extensions provided only by Linux-PAM.
-- `openpam-ext`: Extensions provided by OpenPAM.
-- `use-system-headers`: Source constants from your PAM system headers.
+- Linux: `LinuxPam`
+- BSDs, including Mac OS: `OpenPam`
+- Illumos/Solaris: `Illumos`
+- Unknown: `OpenPamMinimal`
+
+Each implementation exports all the functionality available in its respective PAM library.
+`OpenPamMinimal` is a subset that includes only the functions available in the spec, and the constants shared in common between OpenPAM and Illumos' implementation. 
 
 ## References
 
--- a/libpam-sys/build.rs	Thu Jun 26 22:42:32 2025 -0400
+++ b/libpam-sys/build.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -1,143 +1,3 @@
-use bindgen::MacroTypeVariation;
-use std::error::Error;
-use std::fmt::{Debug, Display, Formatter};
-use std::path::PathBuf;
-use std::{env, fs};
-
-enum PamImpl {
-    Illumos,
-    LinuxPam,
-    OpenPam,
-}
-
-#[derive(Debug)]
-struct InvalidEnum(String);
-
-impl Display for InvalidEnum {
-    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        write!(f, "invalid PAM impl {:?}", self.0)
-    }
-}
-
-impl Error for InvalidEnum {}
-
-impl TryFrom<&str> for PamImpl {
-    type Error = InvalidEnum;
-    fn try_from(value: &str) -> Result<Self, Self::Error> {
-        Ok(match value {
-            "illumos" => Self::Illumos,
-            "linux-pam" => Self::LinuxPam,
-            "openpam" => Self::OpenPam,
-            other => return Err(InvalidEnum(other.to_owned())),
-        })
-    }
-}
-
-impl Debug for PamImpl {
-    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        Debug::fmt(
-            match self {
-                Self::Illumos => "illumos",
-                Self::LinuxPam => "linux-pam",
-                Self::OpenPam => "openpam",
-            },
-            f,
-        )
-    }
-}
-
 fn main() {
     println!("cargo:rustc-link-lib=pam");
-    let out_file = PathBuf::from(env::var("OUT_DIR").unwrap()).join("constants.rs");
-    let pam_impl = do_detection();
-
-    if cfg!(feature = "use-system-headers") {
-        let builder = bindgen::Builder::default()
-            .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
-            .blocklist_function(".*")
-            .blocklist_type(".*")
-            .allowlist_var(".*")
-            .default_macro_constant_type(MacroTypeVariation::Unsigned);
-
-        let builder = match pam_impl {
-            PamImpl::Illumos => builder.header_contents(
-                "illumos.h",
-                "\
-                        #include <security/pam_appl.h>
-                        #include <security/pam_modules.h>
-                    ",
-            ),
-            PamImpl::LinuxPam => builder.header_contents(
-                "linux-pam.h",
-                "\
-                        #include <security/_pam_types.h>
-                        #include <security/pam_appl.h>
-                        #include <security/pam_ext.h>
-                        #include <security/pam_modules.h>
-                    ",
-            ),
-            PamImpl::OpenPam => builder.header_contents(
-                "openpam.h",
-                "\
-                        #include <security/pam_types.h>
-                        #include <security/openpam.h>
-                        #include <security/pam_appl.h>
-                        #include <security/pam_constants.h>
-                    ",
-            ),
-        };
-        let bindings = builder.generate().unwrap();
-        bindings.write_to_file(out_file).unwrap();
-    } else {
-        // Just write empty data to the file to avoid conditional compilation
-        // shenanigans.
-        fs::write(out_file, "").unwrap();
-    }
 }
-
-fn do_detection() -> PamImpl {
-    println!(r#"cargo:rustc-check-cfg=cfg(pam_impl, values("illumos", "linux-pam", "openpam"))"#);
-    let pam_impl = _detect_internal();
-    println!("cargo:rustc-cfg=pam_impl={pam_impl:?}");
-    pam_impl
-}
-
-fn _detect_internal() -> PamImpl {
-    if let Some(pam_impl) = option_env!("LIBPAMSYS_PAM_IMPL") {
-        pam_impl.try_into().unwrap()
-    } else if cfg!(feature = "use-system-headers") {
-        // Detect which impl it is from system headers.
-        if header_exists("security/_pam_types.h") {
-            PamImpl::LinuxPam
-        } else if header_exists("security/openpam.h") {
-            PamImpl::OpenPam
-        } else if header_exists("security/pam_appl.h") {
-            PamImpl::Illumos
-        } else {
-            panic!("could not detect PAM implementation")
-        }
-    } else {
-        // Otherwise, guess what PAM impl we're using based on the OS.
-        if cfg!(target_os = "linux") {
-            PamImpl::LinuxPam
-        } else if cfg!(any(
-            target_os = "macos",
-            target_os = "freebsd",
-            target_os = "netbsd",
-            target_os = "dragonfly",
-            target_os = "openbsd"
-        )) {
-            PamImpl::OpenPam
-        } else {
-            PamImpl::Illumos
-        }
-    }
-}
-
-fn header_exists(header: &str) -> bool {
-    bindgen::Builder::default()
-        .blocklist_item(".*")
-        .header_contents("header.h", &format!("#include <{header}>"))
-        .generate()
-        .is_ok()
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-impls/Cargo.toml	Sat Jun 28 00:34:45 2025 -0400
@@ -0,0 +1,20 @@
+[package]
+name = "libpam-sys-impls"
+description = "Macros for use in libpam-sys."
+version = "0.0.1"
+edition = "2021"
+
+[lib]
+proc-macro = true
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[build-dependencies]
+bindgen = "0.72.0"
+strum = { version = "0.27.1", features = ["derive"] }
+
+
+[dependencies]
+quote = "1.0.40"
+syn = {  version = "2.0.104", default-features = false, features = ["parsing"] }
+proc-macro2 = "1.0.95"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-impls/build.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -0,0 +1,60 @@
+use strum::EnumString;
+
+fn main() {
+    let pam_impl = match option_env!("LIBPAMSYS_IMPL") {
+        // The default option: Guess what PAM impl we're using based on OS.
+        None => {
+            // Otherwise, guess what PAM impl we're using based on the OS.
+            if cfg!(target_os = "linux") {
+                PamImpl::LinuxPam
+            } else if cfg!(any(
+                target_os = "macos",
+                target_os = "freebsd",
+                target_os = "netbsd",
+                target_os = "dragonfly",
+                target_os = "openbsd"
+            )) {
+                PamImpl::OpenPam
+            } else if cfg!(any()) {
+                PamImpl::Illumos
+            } else {
+                PamImpl::MinimalOpenPam
+            }
+        }
+        Some("_detect") => {
+            // Detect which impl it is from system headers.
+            if header_exists("security/_pam_types.h") {
+                PamImpl::LinuxPam
+            } else if header_exists("security/openpam.h") {
+                PamImpl::OpenPam
+            } else if header_exists("security/pam_appl.h") {
+                // We figure we're *probably* on Illumos or something like that.
+                PamImpl::Illumos
+            } else {
+                // If all else fails, assume the bare minimum.
+                PamImpl::MinimalOpenPam
+            }
+        }
+        Some(other) => match PamImpl::try_from(other) {
+            Ok(i) => i,
+            Err(_) => panic!("unknown PAM implementation {other:?}")
+        }
+    };
+    println!("cargo:rustc-env=LIBPAMSYS_IMPL={pam_impl:?}");
+}
+
+#[derive(Debug, EnumString)]
+enum PamImpl {
+    Illumos,
+    LinuxPam,
+    OpenPam,
+    MinimalOpenPam,
+}
+
+fn header_exists(header: &str) -> bool {
+    bindgen::Builder::default()
+        .blocklist_item(".*")
+        .header_contents("header.h", &format!("#include <{header}>"))
+        .generate()
+        .is_ok()
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-impls/src/lib.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -0,0 +1,321 @@
+use proc_macro as pm;
+use proc_macro2::{Delimiter, Group, Literal, Span, TokenStream, TokenTree};
+use quote::{format_ident, quote};
+use std::fmt::Display;
+use syn::Lit;
+
+/// A `cfg`-like attribute macro for code specific to one PAM implementation.
+///
+/// Different versions of PAM export different functions and have some
+/// meaningful internal implementation differences, like the way `pam_conv`
+/// is handled (see [the Linux-PAM man page for details][man7]).
+///
+/// ```
+/// # use libpam_sys_impls::cfg_pam_impl;
+/// #[cfg_pam_impl("illumos")]
+/// fn do_something() { /* illumos-only code */ }
+///
+/// #[cfg_pam_impl(not("illumos"))]
+/// fn do_something() { /* non-illumos code */ }
+///
+/// #[cfg_pam_impl(any("linux-pam", "openpam"))]
+/// fn do_something_else() { /* Linux-PAM or OpenPAM */ }
+///
+/// #[cfg_pam_impl(not(any("illumos", "openpam")))]
+/// fn do_a_third_thing() { /* Neither Illumos nor OpenPAM */ }
+///
+/// #[cfg_pam_impl(any())]
+/// fn this_will_never_build() { /* why would you do this? */ }
+/// ```
+///
+/// [man7]: https://man7.org/linux/man-pages/man3/pam_conv.3.html
+#[proc_macro_attribute]
+pub fn cfg_pam_impl(attr: pm::TokenStream, item: pm::TokenStream) -> pm::TokenStream {
+    eprintln!("Got TokenStream: {:?}", attr);
+    Predicate::parse(attr.into(), None)
+        .map(|p| {
+            if p.matches(pam_impl_str()) {
+                item
+            } else {
+                pm::TokenStream::new()
+            }
+        })
+        .unwrap_or_else(|e| syn::Error::from(e).into_compile_error().into())
+}
+
+#[derive(Debug)]
+enum Error {
+    WithSpan(syn::Error),
+    WithoutSpan(String),
+}
+
+impl Error {
+    fn new<D: Display>(span: Option<Span>, msg: D) -> Self {
+        match span {
+            Some(span) => syn::Error::new(span, msg).into(),
+            None => Self::WithoutSpan(msg.to_string()),
+        }
+    }
+}
+
+impl From<syn::Error> for Error {
+    fn from(value: syn::Error) -> Self {
+        Self::WithSpan(value)
+    }
+}
+
+impl From<String> for Error {
+    fn from(value: String) -> Self {
+        Self::WithoutSpan(value)
+    }
+}
+
+impl From<Error> for syn::Error {
+    fn from(value: Error) -> Self {
+        match value {
+            Error::WithSpan(e) => e,
+            Error::WithoutSpan(s) => syn::Error::new(Span::call_site(), s),
+        }
+    }
+}
+
+type Result<T> = std::result::Result<T, Error>;
+
+#[derive(Debug)]
+enum Predicate {
+    Literal(String),
+    Any(Vec<String>),
+    Not(Box<Predicate>),
+}
+
+impl Predicate {
+    fn matches(&self, value: &str) -> bool {
+        match self {
+            Self::Literal(literal) => value == literal,
+            Self::Not(pred) => !pred.matches(value),
+            Self::Any(options) => {
+                options.iter().any(|s| s == value)
+            }
+        }
+    }
+
+    fn parse(stream: TokenStream, span: Option<Span>) -> Result<Self> {
+        let mut iter = stream.into_iter();
+        let pred = match iter.next() {
+            None => return error(span, "a PAM implementation predicate must be provided"),
+            Some(TokenTree::Literal(lit)) => Self::Literal(Self::string_lit(lit)?),
+            Some(TokenTree::Ident(id)) => {
+                let next = Self::parens(iter.next(), span)?;
+                match id.to_string().as_str() {
+                    "not" => Self::Not(Box::new(Self::parse(next.stream(), Some(next.span()))?)),
+                    "any" => Self::Any(Self::parse_any(next)?),
+                    _ => return unexpected(&id.into(), "\"not\" or \"any\""),
+                }
+            }
+            Some(other) => return unexpected(&other, "\"not\", \"any\", or a string literal"),
+        };
+        // Check for anything after. We only allow a comma and nothing else.
+        if maybe_comma(iter.next())? {
+            if let Some(next) = iter.next() {
+                return unexpected(&next, "nothing");
+            }
+        }
+        Ok(pred)
+    }
+
+    fn parens(tree: Option<TokenTree>, mut span: Option<Span>) -> Result<Group> {
+        if let Some(tree) = tree {
+            span = Some(tree.span());
+            if let TokenTree::Group(g) = tree {
+                if g.delimiter() == Delimiter::Parenthesis {
+                    return Ok(g);
+                }
+            }
+        }
+        Err(Error::new(span, "expected function-call syntax"))
+    }
+
+    fn parse_any(g: Group) -> Result<Vec<String>> {
+        let mut output = Vec::new();
+        let mut iter = g.stream().into_iter();
+        loop {
+            match iter.next() {
+                None => break,
+                Some(TokenTree::Literal(lit)) => {
+                    output.push(Self::string_lit(lit)?);
+                    if !maybe_comma(iter.next())? {
+                        break
+                    }
+                },
+                Some(other) => return unexpected(&other, "string literal"),
+            }
+        }
+        Ok(output)
+    }
+
+    fn string_lit(lit: Literal) -> Result<String> {
+        let tree: TokenTree = lit.clone().into();
+        match syn::parse2::<Lit>(tree.into())? {
+            Lit::Str(s) => Ok(s.value()),
+            _ => unexpected(&lit.into(), "string literal"),
+        }
+    }
+}
+
+fn error<T, M: Display>(span: Option<Span>, message: M) -> Result<T> {
+    Err(Error::new(span, message))
+}
+
+fn unexpected<T>(tree: &TokenTree, want: &str) -> Result<T> {
+    error(
+        Some(tree.span()),
+        format!("expected {want}; got unexpected token {tree}"),
+    )
+}
+
+fn maybe_comma(next: Option<TokenTree>) -> Result<bool> {
+    match next {
+        None => Ok(false),
+        Some(tree) => {
+            if let TokenTree::Punct(p) = &tree {
+                if p.as_char() == ',' {
+                    return Ok(true);
+                }
+            }
+            unexpected(&tree, "',' or ')'")
+        }
+    }
+}
+
+/// A proc macro that outputs the PAM implementation macro and const.
+#[proc_macro]
+pub fn pam_impl_enum(data: pm::TokenStream) -> pm::TokenStream {
+    if !data.is_empty() { panic!("unexpected stuff in pam_impl_enum!()") }
+
+    let variant = format_ident!("{}", pam_impl_str());
+
+    quote!(
+        /// The PAM implementations supported by `libpam-sys`.
+        #[non_exhaustive]
+        #[derive(Clone, Copy, Debug, PartialEq)]
+        pub enum PamImpl {
+            /// [Linux-PAM] is provided by most Linux implementations.
+            /// 
+            /// [Linux-PAM]: https://github.com/linux-pam/linux-pam
+            LinuxPam,
+            /// [OpenPAM] is used by most BSD distributions, including Mac OS X.
+            /// 
+            /// [OpenPAM]: https://git.des.dev/OpenPAM/OpenPAM
+            OpenPam,
+            /// [Illumos PAM] is used on Illumos and Solaris systems.
+            ///
+            /// [Illumos PAM]: https://code.illumos.org/plugins/gitiles/illumos-gate/+/refs/heads/master/usr/src/lib/libpam
+            Illumos,
+            /// Only the functionality in [the PAM spec],
+            /// with OpenPAM/Illumos constants.
+            /// 
+            /// [the PAM spec]: https://pubs.opengroup.org/onlinepubs/8329799/toc.htm
+            MinimalOpenPam,
+        }
+
+        #[doc = concat!("This version of libpam-sys was built for **", stringify!(#variant), "**.")]
+        pub const LIBPAMSYS_IMPL: PamImpl = PamImpl::#variant;
+    ).into()
+}
+
+/// String literal of the name of the PAM implementation this was built for.
+///
+/// The value is the string value of `libpamsys::PamImpl`.
+#[proc_macro]
+pub fn pam_impl_name(data: pm::TokenStream) -> pm::TokenStream {
+    if !data.is_empty() {
+        panic!("pam_impl_name! does not take any input")
+    }
+    pm::TokenTree::Literal(pm::Literal::string(pam_impl_str())).into()
+}
+
+fn pam_impl_str() -> &'static str {
+    env!("LIBPAMSYS_IMPL")
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn parse(tree: TokenStream) -> Predicate {
+        Predicate::parse(tree, None).unwrap()
+    }
+
+    #[test]
+    fn test_parse() {
+        macro_rules! cases {
+            ($(($($i:tt)*)),* $(,)?) => { [ $( quote!($($i)*) ),* ] };
+        }
+
+        let good = cases![
+            ("this"),
+            (any("this", "that", "the other")),
+            (not("the bees")),
+            (not(any("of", "those"))),
+            (not(not("saying it"))),
+            (any("trailing", "comma", "allowed",)),
+            ("even on a singleton",),
+            (not("forbidden here either",)),
+            (not(not(any("this", "is", "stupid"),),),),
+        ];
+        for tree in good {
+            parse(tree);
+        }
+        let bad = cases![
+            (),
+            (wrong),
+            (wheel::of::fortune),
+            ("invalid", "syntax"),
+            (any(any)),
+            (any),
+            (not),
+            (not(any)),
+            ("too many commas",,),
+            (any("too", "many",, "commas")),
+            (not("the commas",,,)),
+            (9),
+            (any("123", 8)),
+            (not(666)),
+        ];
+        for tree in bad {
+            Predicate::parse(tree, None).unwrap_err();
+        }
+    }
+
+    #[test]
+    fn test_match() {
+        macro_rules! cases {
+            ($(($e:expr, ($($i:tt)*))),* $(,)?) => {
+                [$(($e, quote!($($i)*))),*]
+            }
+        }
+        let matching = cases![
+            ("Illumos", (any("Illumos", "OpenPam"))),
+            ("LinuxPam", (not("OpenPam"))),
+            ("Other", (not(any("This", "That")))),
+            ("OpenPam", (not(not("OpenPam")))),
+            ("Anything", (not(any()))),
+        ];
+        for (good, tree) in matching {
+            let pred = parse(tree);
+            assert!(pred.matches(good))
+        }
+
+        let nonmatching = cases![
+            ("LinuxPam", (not("LinuxPam"))),
+            ("Illumos", ("LinuxPam")),
+            ("OpenPam", (any("LinuxPam", "Illumos"))),
+            ("One", (not(any("One", "Another")))),
+            ("Negatory", (not(not("Affirmative")))),
+        ];
+        for (bad, tree) in nonmatching {
+            let pred = parse(tree);
+            assert!(!pred.matches(bad))
+        }
+    }
+}
--- a/libpam-sys/src/constants.rs	Thu Jun 26 22:42:32 2025 -0400
+++ b/libpam-sys/src/constants.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -1,17 +1,20 @@
 //! All the constants.
-#![allow(dead_code)]
+
+// We have to enable these otherwise we get lit up with warnings
+// during conditional compilation.
+#![allow(dead_code, unused_imports)]
 
 /// Macro to make defining a bunch of constants way easier.
 macro_rules! define {
-        ($(#[$attr:meta])* $($name:ident = $value:expr);+$(;)?) => {
-            define!(
-                @meta { $(#[$attr])* }
-                $(pub const $name: u32 = $value;)+
-            );
-        };
-        (@meta $m:tt $($i:item)+) => { define!(@expand $($m $i)+); };
-        (@expand $({ $(#[$m:meta])* } $i:item)+) => {$($(#[$m])* $i)+};
-    }
+    ($(#[$attr:meta])* $($name:ident = $value:expr);+$(;)?) => {
+        define!(
+            @meta { $(#[$attr])* }
+            $(pub const $name: u32 = $value;)+
+        );
+    };
+    (@meta $m:tt $($i:item)+) => { define!(@expand $($m $i)+); };
+    (@expand $({ $(#[$m:meta])* } $i:item)+) => {$($(#[$m])* $i)+};
+}
 
 #[cfg(feature = "use-system-headers")]
 pub use system_headers::*;
@@ -55,7 +58,7 @@
     pub use super::linux_pam::*;
 
     #[cfg(not(pam_impl = "linux-pam"))]
-    pub use super::shared::*;
+    pub use super::illumos_openpam::*;
 
     #[cfg(pam_impl = "illumos")]
     pub use super::illumos::*;
@@ -66,7 +69,7 @@
 
 /// Constants extracted from PAM header files.
 mod system_headers {
-    include!(concat!(env!("OUT_DIR"), "/constants.rs"));
+    // include!(concat!(env!("OUT_DIR"), "/constants.rs"));
 }
 
 /// Constants used by Linux-PAM.
@@ -114,8 +117,13 @@
         PAM_ESTABLISH_CRED = 0x0002;
         PAM_DELETE_CRED = 0x0004;
         PAM_REINITIALIZE_CRED = 0x0008;
+        PAM_REFRESH_CRED = 0x0010;
 
         PAM_CHANGE_EXPIRED_AUTHTOK = 0x0020;
+
+        PAM_PRELIM_CHECK = 0x4000;
+        PAM_UPDATE_AUTHTOK = 0x2000;
+        PAM_DATA_REPLACE = 0x20000000;
     );
 
     define!(
@@ -138,7 +146,7 @@
 }
 
 /// Constants shared between Illumos and OpenPAM.
-mod shared {
+mod illumos_openpam {
     define!(
         /// An error code.
         PAM_OPEN_ERR = 1;
--- a/libpam-sys/src/lib.rs	Thu Jun 26 22:42:32 2025 -0400
+++ b/libpam-sys/src/lib.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -1,22 +1,25 @@
 #![doc = include_str!("../README.md")]
 //!
-//! ## Version info
-//!
-//! This documentation was built with the following configuration:
-//!
-#![cfg_attr(pam_impl = "illumos", doc = "- PAM implementation: Illumos")]
-#![cfg_attr(pam_impl = "linux-pam", doc = "- PAM implementation: Linux-PAM")]
-#![cfg_attr(pam_impl = "openpam", doc = "- PAM implementation: OpenPAM")]
+//! ## PAM implementation
 //!
-#![cfg_attr(feature = "basic-ext", doc = "- Includes common extensions")]
-#![cfg_attr(feature = "linux-pam-ext", doc = "- Includes Linux-PAM extensions")]
-#![cfg_attr(feature = "openpam-ext", doc = "- Includes OpenPAM extensions")]
-#![cfg_attr(
-    feature = "use-system-headers",
-    doc = "- Built with system header files"
-)]
+#![doc = concat!("This documentation was built for the **", pam_impl_name!(), "** implementation.")]
+
 mod constants;
+
+libpam_sys_impls::pam_impl_enum!();
+
+#[doc(inline)]
+pub use libpam_sys_impls::{cfg_pam_impl, pam_impl_name};
+
 pub mod helpers;
 
 #[doc(inline)]
 pub use constants::*;
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn test() {
+        panic!("The pam impl is {:?}", super::LIBPAMSYS_IMPL);
+    }
+}
--- a/src/constants.rs	Thu Jun 26 22:42:32 2025 -0400
+++ b/src/constants.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -192,11 +192,13 @@
     Ignore = pam_ffi::PAM_IGNORE,
     Abort = pam_ffi::PAM_ABORT,
     AuthTokExpired = pam_ffi::PAM_AUTHTOK_EXPIRED,
+    #[cfg(feature = "basic-ext")]
     ModuleUnknown = pam_ffi::PAM_MODULE_UNKNOWN,
+    #[cfg(feature = "basic-ext")]
     BadItem = pam_ffi::PAM_BAD_ITEM,
-    #[cfg(feature = "linux-pam-extensions")]
+    #[cfg(feature = "linux-pam-ext")]
     ConversationAgain = pam_ffi::PAM_CONV_AGAIN,
-    #[cfg(feature = "linux-pam-extensions")]
+    #[cfg(feature = "linux-pam-ext")]
     Incomplete = pam_ffi::PAM_INCOMPLETE,
 }
 
@@ -246,8 +248,8 @@
     fn test_enums() {
         assert_eq!(Ok(()), ErrorCode::result_from(0));
         assert_eq!(
-            pam_ffi::PAM_BAD_ITEM as i32,
-            ErrorCode::result_to_c::<()>(Err(ErrorCode::BadItem))
+            pam_ffi::PAM_SESSION_ERR as i32,
+            ErrorCode::result_to_c::<()>(Err(ErrorCode::SessionError))
         );
         assert_eq!(
             Err(ErrorCode::Abort),
--- a/src/libpam/answer.rs	Thu Jun 26 22:42:32 2025 -0400
+++ b/src/libpam/answer.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -248,7 +248,7 @@
         }
     }
 
-    #[cfg(feature = "linux-pam-extensions")]
+    #[cfg(feature = "linux-pam-ext")]
     fn test_round_trip_linux() {
         use crate::conv::{BinaryData, BinaryQAndA, RadioQAndA};
         let binary_msg = {
--- a/src/libpam/pam_ffi.rs	Thu Jun 26 22:42:32 2025 -0400
+++ b/src/libpam/pam_ffi.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -103,6 +103,8 @@
     }
 }
 
+pub use libpam_sys::*;
+
 type pam_handle = LibPamHandle;
 type pam_conv = LibPamConversation<'static>;
 
--- a/src/libpam/question.rs	Thu Jun 26 22:42:32 2025 -0400
+++ b/src/libpam/question.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -1,6 +1,6 @@
 //! Data and types dealing with PAM messages.
 
-#[cfg(feature = "linux-pam-extensions")]
+#[cfg(feature = "linux-pam-ext")]
 use crate::conv::{BinaryQAndA, RadioQAndA};
 use crate::conv::{ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA};
 use crate::libpam::conversation::OwnedMessage;
@@ -188,13 +188,13 @@
     /// An informational message.
     TextInfo = pam_ffi::PAM_TEXT_INFO,
     /// Yes/No/Maybe conditionals. A Linux-PAM extension.
-    #[cfg(feature = "linux-pam-extensions")]
+    #[cfg(feature = "linux-pam-ext")]
     RadioType = pam_ffi::PAM_RADIO_TYPE,
     /// For server–client non-human interaction.
     ///
     /// NOT part of the X/Open PAM specification.
     /// A Linux-PAM extension.
-    #[cfg(feature = "linux-pam-extensions")]
+    #[cfg(feature = "linux-pam-ext")]
     BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT,
 }
 
@@ -236,13 +236,13 @@
             Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
             Message::Error(p) => alloc(Style::ErrorMsg, p.question()),
             Message::Info(p) => alloc(Style::TextInfo, p.question()),
-            #[cfg(feature = "linux-pam-extensions")]
+            #[cfg(feature = "linux-pam-ext")]
             Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
-            #[cfg(feature = "linux-pam-extensions")]
+            #[cfg(feature = "linux-pam-ext")]
             Message::BinaryPrompt(p) => Ok((Style::BinaryPrompt, unsafe {
                 CHeapBox::cast(CBinaryData::alloc(p.question())?)
             })),
-            #[cfg(not(feature = "linux-pam-extensions"))]
+            #[cfg(not(feature = "linux-pam-ext"))]
             Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError),
         }?;
         Ok(Self {
@@ -261,12 +261,12 @@
             // in the Question. If it's not a supported format, we skip it.
             if let Ok(style) = Style::try_from(self.style) {
                 let _ = match style {
-                    #[cfg(feature = "linux-pam-extensions")]
+                    #[cfg(feature = "linux-pam-ext")]
                     Style::BinaryPrompt => self
                         .data
                         .as_ref()
                         .map(|p| CBinaryData::zero_contents(CHeapBox::as_ptr(p).cast())),
-                    #[cfg(feature = "linux-pam-extensions")]
+                    #[cfg(feature = "linux-pam-ext")]
                     Style::RadioType => self
                         .data
                         .as_ref()
@@ -301,9 +301,9 @@
                 Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)),
                 Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)),
                 Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)),
-                #[cfg(feature = "linux-pam-extensions")]
+                #[cfg(feature = "linux-pam-ext")]
                 Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)),
-                #[cfg(feature = "linux-pam-extensions")]
+                #[cfg(feature = "linux-pam-ext")]
                 Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())),
             }
         };
@@ -353,7 +353,7 @@
             }
 
             #[test]
-            #[cfg(not(feature = "linux-pam-extensions"))]
+            #[cfg(not(feature = "linux-pam-ext"))]
             fn no_linux_extensions() {
                 use crate::conv::{BinaryQAndA, RadioQAndA};
                 <$typ>::new(&[
@@ -363,7 +363,7 @@
             }
 
             #[test]
-            #[cfg(feature = "linux-pam-extensions")]
+            #[cfg(feature = "linux-pam-ext")]
             fn linux_extensions() {
                 let interrogation = Box::pin(<$typ>::new(&[
                     BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
--- a/src/logging.rs	Thu Jun 26 22:42:32 2025 -0400
+++ b/src/logging.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -15,33 +15,20 @@
 //! and may even itself implement `log::Log`, but that interface is not exposed
 //! to the generic PAM user.
 
-#[cfg(feature = "link")]
+#[cfg(all(feature = "link", pam_impl = "openpam"))]
 mod levels {
-    pub use internal::*;
-    #[cfg(pam_impl = "linux-pam")]
-    mod internal {
-        use crate::libpam::pam_ffi;
-        pub const ERROR: u32 = pam_ffi::LOG_ERR;
-        pub const WARN: u32 = pam_ffi::LOG_WARNING;
-        pub const INFO: u32 = pam_ffi::LOG_INFO;
-        pub const DEBUG: u32 = pam_ffi::LOG_DEBUG;
-    }
-    #[cfg(pam_impl = "openpam")]
-    mod internal {
-        use crate::libpam::pam_ffi;
-        pub const ERROR: u32 = pam_ffi::PAM_LOG_ERROR;
-        pub const WARN: u32 = pam_ffi::PAM_LOG_NOTICE;
-        pub const INFO: u32 = pam_ffi::PAM_LOG_VERBOSE;
-        pub const DEBUG: u32 = pam_ffi::PAM_LOG_DEBUG;
-    }
+    use crate::libpam::pam_ffi;
+    pub const ERROR: u32 = pam_ffi::PAM_LOG_ERROR;
+    pub const WARN: u32 = pam_ffi::PAM_LOG_NOTICE;
+    pub const INFO: u32 = pam_ffi::PAM_LOG_VERBOSE;
+    pub const DEBUG: u32 = pam_ffi::PAM_LOG_DEBUG;
 }
-
-#[cfg(not(feature = "link"))]
+#[cfg(not(all(feature = "link", pam_impl = "openpam")))]
 mod levels {
-    pub const ERROR: u32 = 2255887;
-    pub const WARN: u32 = 7265000;
-    pub const INFO: u32 = 7762323;
-    pub const DEBUG: u32 = 8675309;
+    pub const ERROR: u32 = libc::LOG_ERR as u32;
+    pub const WARN: u32 = libc::LOG_WARNING as u32;
+    pub const INFO: u32 = libc::LOG_INFO as u32;
+    pub const DEBUG: u32 = libc::LOG_DEBUG as u32;
 }
 
 /// An entry to be added to the log.
--- a/testharness/Cargo.toml	Thu Jun 26 22:42:32 2025 -0400
+++ b/testharness/Cargo.toml	Sat Jun 28 00:34:45 2025 -0400
@@ -11,8 +11,9 @@
 crate-type = ["cdylib"]
 
 [features]
-linux-pam-extensions = ["nonstick/linux-pam-extensions"]
-openpam-extensions = ["nonstick/openpam-extensions"]
+illumos-ext = ["nonstick/illumos-ext"]
+linux-pam-ext = ["nonstick/linux-pam-ext"]
+openpam-ext = ["nonstick/openpam-ext"]
 test-install = []
 
 [dependencies]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/testharness/container/Containerfile.orig	Sat Jun 28 00:34:45 2025 -0400
@@ -0,0 +1,6 @@
+FROM docker.io/debian:stable
+
+RUN apt-get -y update \
+    && apt-get -y upgrade \
+    && apt-get install -y cargo mercurial wget build-essential \
+    && apt-get clean
\ No newline at end of file