changeset 136:efbc235f01d3

Separate libpam-sys-helpers from libpam-sys. This separates the parts of libpam-sys that don't need linking against libpam from the parts that do need to link against libpam.
author Paul Fisher <paul@pfish.zone>
date Thu, 03 Jul 2025 14:28:04 -0400
parents b52594841480
children 88627c057709
files Cargo.lock Cargo.toml build.rs libpam-sys/Cargo.lock libpam-sys/Cargo.toml libpam-sys/LICENSE libpam-sys/README.md libpam-sys/build.rs libpam-sys/libpam-sys-helpers/Cargo.toml libpam-sys/libpam-sys-helpers/LICENSE libpam-sys/libpam-sys-helpers/README.md libpam-sys/libpam-sys-helpers/build.rs libpam-sys/libpam-sys-helpers/src/constants.rs libpam-sys/libpam-sys-helpers/src/lib.rs libpam-sys/libpam-sys-helpers/src/memory.rs libpam-sys/libpam-sys-helpers/src/pam_impl.rs libpam-sys/libpam-sys-test/Cargo.toml libpam-sys/libpam-sys-test/build.rs libpam-sys/libpam-sys-test/tests/runner.rs libpam-sys/src/constants.rs libpam-sys/src/ffi.rs libpam-sys/src/helpers.rs libpam-sys/src/lib.rs libpam-sys/src/pam_impl.rs src/constants.rs src/libpam/conversation.rs src/logging.rs testharness/Cargo.toml
diffstat 28 files changed, 1940 insertions(+), 1620 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.lock	Thu Jul 03 11:14:49 2025 -0400
+++ b/Cargo.lock	Thu Jul 03 14:28:04 2025 -0400
@@ -3,6 +3,12 @@
 version = 3
 
 [[package]]
+name = "anyhow"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
+[[package]]
 name = "autocfg"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -15,6 +21,18 @@
 checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
 
 [[package]]
+name = "cargo_metadata"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46e3374c604fb39d1a2f35ed5e4a4e30e60d01fab49446e08f1b3e9a90aef202"
+dependencies = [
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+]
+
+[[package]]
 name = "equivalent"
 version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -37,6 +55,12 @@
 ]
 
 [[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
 name = "libc"
 version = "0.2.174"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -47,6 +71,15 @@
 version = "0.1.0"
 dependencies = [
  "libc",
+ "libpam-sys-helpers",
+ "num_enum",
+]
+
+[[package]]
+name = "libpam-sys-helpers"
+version = "0.1.0"
+dependencies = [
+ "libc",
  "num_enum",
 ]
 
@@ -72,11 +105,22 @@
  "bitflags",
  "libc",
  "libpam-sys",
+ "libpam-sys-helpers",
  "memoffset",
  "num_enum",
 ]
 
 [[package]]
+name = "nonstick-testharness"
+version = "0.0.8-alpha0"
+dependencies = [
+ "anyhow",
+ "nonstick",
+ "test-cdylib",
+ "thiserror",
+]
+
+[[package]]
 name = "num_enum"
 version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -132,6 +176,60 @@
 checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
 
 [[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+ "serde",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
 name = "syn"
 version = "2.0.104"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -143,6 +241,47 @@
 ]
 
 [[package]]
+name = "test-cdylib"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8f41b1f729f5ff5177beab62e5a9251e318df8386e260ab3c944cff502ee78d"
+dependencies = [
+ "cargo_metadata",
+ "serde",
+ "serde_json",
+ "toml",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
 name = "toml_datetime"
 version = "0.6.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
--- a/Cargo.toml	Thu Jul 03 11:14:49 2025 -0400
+++ b/Cargo.toml	Thu Jul 03 14:28:04 2025 -0400
@@ -1,13 +1,24 @@
+[workspace]
+members = ["testharness"]
+exclude = ["libpam-sys"]
+
+[workspace.package]
+license = "MIT"
+version = "0.0.8-alpha0"
+authors = ["Paul Fisher <paul@pfish.zone>"]
+edition = "2021"
+rust-version = "1.75.0"
+
 [package]
 name = "nonstick"
 description = "PAM bindings for Rust"
 readme = "README.md"
 keywords = ["pam", "ffi", "linux", "authentication"]
-license = "MIT"
-version = "0.0.8-alpha0"
-authors = ["Paul Fisher <paul@pfish.zone>"]
-edition = "2021"
-rust-version = "1.75.0"
+license.workspace = true
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+rust-version.workspace = true
 
 [features]
 default = ["link", "basic-ext"]
@@ -16,7 +27,7 @@
 #
 # This will fail if you have extensions enabled that are not compatible
 # with your system's PAM.
-link = []
+link = ["libpam-sys"]
 
 basic-ext = []
 linux-pam-ext = []
@@ -31,7 +42,8 @@
 libc = "0.2"
 memoffset = "0.9.1"
 num_enum = "0.7.3"
-libpam-sys = { path = "libpam-sys" }
+libpam-sys = { optional = true, path = "libpam-sys" }
+libpam-sys-helpers = { path = "libpam-sys/libpam-sys-helpers" }
 
 [build-dependencies]
-libpam-sys = { path = "libpam-sys" }
+libpam-sys-helpers = { path = "libpam-sys/libpam-sys-helpers" }
--- a/build.rs	Thu Jul 03 11:14:49 2025 -0400
+++ b/build.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -1,5 +1,5 @@
-use libpam_sys::pam_impl;
+use libpam_sys_helpers::pam_impl;
 
 fn main() {
-    println!("{}", pam_impl::enable_pam_impl_cfg())
+    pam_impl::enable_pam_impl_cfg()
 }
--- a/libpam-sys/Cargo.lock	Thu Jul 03 11:14:49 2025 -0400
+++ b/libpam-sys/Cargo.lock	Thu Jul 03 14:28:04 2025 -0400
@@ -241,6 +241,15 @@
 version = "0.1.0"
 dependencies = [
  "libc",
+ "libpam-sys-helpers",
+ "num_enum",
+]
+
+[[package]]
+name = "libpam-sys-helpers"
+version = "0.1.0"
+dependencies = [
+ "libc",
  "num_enum",
 ]
 
@@ -252,6 +261,7 @@
  "ctest",
  "libc",
  "libpam-sys",
+ "libpam-sys-helpers",
  "proc-macro2",
  "quote",
  "syn",
--- a/libpam-sys/Cargo.toml	Thu Jul 03 11:14:49 2025 -0400
+++ b/libpam-sys/Cargo.toml	Thu Jul 03 14:28:04 2025 -0400
@@ -1,6 +1,6 @@
 [workspace]
 resolver = "2"
-members = ["libpam-sys-test"]
+members = ["libpam-sys-helpers", "libpam-sys-test"]
 
 [workspace.package]
 version = "0.1.0"
@@ -8,6 +8,7 @@
 repository = "https://hg.pfish.zone/crates/nonstick/"
 edition = "2021"
 rust-version = "1.75.0"
+license = "MIT"
 
 [package]
 name = "libpam-sys"
@@ -18,14 +19,13 @@
 repository.workspace = true
 edition.workspace = true
 rust-version.workspace = true
-
-[features]
-default = ["helpers"]
-helpers = []
+license.workspace = true
 
 [dependencies]
+libpam-sys-helpers = { path = "libpam-sys-helpers" }
 libc = "0.2"
 num_enum = "0.7.4"
 
 [build-dependencies]
-libc = "0.2"
\ No newline at end of file
+libc = "0.2"
+libpam-sys-helpers = { path = "libpam-sys-helpers" }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/LICENSE	Thu Jul 03 14:28:04 2025 -0400
@@ -0,0 +1,20 @@
+Copyright 2025 Paul Fisher
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the “Software”),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom
+the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
--- a/libpam-sys/README.md	Thu Jul 03 11:14:49 2025 -0400
+++ b/libpam-sys/README.md	Thu Jul 03 14:28:04 2025 -0400
@@ -5,27 +5,17 @@
 
 If you're looking for a nice, safe, Rusty API to PAM, may I recommend [nonstick][nonstick]?
 
-## Configuration
-
-By default, this crate guesses your system's PAM implementation based upon your OS.
+## PAM implementations
 
-- Linux: `LinuxPam`
-- BSDs, including Mac OS: `OpenPam`
-- Illumos/Solaris: `Sun`
-- Unknown: `XSso`
+This crate detects the PAM implementation you have installed, or guesses based on the OS if that's unavailable.
+Supported PAM implementations are defined in the `pam_impl::PamImpl` enum.
+
+You can also explicitly specify the PAM implementation you want (if not detected correctly) by setting the `LIBPAMSYS_IMPL` environment variable **at build time**.
+All build-time configuration is performed by the build script of the [`libpam-sys-helpers` crate](https://crates.io/crates/libpam-sys-helpers).
 
 Each implementation exports all the functionality available in its respective PAM library.
 `XSso` exports only what is in the [X/SSO specification][xsso].
 
-## Cargo Features
-
-The `helpers` feature (optional, but on by default) exports two helpers for PAM memory management.
-
-- A struct for managing the difference in memory management between Linux-PAM and all other implementations.
-- A struct for handling the Linux-PAM–specific binary data payload structure.
-
-Neither are directly referenced elsewhere, and both allow you to bring your own storage abstractions.
-
 ## Testing
 
 Tests are mostly run through `libpam-sys-test`, which lives in the crate's workspace in its repository (along with [nonstick]).
--- a/libpam-sys/build.rs	Thu Jul 03 11:14:49 2025 -0400
+++ b/libpam-sys/build.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -1,93 +1,8 @@
 #![allow(unexpected_cfgs)]
 
-use std::env;
-use std::ffi::{c_void, CString};
-use std::fs;
-use std::ptr::NonNull;
-
-include!("src/pam_impl.rs");
+use libpam_sys_helpers::pam_impl;
 
 fn main() {
     println!("cargo:rustc-link-lib=pam");
-
-    let pam_impl = match option_env!("LIBPAMSYS_IMPL") {
-        // The default option: Guess what PAM impl we're using based on OS.
-        None | Some("") => detect(),
-        Some(other) => match PamImpl::try_from(other) {
-            Ok(i) => i,
-            Err(_) => {
-                panic!(
-                    "unknown PAM implementation {other:?}. valid LIBPAMSYS_IMPLs are {:?}, or unset to detect", PamImpl::items()
-                )
-            }
-        },
-    };
-    println!("{}", checkcfg());
-    let impl_str = format!("{pam_impl:?}");
-    println!("{}", implcfg(&impl_str));
-    println!("cargo:rustc-env=LIBPAMSYS_IMPL={impl_str}");
-    fs::write(
-        format!("{}/pam_impl_const.rs", env::var("OUT_DIR").unwrap()),
-        format!(
-            "\
-            impl PamImpl {{\
-            /// The implementation of libpam this was built for (`{impl_str}`).
-            pub const CURRENT: Self = Self::{impl_str};\
-            /// String version for internal consumption.
-            const CURRENT_STR: &'static str = {impl_str:?};\
-            }}
-            "
-        ),
-    )
-    .unwrap();
+    pam_impl::enable_pam_impl_cfg();
 }
-
-fn detect() -> PamImpl {
-    if let Some(lib) = LibPam::open() {
-        if lib.has("pam_syslog") {
-            return PamImpl::LinuxPam;
-        } else if lib.has("_openpam_log") {
-            return PamImpl::OpenPam;
-        } else if lib.has("__pam_get_authtok") {
-            return PamImpl::Sun;
-        }
-    }
-    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(target_os = "illumos", target_os = "solaris")) {
-        PamImpl::Sun
-    } else {
-        PamImpl::XSso
-    }
-}
-
-struct LibPam(NonNull<c_void>);
-
-impl LibPam {
-    fn open() -> Option<Self> {
-        NonNull::new(unsafe { libc::dlopen(b"libpam.so\0".as_ptr().cast(), libc::RTLD_LAZY) })
-            .map(Self)
-    }
-
-    fn has(&self, name: &str) -> bool {
-        let name = CString::new(name).unwrap();
-        let symbol = unsafe { libc::dlsym(self.0.as_ptr(), name.as_ptr()) };
-        !symbol.is_null()
-    }
-}
-
-impl Drop for LibPam {
-    fn drop(&mut self) {
-        unsafe {
-            libc::dlclose(self.0.as_ptr());
-        }
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-helpers/Cargo.toml	Thu Jul 03 14:28:04 2025 -0400
@@ -0,0 +1,16 @@
+[package]
+name = "libpam-sys-helpers"
+version.workspace = true
+authors.workspace = true
+repository.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+readme = "README.md"
+
+[dependencies]
+libc = "0.2"
+num_enum = "0.7"
+
+[build-dependencies]
+libc = "0.2"
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-helpers/LICENSE	Thu Jul 03 14:28:04 2025 -0400
@@ -0,0 +1,20 @@
+Copyright 2025 Paul Fisher
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the “Software”),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom
+the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-helpers/README.md	Thu Jul 03 14:28:04 2025 -0400
@@ -0,0 +1,57 @@
+# `libpam-sys-helpers`: cross-platform tools for libpam
+
+This crate contains tools for `libpam` that **don't directly link to `libpam.so`**.
+This allows for creation of `libpam` abstractions (e.g. test doubles) that don't require libpam to be present if they don't link directly into it.
+
+## Handling PAM implementations
+
+Different PAM implementations have different constants and some different behaviors.
+If you need to change your behavior based on PAM implementation, there are a few ways to do so.
+
+### Constants
+
+The current PAM implementation is available in `PamImpl::CURRENT`.
+This is present as a string literal macro in `pam_impl_name!`.
+
+### Conditional compilation
+
+This package provides custom `#[cfg]`s to compile based on the current PAM implementation.
+
+First, **enable custom `#[cfg]`s in your build.rs**:
+
+```rust
+// build.rs
+use libpam_sys_helpers::pam_impl;
+
+fn main() {
+    pam_impl::enable_pam_impl_cfg();
+    
+    // everything else you do at build time
+}
+```
+
+This will then allow you to use the `pam_impl` configuration variable at compile time:
+
+```rust
+#[cfg(pam_impl = "LinuxPam")]
+fn handle_pam() {
+    // do things in a Linux-PAM specific way
+}
+
+#[cfg(not(pam_impl = "LinuxPam"))]
+fn handle_pam() {
+    // do things in another way
+}
+```
+
+## Configuration
+
+By default, this crate automatically detects your libpam version.
+Known implementations are listed in the `PamImpl` enum.
+
+You can override this **at build time** by setting the `LIBPAMSYS_IMPL` environment variable to one of those names.
+For example, `LIBPAMSYS_IMPL=OpenPam cargo build` will build this library for OpenPAM.
+
+## MSRV
+
+This library supports **Rust 1.75**, as the version currently (July 2025) available in Debian Trixie and Ubuntu 24.04 LTS.
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-helpers/build.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -0,0 +1,96 @@
+#![allow(unexpected_cfgs)]
+
+use std::ffi::{c_void, CString};
+use std::ptr::NonNull;
+use std::{env, fs};
+
+include!("src/pam_impl.rs");
+
+fn main() {
+    let pam_impl = match option_env!("LIBPAMSYS_IMPL") {
+        // The default option: Guess what PAM impl we're using based on OS.
+        None | Some("") => LibPam::detect(),
+        Some(other) => match PamImpl::try_from(other) {
+            Ok(i) => i,
+            Err(_) => {
+                panic!(
+                    "unknown PAM implementation {other:?}. valid LIBPAMSYS_IMPLs are {:?}, or unset to detect", PamImpl::items()
+                )
+            }
+        },
+    };
+    let impl_str = format!("{pam_impl:?}");
+    println!("{}", generate_cfg(&impl_str));
+    // We set this environment variable to substitute into docstrings.
+    println!("cargo:rustc-env=LIBPAMSYS_IMPL={impl_str}");
+    fs::write(
+        format!("{}/pam_impl_const.rs", env::var("OUT_DIR").unwrap()),
+        generate_consts(&impl_str),
+    )
+    .unwrap();
+}
+
+fn generate_consts(impl_str: &str) -> String {
+    format!(
+        "\
+impl PamImpl {{
+/// The implementation of libpam this was built for (`{impl_str}`).
+pub const CURRENT: Self = Self::{impl_str};
+}}
+
+/// String name of [`PamImpl::CURRENT`], for substituting into docs.
+#[macro_export]
+macro_rules! pam_impl_name {{ () => ({impl_str:?}) }}
+        "
+    )
+}
+
+struct LibPam(NonNull<c_void>);
+
+impl LibPam {
+    fn detect() -> PamImpl {
+        if let Some(lib) = Self::open() {
+            if lib.has("pam_syslog") {
+                return PamImpl::LinuxPam;
+            } else if lib.has("_openpam_log") {
+                return PamImpl::OpenPam;
+            } else if lib.has("__pam_get_authtok") {
+                return PamImpl::Sun;
+            }
+        }
+        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(target_os = "illumos", target_os = "solaris")) {
+            PamImpl::Sun
+        } else {
+            PamImpl::XSso
+        }
+    }
+
+    fn open() -> Option<Self> {
+        NonNull::new(unsafe { libc::dlopen(b"libpam.so\0".as_ptr().cast(), libc::RTLD_LAZY) })
+            .map(Self)
+    }
+
+    fn has(&self, name: &str) -> bool {
+        let name = CString::new(name).unwrap();
+        let symbol = unsafe { libc::dlsym(self.0.as_ptr(), name.as_ptr()) };
+        !symbol.is_null()
+    }
+}
+
+impl Drop for LibPam {
+    fn drop(&mut self) {
+        unsafe {
+            libc::dlclose(self.0.as_ptr());
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-helpers/src/constants.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -0,0 +1,298 @@
+//! All of `libpam`'s constants.
+//!
+//! These constants are tested on a per-platform basis by `libpam-sys-test`'s
+//! `test_constants.rs`.
+
+#![allow(non_camel_case_types)]
+#![allow(unused_imports)]
+use num_enum::{IntoPrimitive, TryFromPrimitive};
+
+/// 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: i32 = $value;)+
+        );
+    };
+    (@meta $m:tt $($i:item)+) => { define!(@expand $($m $i)+); };
+    (@expand $({ $(#[$m:meta])* } $i:item)+) => {$($(#[$m])* $i)+};
+}
+
+/// Macro to make defining C-style enums way easier.
+macro_rules! c_enum {
+    ($(#[$attr:meta])* $($name:ident $(= $value:expr)?,)*) => {
+        c_enum!(
+            (0)
+            $(#[$attr])*
+            $($name $(= $value)?,)*
+        );
+    };
+    (($n:expr) $(#[$attr:meta])* $name:ident, $($rest:ident $(= $rv:expr)?,)*) => {
+        $(#[$attr])* pub const $name: i32 = $n;
+        c_enum!(($n + 1) $(#[$attr])* $($rest $(= $rv)?,)*);
+    };
+    (($n:expr) $(#[$attr:meta])* $name:ident = $value:expr, $($rest:ident $(= $rv:expr)?,)*) => {
+        $(#[$attr])* pub const $name: i32 = $value;
+        c_enum!(($value + 1) $(#[$attr])* $($rest $(= $rv)?,)*);
+    };
+    (($n:expr) $(#[$attr:meta])*) => {};
+}
+
+// There are a few truly universal constants.
+// They are defined here directly.
+/// The successful return code.
+pub const PAM_SUCCESS: i32 = 0;
+
+c_enum!(
+    /// An item type.
+    PAM_SERVICE = 1,
+    PAM_USER,
+    PAM_TTY,
+    PAM_RHOST,
+    PAM_CONV,
+    PAM_AUTHTOK,
+    PAM_OLDAUTHTOK,
+    PAM_RUSER,
+    PAM_USER_PROMPT,
+);
+
+c_enum!(
+    /// A message style.
+    PAM_PROMPT_ECHO_OFF = 1,
+    PAM_PROMPT_ECHO_ON,
+    PAM_ERROR_MSG,
+    PAM_TEXT_INFO,
+);
+
+define!(
+    /// Maximum size of PAM conversation elements (suggested).
+    PAM_MAX_NUM_MSG = 32;
+    PAM_MAX_MSG_SIZE = 512;
+    PAM_MAX_RESP_SIZE = 512;
+);
+
+#[cfg(pam_impl = "LinuxPam")]
+pub use linux_pam::*;
+#[cfg(pam_impl = "LinuxPam")]
+mod linux_pam {
+    use super::{IntoPrimitive, TryFromPrimitive};
+    c_enum!(
+        /// An error return code.
+        PAM_OPEN_ERR = 1,
+        PAM_SYMBOL_ERR,
+        PAM_SERVICE_ERR,
+        PAM_SYSTEM_ERR,
+        PAM_BUF_ERR,
+        PAM_PERM_DENIED,
+        PAM_AUTH_ERR,
+        PAM_CRED_INSUFFICIENT,
+        PAM_AUTHINFO_UNAVAIL,
+        PAM_USER_UNKNOWN,
+        PAM_MAXTRIES,
+        PAM_NEW_AUTHTOK_REQD,
+        PAM_ACCT_EXPIRED,
+        PAM_SESSION_ERR,
+        PAM_CRED_UNAVAIL,
+        PAM_CRED_EXPIRED,
+        PAM_CRED_ERR,
+        PAM_NO_MODULE_DATA,
+        PAM_CONV_ERR,
+        PAM_AUTHTOK_ERR,
+        PAM_AUTHTOK_RECOVERY_ERR,
+        PAM_AUTHTOK_LOCK_BUSY,
+        PAM_AUTHTOK_DISABLE_AGING,
+        PAM_TRY_AGAIN,
+        PAM_IGNORE,
+        PAM_ABORT,
+        PAM_AUTHTOK_EXPIRED,
+        PAM_MODULE_UNKNOWN,
+        PAM_BAD_ITEM,
+        PAM_CONV_AGAIN,
+        PAM_INCOMPLETE,
+        _PAM_RETURN_VALUES,
+    );
+
+    define!(
+        /// A flag value.
+        PAM_SILENT = 0x8000;
+        PAM_DISALLOW_NULL_AUTHTOK = 0x0001;
+        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;
+    );
+
+    c_enum!(
+        /// An item type (Linux-only).
+        PAM_FAIL_DELAY = 10,
+        PAM_XDISPLAY,
+        PAM_XAUTHDATA,
+        PAM_AUTHTOK_TYPE,
+    );
+
+    /// To suppress messages in the item cleanup function.
+    pub const PAM_DATA_SILENT: i32 = 0x40000000;
+
+    // Message styles
+    define!(
+        /// A message style.
+        PAM_RADIO_TYPE = 5;
+        PAM_BINARY_PROMPT = 7;
+    );
+
+    pub const PAM_MODUTIL_NGROUPS: i32 = 64;
+
+    #[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+    #[repr(i32)]
+    pub enum pam_modutil_redirect_fd {
+        PAM_MODUTIL_IGNORE_FD,
+        PAM_MODUTIL_PIPE_FD,
+        PAM_MODUTIL_NULL_FD,
+    }
+
+    pub use pam_modutil_redirect_fd::*;
+}
+
+#[cfg(any(pam_impl = "OpenPam", pam_impl = "Sun", pam_impl = "XSso"))]
+pub use xsso_shared::*;
+#[cfg(any(pam_impl = "OpenPam", pam_impl = "Sun", pam_impl = "XSso"))]
+mod xsso_shared {
+    c_enum!(
+        /// An error return code.
+        PAM_OPEN_ERR = 1,
+        PAM_SYMBOL_ERR,
+        PAM_SERVICE_ERR,
+        PAM_SYSTEM_ERR,
+        PAM_BUF_ERR,
+        PAM_CONV_ERR,
+        PAM_PERM_DENIED,
+        PAM_MAXTRIES,
+        PAM_AUTH_ERR,
+        PAM_NEW_AUTHTOK_REQD,
+        PAM_CRED_INSUFFICIENT,
+        PAM_AUTHINFO_UNAVAIL,
+        PAM_USER_UNKNOWN,
+        PAM_CRED_UNAVAIL,
+        PAM_CRED_EXPIRED,
+        PAM_CRED_ERR,
+        PAM_ACCT_EXPIRED,
+        PAM_AUTHTOK_EXPIRED,
+        PAM_SESSION_ERR,
+        PAM_AUTHTOK_ERR,
+        PAM_AUTHTOK_RECOVERY_ERR,
+        PAM_AUTHTOK_LOCK_BUSY,
+        PAM_AUTHTOK_DISABLE_AGING,
+        PAM_NO_MODULE_DATA,
+        PAM_IGNORE,
+        PAM_ABORT,
+        PAM_TRY_AGAIN,
+    );
+    // While `PAM_MODULE_UNKNOWN` and `PAM_DOMAIN_UNKNOWN` are in X/SSO,
+    // Sun doesn't use them so we're omitting them here.
+
+    /// A general flag for PAM operations.
+    pub const PAM_SILENT: i32 = 0x80000000u32 as i32;
+
+    /// A flag for `pam_authenticate`.
+    pub const PAM_DISALLOW_NULL_AUTHTOK: i32 = 0b1;
+
+    define!(
+        /// A flag for `pam_setcred`.
+        PAM_ESTABLISH_CRED = 0b0001;
+        PAM_DELETE_CRED = 0b0010;
+        PAM_REINITIALIZE_CRED = 0b0100;
+        PAM_REFRESH_CRED = 0b1000;
+    );
+
+    define!(
+        /// A flag for `pam_sm_chauthtok`.
+        PAM_PRELIM_CHECK = 0b0001;
+        PAM_UPDATE_AUTHTOK = 0b0010;
+        PAM_CHANGE_EXPIRED_AUTHTOK = 0b0100;
+    );
+}
+
+#[cfg(pam_impl = "OpenPam")]
+pub use openpam::*;
+#[cfg(pam_impl = "OpenPam")]
+mod openpam {
+    c_enum!(
+        /// An error return code.
+        PAM_MODULE_UNKNOWN = 28,
+        PAM_DOMAIN_UNKNOWN,
+        PAM_BAD_HANDLE,
+        PAM_BAD_ITEM,
+        PAM_BAD_FEATURE,
+        PAM_BAD_CONSTANT,
+    );
+    /// The total number of PAM error codes (including success).
+    pub const PAM_NUM_ERRORS: i32 = 34;
+
+    c_enum!(
+        /// An item type.
+        PAM_REPOSITORY = 10,
+        PAM_AUTHTOK_PROMPT,
+        PAM_OLDAUTHTOK_PROMPT,
+        PAM_HOST,
+    );
+    /// The total number of PAM items.
+    pub const PAM_NUM_ITEMS: i32 = 14;
+
+    c_enum!(
+        /// An optional OpenPAM feature.
+        OPENPAM_RESTRICT_SERVICE_NAME,
+        OPENPAM_VERIFY_POLICY_FILE,
+        OPENPAM_RESTRICT_MODULE_NAME,
+        OPENPAM_VERIFY_MODULE_FILE,
+        OPENPAM_FALLBACK_TO_OTHER,
+    );
+    /// The number of optional OpenPAM features.
+    pub const OPENPAM_NUM_FEATURES: i32 = 5;
+
+    c_enum!(
+        /// Log level.
+        PAM_LOG_LIBDEBUG = -1,
+        PAM_LOG_DEBUG,
+        PAM_LOG_VERBOSE,
+        PAM_LOG_NOTICE,
+        PAM_LOG_ERROR,
+    );
+
+    c_enum!(
+        /// PAM primitives.
+        PAM_SM_AUTHENTICATE,
+        PAM_SM_SETCRED,
+        PAM_SM_ACCT_MGMT,
+        PAM_SM_OPEN_SESSION,
+        PAM_SM_CLOSE_SESSION,
+        PAM_SM_CHAUTHTOK,
+    );
+    /// The number of PAM primitives.
+    pub const PAM_NUM_PRIMITIVES: i32 = 6;
+}
+
+/// Constants exclusive to Illumos.
+#[cfg(pam_impl = "Sun")]
+pub use sun::*;
+#[cfg(pam_impl = "Sun")]
+mod sun {
+    /// The total number of PAM error codes.
+    pub const PAM_TOTAL_ERRNUM: i32 = 28;
+
+    c_enum!(
+        /// An item type.
+        PAM_REPOSITORY = 10,
+        PAM_RESOURCE,
+        PAM_AUSER,
+    );
+
+    /// A flag for `pam_chauthtok`.
+    pub const PAM_NO_AUTHTOK_CHECK: i32 = 0b1000;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-helpers/src/lib.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -0,0 +1,51 @@
+#![doc = include_str!("../README.md")]
+//!
+//! ## Current implementation
+//!
+//! This documentation was built based on the
+#![doc = concat!("**", env!("LIBPAMSYS_IMPL"), "** implementation.")]
+
+pub mod constants;
+pub mod memory;
+
+/// Information about the PAM implementation you're using right now.
+///
+/// This module contains constants and values that can be used at build-script,
+/// compile, and run time to determine what PAM implementation you're using.
+///
+/// ## Always available
+///
+/// [`PamImpl::CURRENT`] will tell you what version of PAM you're using.
+/// It can be imported in any Rust code, from build scripts to runtime.
+///
+/// ## Compile time
+///
+/// Use [`enable_pam_impl_cfg`] in your `build.rs` to generate custom `#[cfg]`s
+/// for conditional compilation based on PAM implementation.
+///
+/// This will set the current `pam_impl` as well as registering all known
+/// PAM implementations with `rustc-check-cfg` to get cfg-checking.
+///
+/// The names that appear in the `cfg` variables are the same as the values
+/// in the [`PamImpl`] enum.
+///
+/// ```
+/// #[cfg(pam_impl = "OpenPam")]
+/// fn openpam_specific_func(handle: *const libpam_sys::pam_handle) {
+///     let environ = libpam_sys::pam_getenvlist(handle);
+///     // ...
+///     libpam_sys::openpam_free_envlist()
+/// }
+///
+/// // This will give you a warning since "UnknownImpl" is not in the cfg.
+/// #[cfg(not(pam_impl = "UnknownImpl"))]
+/// fn do_something() {
+///     // ...
+/// }
+/// ```
+///
+/// The [`pam_impl_name!`] macro will expand to this same value, currently
+#[doc = concat!("`", env!("LIBPAMSYS_IMPL"), "`.")]
+pub mod pam_impl;
+#[doc(inline)]
+pub use pam_impl::*;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-helpers/src/memory.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -0,0 +1,510 @@
+//! Helpers to deal with annoying memory management in the PAM API.
+//!
+//!
+
+use std::error::Error;
+use std::marker::{PhantomData, PhantomPinned};
+use std::mem::ManuallyDrop;
+use std::ptr::NonNull;
+use std::{any, fmt, mem, slice};
+
+/// A pointer-to-pointer-to-message container for PAM's conversation callback.
+///
+/// The PAM conversation callback requires a pointer to a pointer of
+/// `pam_message`s. Linux-PAM handles this differently than all other
+/// PAM implementations (including the X/SSO PAM standard).
+///
+/// X/SSO appears to specify a pointer-to-pointer-to-array:
+///
+/// ```text
+///           points to  ┌────────────┐       ╔═ Message[] ═╗
+/// messages ┄┄┄┄┄┄┄┄┄┄> │ *messages ┄┼┄┄┄┄┄> ║ style       ║
+///                      └────────────┘       ║ data ┄┄┄┄┄┄┄╫┄┄> ...
+///                                           ╟─────────────╢
+///                                           ║ style       ║
+///                                           ║ data ┄┄┄┄┄┄┄╫┄┄> ...
+///                                           ╟─────────────╢
+///                                           ║ ...         ║
+/// ```
+///
+/// whereas Linux-PAM uses an `**argv`-style pointer-to-array-of-pointers:
+///
+/// ```text
+///           points to  ┌──────────────┐      ╔═ Message ═╗
+/// messages ┄┄┄┄┄┄┄┄┄┄> │ messages[0] ┄┼┄┄┄┄> ║ style     ║
+///                      │ messages[1] ┄┼┄┄┄╮  ║ data ┄┄┄┄┄╫┄┄> ...
+///                      │ ...          │   ┆  ╚═══════════╝
+///                                         ┆
+///                                         ┆    ╔═ Message ═╗
+///                                         ╰┄┄> ║ style     ║
+///                                              ║ data ┄┄┄┄┄╫┄┄> ...
+///                                              ╚═══════════╝
+/// ```
+///
+/// Because the `messages` remain owned by the application which calls into PAM,
+/// we can solve this with One Simple Trick: make the intermediate list point
+/// into the same array:
+///
+/// ```text
+///           points to  ┌──────────────┐      ╔═ Message[] ═╗
+/// messages ┄┄┄┄┄┄┄┄┄┄> │ messages[0] ┄┼┄┄┄┄> ║ style       ║
+///                      │ messages[1] ┄┼┄┄╮   ║ data ┄┄┄┄┄┄┄╫┄┄> ...
+///                      │ ...          │  ┆   ╟─────────────╢
+///                                        ╰┄> ║ style       ║
+///                                            ║ data ┄┄┄┄┄┄┄╫┄┄> ...
+///                                            ╟─────────────╢
+///                                            ║ ...         ║
+///
+/// ```
+#[derive(Debug)]
+pub struct PtrPtrVec<T> {
+    data: Vec<T>,
+    pointers: Vec<*const T>,
+}
+
+// Since this is a wrapper around a Vec with no dangerous functionality*,
+// this can be Send and Sync provided the original Vec is.
+//
+// * It will only become unsafe when the user dereferences a pointer or sends it
+// to an unsafe function.
+unsafe impl<T> Send for PtrPtrVec<T> where Vec<T>: Send {}
+unsafe impl<T> Sync for PtrPtrVec<T> where Vec<T>: Sync {}
+
+impl<T> PtrPtrVec<T> {
+    /// Takes ownership of the given Vec and creates a vec of pointers to it.
+    pub fn new(data: Vec<T>) -> Self {
+        let pointers: Vec<_> = data.iter().map(|r| r as *const T).collect();
+        Self { data, pointers }
+    }
+
+    /// Gives you back your Vec.
+    pub fn into_inner(self) -> Vec<T> {
+        self.data
+    }
+
+    /// Gets a pointer-to-pointer suitable for passing into the Conversation.
+    pub fn as_ptr<Dest>(&self) -> *const *const Dest {
+        Self::assert_size::<Dest>();
+        self.pointers.as_ptr().cast::<*const Dest>()
+    }
+
+    /// Iterates over a Linux-PAM–style pointer-to-array-of-pointers.
+    ///
+    /// # Safety
+    ///
+    /// `ptr_ptr` must be a valid pointer to an array of pointers,
+    /// there must be at least `count` valid pointers in the array,
+    /// and each pointer in that array must point to a valid `T`.
+    #[deprecated = "use [`Self::iter_over`] instead, unless you really need this specific version"]
+    #[allow(dead_code)]
+    pub unsafe fn iter_over_linux<'a, Src>(
+        ptr_ptr: *const *const Src,
+        count: usize,
+    ) -> impl Iterator<Item = &'a T>
+    where
+        T: 'a,
+    {
+        Self::assert_size::<Src>();
+        slice::from_raw_parts(ptr_ptr.cast::<&T>(), count)
+            .iter()
+            .copied()
+    }
+
+    /// Iterates over an X/SSO–style pointer-to-pointer-to-array.
+    ///
+    /// # Safety
+    ///
+    /// You must pass a valid pointer to a valid pointer to an array,
+    /// there must be at least `count` elements in the array,
+    /// and each value in that array must be a valid `T`.
+    #[deprecated = "use [`Self::iter_over`] instead, unless you really need this specific version"]
+    #[allow(dead_code)]
+    pub unsafe fn iter_over_xsso<'a, Src>(
+        ptr_ptr: *const *const Src,
+        count: usize,
+    ) -> impl Iterator<Item = &'a T>
+    where
+        T: 'a,
+    {
+        Self::assert_size::<Src>();
+        slice::from_raw_parts(*ptr_ptr.cast(), count).iter()
+    }
+
+    /// Iterates over a PAM message list appropriate to your system's impl.
+    ///
+    /// This selects the correct pointer/array structure to use for a message
+    /// that was given to you by your system.
+    ///
+    /// # Safety
+    ///
+    /// `ptr_ptr` must point to a valid message list, there must be at least
+    /// `count` messages in the list, and all messages must be a valid `Src`.
+    #[allow(deprecated)]
+    pub unsafe fn iter_over<'a, Src>(
+        ptr_ptr: *const *const Src,
+        count: usize,
+    ) -> impl Iterator<Item = &'a T>
+    where
+        T: 'a,
+    {
+        #[cfg(pam_impl = "LinuxPam")]
+        return Self::iter_over_linux(ptr_ptr, count);
+        #[cfg(not(pam_impl = "LinuxPam"))]
+        return Self::iter_over_xsso(ptr_ptr, count);
+    }
+
+    fn assert_size<That>() {
+        debug_assert_eq!(
+            mem::size_of::<T>(),
+            mem::size_of::<That>(),
+            "type {t} is not the size of {that}",
+            t = any::type_name::<T>(),
+            that = any::type_name::<That>(),
+        );
+    }
+}
+
+/// Error returned when attempting to allocate a buffer that is too big.
+///
+/// This is specifically used in [`OwnedBinaryPayload`] when you try to allocate
+/// a message larger than 2<sup>32</sup> bytes.
+#[derive(Debug, PartialEq)]
+pub struct TooBigError {
+    pub size: usize,
+    pub max: usize,
+}
+
+impl Error for TooBigError {}
+
+impl fmt::Display for TooBigError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "can't allocate a message of {size} bytes (max {max})",
+            size = self.size,
+            max = self.max
+        )
+    }
+}
+
+/// A trait wrapping memory management.
+///
+/// This is intended to allow you to bring your own allocator for
+/// [`OwnedBinaryPayload`]s.
+///
+/// For an implementation example, see the implementation of this trait
+/// for [`Vec`].
+pub trait Buffer<T: Default> {
+    /// Allocates a buffer of `len` elements, filled with the default.
+    fn allocate(len: usize) -> Self;
+
+    fn as_ptr(&self) -> *const T;
+
+    /// Returns a slice view of `size` elements of the given memory.
+    ///
+    /// # Safety
+    ///
+    /// The caller must not request more elements than are allocated.
+    unsafe fn as_mut_slice(&mut self, len: usize) -> &mut [T];
+
+    /// Consumes this ownership and returns a pointer to the start of the arena.
+    fn into_ptr(self) -> NonNull<T>;
+
+    /// "Adopts" the memory at the given pointer, taking it under management.
+    ///
+    /// Running the operation:
+    ///
+    /// ```
+    /// # use libpam_sys_helpers::memory::Buffer;
+    /// # fn test<T: Default, OwnerType: Buffer<T>>(bytes: usize) {
+    /// let owner = OwnerType::allocate(bytes);
+    /// let ptr = owner.into_ptr();
+    /// let owner = unsafe { OwnerType::from_ptr(ptr, bytes) };
+    /// # }
+    /// ```
+    ///
+    /// must be a no-op.
+    ///
+    /// # Safety
+    ///
+    /// The pointer must be valid, and the caller must provide the exact size
+    /// of the given arena.
+    unsafe fn from_ptr(ptr: NonNull<T>, bytes: usize) -> Self;
+}
+
+impl<T: Default> Buffer<T> for Vec<T> {
+    fn allocate(bytes: usize) -> Self {
+        (0..bytes).map(|_| Default::default()).collect()
+    }
+
+    fn as_ptr(&self) -> *const T {
+        Vec::as_ptr(self)
+    }
+
+    unsafe fn as_mut_slice(&mut self, bytes: usize) -> &mut [T] {
+        debug_assert!(bytes <= self.len());
+        Vec::as_mut(self)
+    }
+
+    fn into_ptr(self) -> NonNull<T> {
+        let mut me = ManuallyDrop::new(self);
+        // SAFETY: a Vec is guaranteed to have a nonzero pointer.
+        unsafe { NonNull::new_unchecked(me.as_mut_ptr()) }
+    }
+
+    unsafe fn from_ptr(ptr: NonNull<T>, bytes: usize) -> Self {
+        Vec::from_raw_parts(ptr.as_ptr(), bytes, bytes)
+    }
+}
+
+/// The structure of the "binary message" payload for the `PAM_BINARY_PROMPT`
+/// extension from Linux-PAM.
+pub struct BinaryPayload {
+    /// The total byte size of the message, including this header,
+    /// as a u32 in network byte order (big endian).
+    pub total_bytes_u32be: [u8; 4],
+    /// A tag used to provide some kind of hint as to what the data is.
+    /// Its meaning is undefined.
+    pub data_type: u8,
+    /// Where the data itself would start, used as a marker to make this
+    /// not [`Unpin`] (since it is effectively an intrusive data structure
+    /// pointing to immediately after itself).
+    pub _marker: PhantomData<PhantomPinned>,
+}
+
+impl BinaryPayload {
+    /// The most data it's possible to put into a [`BinaryPayload`].
+    pub const MAX_SIZE: usize = (u32::MAX - 5) as usize;
+
+    /// Fills in the provided buffer with the given data.
+    ///
+    /// This uses [`copy_from_slice`](slice::copy_from_slice) internally,
+    /// so `buf` must be exactly 5 bytes longer than `data`, or this function
+    /// will panic.
+    pub fn fill(buf: &mut [u8], data_type: u8, data: &[u8]) {
+        let ptr: *mut Self = buf.as_mut_ptr().cast();
+        // SAFETY: We're given a slice, which always has a nonzero pointer.
+        let me = unsafe { ptr.as_mut().unwrap_unchecked() };
+        me.total_bytes_u32be = u32::to_be_bytes(buf.len() as u32);
+        me.data_type = data_type;
+        buf[5..].copy_from_slice(data)
+    }
+
+    /// The total storage needed for the message, including header.
+    pub fn total_bytes(&self) -> usize {
+        u32::from_be_bytes(self.total_bytes_u32be) as usize
+    }
+
+    /// Gets the total byte buffer of the BinaryMessage stored at the pointer.
+    ///
+    /// The returned data slice is borrowed from where the pointer points to.
+    ///
+    /// # Safety
+    ///
+    /// - The pointer must point to a valid `BinaryPayload`.
+    /// - The borrowed data must not outlive the pointer's validity.
+    pub unsafe fn buffer_of<'a>(ptr: *const Self) -> &'a [u8] {
+        let header: &Self = ptr.as_ref().unwrap_unchecked();
+        slice::from_raw_parts(ptr.cast(), header.total_bytes().max(5))
+    }
+
+    /// Gets the contents of the BinaryMessage stored at the given pointer.
+    ///
+    /// The returned data slice is borrowed from where the pointer points to.
+    /// This is a cheap operation and doesn't do *any* copying.
+    ///
+    /// We don't take a `&self` reference here because accessing beyond
+    /// the range of the `Self` data (i.e., beyond the 5 bytes of `self`)
+    /// is undefined behavior. Instead, you have to pass a raw pointer
+    /// directly to the data.
+    ///
+    /// # Safety
+    ///
+    /// - The pointer must point to a valid `BinaryPayload`.
+    /// - The borrowed data must not outlive the pointer's validity.
+    pub unsafe fn contents<'a>(ptr: *const Self) -> (u8, &'a [u8]) {
+        let header: &Self = ptr.as_ref().unwrap_unchecked();
+        (header.data_type, &Self::buffer_of(ptr)[5..])
+    }
+}
+
+/// A binary message owned by some storage.
+///
+/// This is an owned, memory-managed version of [`BinaryPayload`].
+/// The `O` type manages the memory where the payload lives.
+/// [`Vec<u8>`] is one such manager and can be used when ownership
+/// of the data does not need to transit through PAM.
+#[derive(Debug)]
+pub struct OwnedBinaryPayload<Owner: Buffer<u8>>(Owner);
+
+impl<O: Buffer<u8>> OwnedBinaryPayload<O> {
+    /// Allocates a new OwnedBinaryPayload.
+    ///
+    /// This will return a [`TooBigError`] if you try to allocate too much
+    /// (more than [`BinaryPayload::MAX_SIZE`]).
+    pub fn new(data_type: u8, data: &[u8]) -> Result<Self, TooBigError> {
+        let total_len: u32 = (data.len() + 5).try_into().map_err(|_| TooBigError {
+            size: data.len(),
+            max: BinaryPayload::MAX_SIZE,
+        })?;
+        let total_len = total_len as usize;
+        let mut buf = O::allocate(total_len);
+        // SAFETY: We just allocated this exact size.
+        BinaryPayload::fill(unsafe { buf.as_mut_slice(total_len) }, data_type, data);
+        Ok(Self(buf))
+    }
+
+    /// The contents of the buffer.
+    pub fn contents(&self) -> (u8, &[u8]) {
+        unsafe { BinaryPayload::contents(self.as_ptr()) }
+    }
+
+    /// The total bytes needed to store this, including the header.
+    pub fn total_bytes(&self) -> usize {
+        unsafe { BinaryPayload::buffer_of(self.0.as_ptr().cast()).len() }
+    }
+
+    /// Unwraps this into the raw storage backing it.
+    pub fn into_inner(self) -> O {
+        self.0
+    }
+
+    /// Gets a const pointer to the start of the message's buffer.
+    pub fn as_ptr(&self) -> *const BinaryPayload {
+        self.0.as_ptr().cast()
+    }
+
+    /// Consumes ownership of this message and converts it to a raw pointer
+    /// to the start of the message.
+    ///
+    /// To clean this up, you should eventually pass it into [`Self::from_ptr`]
+    /// with the same `O` ownership type.
+    pub fn into_ptr(self) -> NonNull<BinaryPayload> {
+        self.0.into_ptr().cast()
+    }
+
+    /// Takes ownership of the given pointer.
+    ///
+    /// # Safety
+    ///
+    /// You must provide a valid pointer, allocated by (or equivalent to one
+    /// allocated by) [`Self::new`]. For instance, passing a pointer allocated
+    /// by `malloc` to `OwnedBinaryPayload::<Vec<u8>>::from_ptr` is not allowed.
+    pub unsafe fn from_ptr(ptr: NonNull<BinaryPayload>) -> Self {
+        Self(O::from_ptr(ptr.cast(), ptr.as_ref().total_bytes()))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::ptr;
+
+    type VecPayload = OwnedBinaryPayload<Vec<u8>>;
+
+    #[test]
+    fn test_binary_payload() {
+        let simple_message = &[0u8, 0, 0, 16, 0xff, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+        let empty = &[0u8; 5];
+
+        assert_eq!((0xff, &[0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10][..]), unsafe {
+            BinaryPayload::contents(simple_message.as_ptr().cast())
+        });
+        assert_eq!((0x00, &[][..]), unsafe {
+            BinaryPayload::contents(empty.as_ptr().cast())
+        });
+    }
+
+    #[test]
+    fn test_owned_binary_payload() {
+        let (typ, data) = (
+            112,
+            &[0, 1, 1, 8, 9, 9, 9, 8, 8, 1, 9, 9, 9, 1, 1, 9, 7, 2, 5, 3][..],
+        );
+        let payload = VecPayload::new(typ, data).unwrap();
+        assert_eq!((typ, data), payload.contents());
+        let ptr = payload.into_ptr();
+        let payload = unsafe { VecPayload::from_ptr(ptr) };
+        assert_eq!((typ, data), payload.contents());
+    }
+
+    #[test]
+    #[ignore]
+    fn test_owned_too_big() {
+        let data = vec![0xFFu8; 0x1_0000_0001];
+        assert_eq!(
+            TooBigError {
+                max: 0xffff_fffa,
+                size: 0x1_0000_0001
+            },
+            VecPayload::new(5, &data).unwrap_err()
+        )
+    }
+
+    #[cfg(debug_assertions)]
+    #[test]
+    #[should_panic]
+    fn test_new_wrong_size() {
+        let bad_vec = vec![0; 19];
+        let msg = PtrPtrVec::new(bad_vec);
+        let _ = msg.as_ptr::<u64>();
+    }
+
+    #[allow(deprecated)]
+    #[cfg(debug_assertions)]
+    #[test]
+    #[should_panic]
+    fn test_iter_xsso_wrong_size() {
+        unsafe {
+            let _ = PtrPtrVec::<u8>::iter_over_xsso::<f64>(ptr::null(), 1);
+        }
+    }
+
+    #[allow(deprecated)]
+    #[cfg(debug_assertions)]
+    #[test]
+    #[should_panic]
+    fn test_iter_linux_wrong_size() {
+        unsafe {
+            let _ = PtrPtrVec::<u128>::iter_over_linux::<()>(ptr::null(), 1);
+        }
+    }
+
+    #[allow(deprecated)]
+    #[test]
+    fn test_right_size() {
+        let good_vec = vec![(1u64, 2u64), (3, 4), (5, 6)];
+        let ptr = good_vec.as_ptr();
+        let msg = PtrPtrVec::new(good_vec);
+        let msg_ref: *const *const (i64, i64) = msg.as_ptr();
+        assert_eq!(unsafe { *msg_ref }, ptr.cast());
+
+        let linux_result: Vec<(i64, i64)> = unsafe { PtrPtrVec::iter_over_linux(msg_ref, 3) }
+            .cloned()
+            .collect();
+        let xsso_result: Vec<(i64, i64)> = unsafe { PtrPtrVec::iter_over_xsso(msg_ref, 3) }
+            .cloned()
+            .collect();
+        assert_eq!(vec![(1, 2), (3, 4), (5, 6)], linux_result);
+        assert_eq!(vec![(1, 2), (3, 4), (5, 6)], xsso_result);
+        drop(msg)
+    }
+
+    #[allow(deprecated)]
+    #[test]
+    fn test_iter_ptr_ptr() {
+        let strs = vec![Box::new("a"), Box::new("b"), Box::new("c"), Box::new("D")];
+        let ptr: *const *const &str = strs.as_ptr().cast();
+        let got: Vec<&str> = unsafe { PtrPtrVec::iter_over_linux(ptr, 4) }
+            .cloned()
+            .collect();
+        assert_eq!(vec!["a", "b", "c", "D"], got);
+
+        let nums = [-1i8, 2, 3];
+        let ptr = nums.as_ptr();
+        let got: Vec<u8> = unsafe { PtrPtrVec::iter_over_xsso(&ptr, 3) }
+            .cloned()
+            .collect();
+        assert_eq!(vec![255, 2, 3], got);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-helpers/src/pam_impl.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -0,0 +1,113 @@
+// This file is include!d directly by `../build.rs`, so its doc comment
+// is found in lib.rs.
+
+/// An enum that knows its own values.
+macro_rules! self_aware_enum {
+    (
+        $(#[$enumeta:meta])*
+        $viz:vis enum $name:ident {
+            $(
+                $(#[$itemeta:meta])*
+                $item:ident,
+            )*
+        }
+    ) => {
+        $(#[$enumeta])*
+        $viz enum $name {
+            $(
+                $(#[$itemeta])*
+                $item,
+            )*
+        }
+
+        // The implementations in this block are private for now
+        // to avoid putting a contract into the public API.
+        #[allow(dead_code)]
+        impl $name {
+            /// Iterator over the items in the enum. For internal use.
+            fn items() -> Vec<Self> {
+                vec![$(Self::$item),*]
+            }
+
+            /// Attempts to parse the enum from the string. For internal use.
+            fn try_from(value: &str) -> Result<Self, String> {
+                match value {
+                    $(stringify!($item) => Ok(Self::$item),)*
+                    _ => Err(value.into()),
+                }
+            }
+        }
+    };
+}
+
+self_aware_enum! {
+    /// The PAM implementations supported by `libpam-sys`.
+    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
+    #[cfg_attr(pam_impl, non_exhaustive)]
+    pub enum PamImpl {
+        /// [Linux-PAM] is provided by most Linux distributions.
+        ///
+        /// [Linux-PAM]: https://github.com/linux-pam/linux-pam
+        LinuxPam,
+        /// [OpenPAM] is used by most BSDs, including Mac OS X.
+        ///
+        /// [OpenPAM]: https://git.des.dev/OpenPAM/OpenPAM
+        OpenPam,
+        /// Illumos and Solaris use a derivative of [Sun's implementation][sun].
+        ///
+        /// [sun]: https://code.illumos.org/plugins/gitiles/illumos-gate/+/refs/heads/master/usr/src/lib/libpam
+        Sun,
+        /// Only the functionality and constants in [the PAM spec].
+        ///
+        /// [the PAM spec]: https://pubs.opengroup.org/onlinepubs/8329799/toc.htm
+        XSso,
+    }
+}
+
+// This generated file contains:
+// - pam_impl_name!
+// - PamImpl::CURRENT
+#[cfg(pam_impl)]
+include!(concat!(env!("OUT_DIR"), "/pam_impl_const.rs"));
+
+#[allow(clippy::needless_doctest_main)]
+/// Generates `cargo` directives for build scripts to enable `cfg(pam_impl)`.
+///
+/// Print this in your `build.rs` script to be able to use the custom `pam_impl`
+/// configuration directive.
+///
+/// ```
+/// // build.rs
+/// use libpam_sys_helpers::pam_impl;
+/// fn main() {
+///     pam_impl::enable_pam_impl_cfg();
+///
+///     // Whatever other stuff you do in your build.rs.
+/// }
+/// ```
+#[cfg(pam_impl)]
+pub fn enable_pam_impl_cfg() {
+    println!("{}", pam_impl_cfg_string())
+}
+
+/// Generates the `cargo:` directives to print in build scripts.
+#[cfg(pam_impl)]
+pub fn pam_impl_cfg_string() -> String {
+    generate_cfg(pam_impl_name!())
+}
+
+fn generate_cfg(name: &str) -> String {
+    let impls: Vec<_> = PamImpl::items()
+        .into_iter()
+        .map(|i| format!(r#""{i:?}""#))
+        .collect();
+    format!(
+        "\
+cargo:rustc-check-cfg=cfg(pam_impl)
+cargo:rustc-check-cfg=cfg(pam_impl, values({impls}))
+cargo:rustc-cfg=pam_impl
+cargo:rustc-cfg=pam_impl={name:?}
+        ",
+        impls = impls.join(",")
+    )
+}
--- a/libpam-sys/libpam-sys-test/Cargo.toml	Thu Jul 03 11:14:49 2025 -0400
+++ b/libpam-sys/libpam-sys-test/Cargo.toml	Thu Jul 03 14:28:04 2025 -0400
@@ -14,6 +14,7 @@
 bindgen = "0.72.0"
 ctest = "0.4.11"
 libpam-sys = { path = ".." }
+libpam-sys-helpers = { path = "../libpam-sys-helpers" }
 proc-macro2 = "1.0.95"
 quote = "1.0.40"
 syn = { version = "2.0.104", features = ["full"] }
--- a/libpam-sys/libpam-sys-test/build.rs	Thu Jul 03 11:14:49 2025 -0400
+++ b/libpam-sys/libpam-sys-test/build.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -1,5 +1,6 @@
 use bindgen::MacroTypeVariation;
-use libpam_sys::pam_impl::PamImpl;
+use libpam_sys_helpers::pam_impl::PamImpl;
+use libpam_sys_helpers::{pam_impl, pam_impl_name};
 use proc_macro2::{Group, Ident, TokenStream, TokenTree};
 use quote::{format_ident, ToTokens};
 use std::path::Path;
@@ -11,6 +12,7 @@
 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![
@@ -67,8 +69,7 @@
     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![
-        "\
+    let mut tests = vec!["\
             #[allow(dead_code, non_camel_case_types, non_upper_case_globals)]
             mod generated {
                 include!(\"bindgen.rs\");
@@ -78,7 +79,7 @@
         "
         .into(),
         format!(
-            "assert_eq!(libpam_sys::PamImpl::CURRENT, libpam_sys::PamImpl::{:?});",
+            "assert_eq!(libpam_sys::pam_impl::PamImpl::CURRENT, libpam_sys::pam_impl::PamImpl::{:?});",
             PamImpl::CURRENT
         ),
     ];
@@ -110,7 +111,7 @@
 
 fn generate_ctest(config: &TestConfig) {
     let mut test = ctest::TestGenerator::new();
-    test.cfg("_hack_impl", Some(&format!("{:?}", PamImpl::CURRENT)));
+    test.cfg("pam_impl", Some(pam_impl_name!()));
 
     for &header in config.headers.iter() {
         if header.starts_with('"') {
@@ -143,7 +144,7 @@
     test.define("const", Some(""));
 
     // Also replace all the `const`s with `mut`s in the ffi.rs file.
-    let file_contents = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../src/ffi.rs"));
+    let file_contents = include_str!("../src/lib.rs");
     let deconsted_file = test_file("ffi.rs");
     remove_consts(file_contents, &deconsted_file);
 
--- a/libpam-sys/libpam-sys-test/tests/runner.rs	Thu Jul 03 11:14:49 2025 -0400
+++ b/libpam-sys/libpam-sys-test/tests/runner.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -17,7 +17,6 @@
 
     #[test]
     fn test_c() {
-        eprintln!("Running a test!");
         main();
     }
 }
--- a/libpam-sys/src/constants.rs	Thu Jul 03 11:14:49 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,283 +0,0 @@
-//! All the constants.
-//!
-//! These constants are tested on a per-platform basis by `libpam-sys-test`'s
-//! `test_constants.rs`.
-
-/// 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: i32 = $value;)+
-        );
-    };
-    (@meta $m:tt $($i:item)+) => { define!(@expand $($m $i)+); };
-    (@expand $({ $(#[$m:meta])* } $i:item)+) => {$($(#[$m])* $i)+};
-}
-
-/// Macro to make defining C-style enums way easier.
-macro_rules! c_enum {
-    ($(#[$attr:meta])* $($name:ident $(= $value:expr)?,)*) => {
-        c_enum!(
-            (0)
-            $(#[$attr])*
-            $($name $(= $value)?,)*
-        );
-    };
-    (($n:expr) $(#[$attr:meta])* $name:ident, $($rest:ident $(= $rv:expr)?,)*) => {
-        $(#[$attr])* pub const $name: i32 = $n;
-        c_enum!(($n + 1) $(#[$attr])* $($rest $(= $rv)?,)*);
-    };
-    (($n:expr) $(#[$attr:meta])* $name:ident = $value:expr, $($rest:ident $(= $rv:expr)?,)*) => {
-        $(#[$attr])* pub const $name: i32 = $value;
-        c_enum!(($value + 1) $(#[$attr])* $($rest $(= $rv)?,)*);
-    };
-    (($n:expr) $(#[$attr:meta])*) => {};
-}
-
-// There are a few truly universal constants.
-// They are defined here directly.
-/// The successful return code.
-pub const PAM_SUCCESS: i32 = 0;
-
-c_enum!(
-    /// An item type.
-    PAM_SERVICE = 1,
-    PAM_USER,
-    PAM_TTY,
-    PAM_RHOST,
-    PAM_CONV,
-    PAM_AUTHTOK,
-    PAM_OLDAUTHTOK,
-    PAM_RUSER,
-    PAM_USER_PROMPT,
-);
-
-c_enum!(
-    /// A message style.
-    PAM_PROMPT_ECHO_OFF = 1,
-    PAM_PROMPT_ECHO_ON,
-    PAM_ERROR_MSG,
-    PAM_TEXT_INFO,
-);
-
-define!(
-    /// Maximum size of PAM conversation elements (suggested).
-    PAM_MAX_NUM_MSG = 32;
-    PAM_MAX_MSG_SIZE = 512;
-    PAM_MAX_RESP_SIZE = 512;
-);
-
-#[cfg(pam_impl = "LinuxPam")]
-pub use linux_pam::*;
-#[cfg(pam_impl = "LinuxPam")]
-mod linux_pam {
-    c_enum!(
-        /// An error return code.
-        PAM_OPEN_ERR = 1,
-        PAM_SYMBOL_ERR,
-        PAM_SERVICE_ERR,
-        PAM_SYSTEM_ERR,
-        PAM_BUF_ERR,
-        PAM_PERM_DENIED,
-        PAM_AUTH_ERR,
-        PAM_CRED_INSUFFICIENT,
-        PAM_AUTHINFO_UNAVAIL,
-        PAM_USER_UNKNOWN,
-        PAM_MAXTRIES,
-        PAM_NEW_AUTHTOK_REQD,
-        PAM_ACCT_EXPIRED,
-        PAM_SESSION_ERR,
-        PAM_CRED_UNAVAIL,
-        PAM_CRED_EXPIRED,
-        PAM_CRED_ERR,
-        PAM_NO_MODULE_DATA,
-        PAM_CONV_ERR,
-        PAM_AUTHTOK_ERR,
-        PAM_AUTHTOK_RECOVERY_ERR,
-        PAM_AUTHTOK_LOCK_BUSY,
-        PAM_AUTHTOK_DISABLE_AGING,
-        PAM_TRY_AGAIN,
-        PAM_IGNORE,
-        PAM_ABORT,
-        PAM_AUTHTOK_EXPIRED,
-        PAM_MODULE_UNKNOWN,
-        PAM_BAD_ITEM,
-        PAM_CONV_AGAIN,
-        PAM_INCOMPLETE,
-        _PAM_RETURN_VALUES,
-    );
-
-    define!(
-        /// A flag value.
-        PAM_SILENT = 0x8000;
-        PAM_DISALLOW_NULL_AUTHTOK = 0x0001;
-        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;
-    );
-
-    c_enum!(
-        /// An item type (Linux-only).
-        PAM_FAIL_DELAY = 10,
-        PAM_XDISPLAY,
-        PAM_XAUTHDATA,
-        PAM_AUTHTOK_TYPE,
-    );
-
-    /// To suppress messages in the item cleanup function.
-    pub const PAM_DATA_SILENT: i32 = 0x40000000;
-
-    // Message styles
-    define!(
-        /// A message style.
-        PAM_RADIO_TYPE = 5;
-        PAM_BINARY_PROMPT = 7;
-    );
-
-    pub const PAM_MODUTIL_NGROUPS: i32 = 64;
-}
-
-#[cfg(any(pam_impl = "OpenPam", pam_impl = "Sun", pam_impl = "XSso"))]
-pub use xsso_shared::*;
-#[cfg(any(pam_impl = "OpenPam", pam_impl = "Sun", pam_impl = "XSso"))]
-mod xsso_shared {
-    c_enum!(
-        /// An error return code.
-        PAM_OPEN_ERR = 1,
-        PAM_SYMBOL_ERR,
-        PAM_SERVICE_ERR,
-        PAM_SYSTEM_ERR,
-        PAM_BUF_ERR,
-        PAM_CONV_ERR,
-        PAM_PERM_DENIED,
-        PAM_MAXTRIES,
-        PAM_AUTH_ERR,
-        PAM_NEW_AUTHTOK_REQD,
-        PAM_CRED_INSUFFICIENT,
-        PAM_AUTHINFO_UNAVAIL,
-        PAM_USER_UNKNOWN,
-        PAM_CRED_UNAVAIL,
-        PAM_CRED_EXPIRED,
-        PAM_CRED_ERR,
-        PAM_ACCT_EXPIRED,
-        PAM_AUTHTOK_EXPIRED,
-        PAM_SESSION_ERR,
-        PAM_AUTHTOK_ERR,
-        PAM_AUTHTOK_RECOVERY_ERR,
-        PAM_AUTHTOK_LOCK_BUSY,
-        PAM_AUTHTOK_DISABLE_AGING,
-        PAM_NO_MODULE_DATA,
-        PAM_IGNORE,
-        PAM_ABORT,
-        PAM_TRY_AGAIN,
-    );
-    // While `PAM_MODULE_UNKNOWN` and `PAM_DOMAIN_UNKNOWN` are in X/SSO,
-    // Sun doesn't use them so we're omitting them here.
-
-    /// A general flag for PAM operations.
-    pub const PAM_SILENT: i32 = 0x80000000u32 as i32;
-
-    /// A flag for `pam_authenticate`.
-    pub const PAM_DISALLOW_NULL_AUTHTOK: i32 = 0b1;
-
-    define!(
-        /// A flag for `pam_setcred`.
-        PAM_ESTABLISH_CRED = 0b0001;
-        PAM_DELETE_CRED = 0b0010;
-        PAM_REINITIALIZE_CRED = 0b0100;
-        PAM_REFRESH_CRED = 0b1000;
-    );
-
-    define!(
-        /// A flag for `pam_sm_chauthtok`.
-        PAM_PRELIM_CHECK = 0b0001;
-        PAM_UPDATE_AUTHTOK = 0b0010;
-        PAM_CHANGE_EXPIRED_AUTHTOK = 0b0100;
-    );
-}
-
-#[cfg(pam_impl = "OpenPam")]
-pub use openpam::*;
-#[cfg(pam_impl = "OpenPam")]
-mod openpam {
-    c_enum!(
-        /// An error return code.
-        PAM_MODULE_UNKNOWN = 28,
-        PAM_DOMAIN_UNKNOWN,
-        PAM_BAD_HANDLE,
-        PAM_BAD_ITEM,
-        PAM_BAD_FEATURE,
-        PAM_BAD_CONSTANT,
-    );
-    /// The total number of PAM error codes (including success).
-    pub const PAM_NUM_ERRORS: i32 = 34;
-
-    c_enum!(
-        /// An item type.
-        PAM_REPOSITORY = 10,
-        PAM_AUTHTOK_PROMPT,
-        PAM_OLDAUTHTOK_PROMPT,
-        PAM_HOST,
-    );
-    /// The total number of PAM items.
-    pub const PAM_NUM_ITEMS: i32 = 14;
-
-    c_enum!(
-        /// An optional OpenPAM feature.
-        OPENPAM_RESTRICT_SERVICE_NAME,
-        OPENPAM_VERIFY_POLICY_FILE,
-        OPENPAM_RESTRICT_MODULE_NAME,
-        OPENPAM_VERIFY_MODULE_FILE,
-        OPENPAM_FALLBACK_TO_OTHER,
-    );
-    /// The number of optional OpenPAM features.
-    pub const OPENPAM_NUM_FEATURES: i32 = 5;
-
-    c_enum!(
-        /// Log level.
-        PAM_LOG_LIBDEBUG = -1,
-        PAM_LOG_DEBUG,
-        PAM_LOG_VERBOSE,
-        PAM_LOG_NOTICE,
-        PAM_LOG_ERROR,
-    );
-
-    c_enum!(
-        /// PAM primitives.
-        PAM_SM_AUTHENTICATE,
-        PAM_SM_SETCRED,
-        PAM_SM_ACCT_MGMT,
-        PAM_SM_OPEN_SESSION,
-        PAM_SM_CLOSE_SESSION,
-        PAM_SM_CHAUTHTOK,
-    );
-    /// The number of PAM primitives.
-    pub const PAM_NUM_PRIMITIVES: i32 = 6;
-}
-
-/// Constants exclusive to Illumos.
-#[cfg(pam_impl = "Sun")]
-pub use sun::*;
-#[cfg(pam_impl = "Sun")]
-mod sun {
-    /// The total number of PAM error codes.
-    pub const PAM_TOTAL_ERRNUM: i32 = 28;
-
-    c_enum!(
-        /// An item type.
-        PAM_REPOSITORY = 10,
-        PAM_RESOURCE,
-        PAM_AUSER,
-    );
-
-    /// A flag for `pam_chauthtok`.
-    pub const PAM_NO_AUTHTOK_CHECK: i32 = 0b1000;
-}
--- a/libpam-sys/src/ffi.rs	Thu Jul 03 11:14:49 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,514 +0,0 @@
-//! The actual PAM FFI bindings.
-//!
-//! They live in this specific file rather than lib.rs because otherwise
-//! ctest gets very upset about some of the macros we use.
-#![allow(non_camel_case_types)]
-#![allow(unused_imports)]
-
-use num_enum::{IntoPrimitive, TryFromPrimitive};
-use std::ffi::{c_char, c_int, c_uint, c_void};
-use std::fmt;
-use std::marker::{PhantomData, PhantomPinned};
-
-/// A marker struct to make whatever it's in `!Sync`, `!Send`, and `!Unpin`.
-#[derive(Default, PartialOrd, PartialEq, Ord, Eq)]
-#[repr(C)]
-struct ExtremelyUnsafe {
-    _value: (),
-    _marker: PhantomData<(PhantomPinned, *mut c_void)>,
-}
-
-impl fmt::Debug for ExtremelyUnsafe {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "ExtremelyUnsafe({self:p})")
-    }
-}
-
-/// An opaque structure that PAM uses to communicate.
-///
-/// This is only ever returned in pointer form and cannot be constructed.
-#[repr(C)]
-pub struct pam_handle(ExtremelyUnsafe);
-
-impl fmt::Debug for pam_handle {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "pam_handle({self:p}")
-    }
-}
-
-/// An opaque structure that is passed through PAM in a conversation.
-#[repr(C)]
-pub struct AppData(ExtremelyUnsafe);
-
-impl fmt::Debug for AppData {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "AppData({self:p}")
-    }
-}
-
-/// Just an alias for the type of [`pam_conv::conv`].
-///
-/// For important details about the format of `messages`,
-/// see the [`helpers`](crate::helpers) module.
-///
-/// ```no_run
-/// # use libpam_sys::{ConversationCallback, pam_conv};
-/// fn convo() -> ConversationCallback {
-///     // ...
-/// #    unimplemented!()
-/// }
-/// let conv = pam_conv{conv: convo(), appdata_ptr: std::ptr::null_mut()};
-/// ```
-pub type ConversationCallback = unsafe extern "C" fn(
-    num_msg: c_int,
-    msg: *const *const pam_message,
-    resp: *mut *mut pam_response,
-    appdata: *mut AppData,
-) -> c_int;
-
-/// Alias for the callback to [`pam_set_data`](pam_set_data).
-///
-/// ```no_run
-/// # use std::ffi::CString;
-/// use libpam_sys::{CleanupCallback, pam_set_data};
-/// # use libpam_sys::pam_handle;
-/// # let handle: *mut pam_handle = std::ptr::null_mut();
-/// # let mut my_data = 100;
-/// # let data_ptr = &mut my_data as *mut i32;
-/// fn cleanup() -> CleanupCallback {
-///     // ...
-/// #    unimplemented!()
-/// }
-/// let name = CString::new("name").unwrap();
-/// unsafe {
-///     pam_set_data(handle, name.as_ptr().cast_mut(), data_ptr.cast(), cleanup());
-/// }
-/// ```
-pub type CleanupCallback =
-    unsafe extern "C" fn(pamh: *mut pam_handle, data: *mut c_void, pam_end_status: c_int);
-
-/// Used by PAM to communicate between the module and the application.
-#[repr(C)]
-pub struct pam_conv {
-    pub conv: unsafe extern "C" fn(
-        num_msg: c_int,
-        msg: *const *const pam_message,
-        resp: *mut *mut pam_response,
-        appdata: *mut AppData,
-    ) -> c_int,
-    pub appdata_ptr: *mut AppData,
-}
-
-/// A message sent into a PAM conversation.
-#[repr(C)]
-pub struct pam_message {
-    pub msg_style: c_int,
-    pub msg: *const c_char,
-}
-
-/// A response returned from a PAM conversation.
-#[repr(C)]
-pub struct pam_response {
-    pub resp: *mut c_char,
-    /// Completely unused.
-    pub resp_retcode: c_int,
-}
-
-/// Definition of the PAM_XAUTHDATA item. Compatible with `xcb_auth_info_t`.
-#[cfg(pam_impl = "LinuxPam")]
-#[repr(C)]
-pub struct pam_xauth_data {
-    namelen: c_int,
-    name: *mut c_char,
-    datalen: c_int,
-    data: *mut c_char,
-}
-
-#[cfg(pam_impl = "LinuxPam")]
-#[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
-#[repr(i32)]
-pub enum pam_modutil_redirect_fd {
-    PAM_MODUTIL_IGNORE_FD,
-    PAM_MODUTIL_PIPE_FD,
-    PAM_MODUTIL_NULL_FD,
-}
-
-#[cfg(pam_impl = "LinuxPam")]
-pub use pam_modutil_redirect_fd::*;
-
-#[cfg(pam_impl = "LinuxPam")]
-#[derive(Debug)]
-#[repr(C)]
-pub struct pam_modutil_privs {
-    grplist: *mut libc::gid_t,
-    number_of_groups: c_int,
-    allocated: c_int,
-    old_gid: libc::gid_t,
-    old_uid: libc::uid_t,
-    is_dropped: c_int,
-}
-
-#[cfg(pam_impl = "OpenPam")]
-pub type pam_func_t = unsafe extern "C" fn(
-    handle: *mut pam_handle,
-    flags: c_int,
-    argc: c_int,
-    argv: *const *const c_char,
-) -> c_int;
-
-#[cfg(pam_impl = "OpenPam")]
-#[derive(Debug)]
-#[repr(C)]
-pub struct pam_module {
-    path: *mut c_char,
-    func: [pam_func_t; 6],
-    dlh: *mut c_void,
-}
-
-#[cfg(pam_impl = "OpenPam")]
-#[derive(Debug)]
-#[repr(C)]
-pub struct pam_repository {
-    typ: *mut c_char,
-    scope: *mut c_void,
-    scope_len: usize,
-}
-
-// These are the functions specified in X/SSO. Everybody exports them.
-#[link(name = "pam")]
-extern "C" {
-    /// Account validation.
-    pub fn pam_acct_mgmt(pamh: *mut pam_handle, flags: c_int) -> c_int;
-
-    /// Authenticate a user.
-    pub fn pam_authenticate(pamh: *mut pam_handle, flags: c_int) -> c_int;
-
-    // Nobody implements pam_authenticate_secondary.
-
-    /// Manage authentication tokens.
-    pub fn pam_chauthtok(pamh: *mut pam_handle, flags: c_int) -> c_int;
-
-    /// Close an opened user session.
-    pub fn pam_close_session(pamh: *mut pam_handle, flags: c_int) -> c_int;
-
-    /// Ends the PAM transaction.
-    pub fn pam_end(pamh: *mut pam_handle, flags: c_int) -> c_int;
-
-    /// Gets module-specific data. PAM still owns the data.
-    pub fn pam_get_data(
-        pamh: *mut pam_handle,
-        module_data_name: *const c_char,
-        data: *mut *const c_void,
-    ) -> c_int;
-
-    /// Gets an environment variable.  You own the return value.
-    pub fn pam_getenv(pamh: *const pam_handle, name: *const c_char) -> *mut c_char;
-
-    /// Gets all the environment variables.  You own everything it points to.
-    pub fn pam_getenvlist(pamh: *const pam_handle) -> *mut *mut c_char;
-
-    /// Get information about the transaction.
-    ///
-    /// The item is owned by PAM.
-    pub fn pam_get_item(
-        pamh: *const pam_handle,
-        item_type: c_int,
-        item: *mut *const c_void,
-    ) -> c_int;
-
-    // Nobody implements pam_get_mapped_authtok.
-    // Nobody implements pam_get_mapped_username.
-
-    /// Get the username. PAM owns it.
-    pub fn pam_get_user(
-        pamh: *mut pam_handle,
-        user: *mut *const c_char,
-        prompt: *const c_char,
-    ) -> c_int;
-
-    /// Opens a user session.
-    pub fn pam_open_session(pamh: *mut pam_handle, flags: c_int) -> c_int;
-
-    /// Sets the value of an environment variable. `namevalue` is copied.
-    pub fn pam_putenv(pamh: *mut pam_handle, namevalue: *const c_char) -> c_int;
-
-    /// Update or delete user credentials.
-    pub fn pam_setcred(pamh: *mut pam_handle, flags: c_int) -> c_int;
-
-    /// Set module-specific data. PAM will call `cleanup` when completed.
-    pub fn pam_set_data(
-        pamh: *mut pam_handle,
-        module_data_name: *const c_char,
-        data: *mut c_void,
-        cleanup: unsafe extern "C" fn(
-            pamh: *mut pam_handle,
-            data: *mut c_void,
-            pam_end_status: c_int,
-        ),
-    ) -> c_int;
-
-    /// Set information about the transaction.  The `item` is copied.
-    pub fn pam_set_item(pamh: *mut pam_handle, item_type: c_int, item: *const c_void) -> c_int;
-
-    // Nobody implements pam_set_mapped_authtok.
-    // Nobody implements pam_set_mapped_username.
-
-    // The pam_sm_whatever functions are prototypes for the functions that
-    // a PAM module should implement, not symbols provided by PAM.
-
-    // Nobody implements pam_authenticate_secondary.
-
-    /// Starts a PAM transaction.  The `conv` may or may not be copied.
-    pub fn pam_start(
-        service: *const c_char,
-        user: *const c_char,
-        pam_conv: *mut pam_conv,
-        pamh: *mut *mut pam_handle,
-    ) -> c_int;
-
-    /// Gets a statically-allocated error string.
-    ///
-    /// All implementations of PAM known to this library (Linux-PAM, OpenPAM,
-    /// and Sun) ignore `pamh` and will accept a null pointer.
-    pub fn pam_strerror(pamh: *const pam_handle, error_number: c_int) -> *mut c_char;
-}
-
-// We use `pam_impl` because ctest loses its mind
-// when it comes across the `cfg_pam_impl` macro.
-// This is a custom cfg variable set in our build.rs. Don't do this; just use
-// cfg_pam_impl.
-#[cfg(any(pam_impl = "LinuxPam", pam_impl = "OpenPam"))]
-extern "C" {
-    /// Gets `PAM_AUTHTOK`, or asks the user if that is unset.
-    pub fn pam_get_authtok(
-        pamh: *mut pam_handle,
-        item: c_int,
-        authtok: *mut *const c_char,
-        prompt: *const c_char,
-    ) -> c_int;
-
-    pub fn pam_prompt(
-        pamh: *const pam_handle,
-        style: c_int,
-        response: *mut *mut c_char,
-        fmt: *const c_char,
-        ...
-    ) -> c_int;
-
-}
-
-#[cfg(pam_impl = "LinuxPam")]
-extern "C" {
-    pub fn pam_fail_delay(pamh: *mut pam_handle, musec_delay: c_uint) -> c_int;
-
-    /// Start a PAM transaction based on configuration in the given directory.
-    pub fn pam_start_confdir(
-        service_name: *const c_char,
-        user: *const c_char,
-        pam_conversation: *mut pam_conv,
-        confdir: *const c_char,
-        pamh: *mut *mut pam_handle,
-    ) -> c_int;
-
-    // We don't export the v-variants of the formatting functions.
-
-    pub fn pam_syslog(pamh: *const pam_handle, priority: c_int, fmt: *const c_char, ...);
-
-    pub fn pam_get_authtok_noverify(
-        pamh: *const pam_handle,
-        authtok: *mut *const c_char,
-        prompt: *const c_char,
-    ) -> c_int;
-
-    pub fn pam_get_authtok_verify(
-        pamh: *const pam_handle,
-        authtok: *mut *const c_char,
-        prompt: *const c_char,
-    ) -> c_int;
-
-    // pam_modutil also lives in libpam for Linux.
-
-    pub fn pam_modutil_check_user_in_passwd(
-        pamh: *mut pam_handle,
-        user_name: *const c_char,
-        file_name: *const c_char,
-    ) -> c_int;
-
-    pub fn pam_modutil_getpwnam(pamh: *mut pam_handle, user: *const c_char) -> *mut libc::passwd;
-
-    pub fn pam_modutil_getpwuid(pamh: *mut pam_handle, uid: libc::uid_t) -> *mut libc::passwd;
-
-    pub fn pam_modutil_getgrnam(pamh: *mut pam_handle, group: *const c_char) -> *mut libc::group;
-
-    pub fn pam_modutil_getgrgid(pamh: *mut pam_handle, gid: libc::gid_t) -> *mut libc::group;
-
-    pub fn pam_modutil_getspnam(pamh: *mut pam_handle, user: *const c_char) -> *mut libc::spwd;
-
-    pub fn pam_modutil_user_in_group_nam_nam(
-        pamh: *mut pam_handle,
-        user: *const c_char,
-        group: *const c_char,
-    ) -> c_int;
-    pub fn pam_modutil_user_in_group_nam_gid(
-        pamh: *mut pam_handle,
-        user: *const c_char,
-        group: libc::gid_t,
-    ) -> c_int;
-
-    pub fn pam_modutil_user_in_group_uid_nam(
-        pamh: *mut pam_handle,
-        user: libc::uid_t,
-        group: *const c_char,
-    ) -> c_int;
-
-    pub fn pam_modutil_user_in_group_uid_gid(
-        pamh: *mut pam_handle,
-        user: libc::uid_t,
-        group: libc::gid_t,
-    ) -> c_int;
-
-    pub fn pam_modutil_getlogin(pamh: *mut pam_handle) -> *const c_char;
-
-    pub fn pam_modutil_read(fd: c_int, buffer: *mut c_char, count: c_int) -> c_int;
-
-    pub fn pam_modutil_write(fd: c_int, buffer: *const c_char, count: c_int) -> c_int;
-
-    pub fn pam_modutil_audit_write(
-        pamh: *mut pam_handle,
-        typ: c_int,
-        message: *const c_char,
-        retval: c_int,
-    ) -> c_int;
-
-    pub fn pam_modutil_drop_priv(
-        pamh: *mut pam_handle,
-        p: *mut pam_modutil_privs,
-        pw: *const libc::passwd,
-    ) -> c_int;
-
-    pub fn pam_modutil_regain_priv(pamh: *mut pam_handle, p: *mut pam_modutil_privs) -> c_int;
-
-    pub fn pam_modutil_sanitize_helper_fds(
-        pamh: *mut pam_handle,
-        redirect_stdin: pam_modutil_redirect_fd,
-        redirect_stdout: pam_modutil_redirect_fd,
-        redirect_stderr: pam_modutil_redirect_fd,
-    ) -> c_int;
-
-    pub fn pam_modutil_search_key(
-        pamh: *mut pam_handle,
-        file_name: *const c_char,
-        key: *const c_char,
-    ) -> *mut c_char;
-}
-
-#[cfg(pam_impl = "OpenPam")]
-extern "C" {
-    pub fn openpam_borrow_cred(pamh: *mut pam_handle, passwd: *const libc::passwd) -> c_int;
-
-    pub fn openpam_subst(
-        pamh: *const pam_handle,
-        buf: *mut c_char,
-        _bufsize: *mut usize,
-        _template: *const c_char,
-    ) -> c_int;
-
-    pub fn openpam_free_data(pamh: *mut pam_handle, data: *mut c_void, status: c_int);
-
-    pub fn openpam_free_envlist(_envlist: *mut *mut c_char);
-
-    pub fn openpam_get_option(_pamh: *mut pam_handle, _option: *const c_char) -> *const c_char;
-
-    pub fn openpam_restore_cred(pamh: *mut pam_handle) -> c_int;
-
-    pub fn openpam_set_option(
-        _pamh: *mut pam_handle,
-        _option: *const c_char,
-        _value: *const c_char,
-    ) -> c_int;
-
-    pub fn pam_error(pamh: *const pam_handle, _fmt: *const c_char, ...) -> c_int;
-
-    pub fn pam_info(_pamh: *const pam_handle, _fmt: *const c_char, ...) -> c_int;
-
-    pub fn openpam_readline(
-        _f: *mut libc::FILE,
-        _lineno: *mut c_int,
-        _lenp: *mut usize,
-    ) -> *mut c_char;
-
-    pub fn openpam_readlinev(
-        _f: *mut libc::FILE,
-        _lineno: *mut c_int,
-        _lenp: *mut c_int,
-    ) -> *mut *mut c_char;
-
-    pub fn openpam_readword(
-        _f: *mut libc::FILE,
-        _lineno: *mut c_int,
-        _lenp: *mut usize,
-    ) -> *mut c_char;
-
-    pub fn openpam_straddch(
-        _str: *mut *mut c_char,
-        _sizep: *mut usize,
-        _lenp: *mut usize,
-        ch: c_int,
-    ) -> c_int;
-
-    pub fn openpam_set_feature(_feature: c_int, _onoff: c_int) -> c_int;
-
-    pub fn openpam_get_feature(_feature: c_int, _onoff: *mut c_int) -> c_int;
-
-    pub fn _openpam_log(_level: c_int, _func: *const c_char, _fmt: *const c_char, ...);
-
-    /// A premade conversation function that talks to the TTY.
-    ///
-    /// ```no_run
-    /// # use std::ffi::CString;
-    /// # use std::ptr;
-    /// use libpam_sys::*;
-    /// # let service = CString::new("whatever").unwrap();
-    /// # let user = CString::new("whatever").unwrap();
-    /// let mut handle: *mut pam_handle = ptr::null_mut();
-    /// let mut conv = pam_conv{
-    ///     conv: openpam_ttyconv,
-    ///     appdata_ptr: ptr::null_mut(),
-    /// };
-    /// let result = unsafe { pam_start(
-    ///     service.as_ptr(), user.as_ptr(), &mut conv, &mut handle
-    /// ) };
-    /// ```
-    pub fn openpam_ttyconv(
-        n: c_int,
-        _msg: *const *const pam_message,
-        _resp: *mut *mut pam_response,
-        _data: *mut AppData,
-    ) -> c_int;
-
-    pub static mut openpam_ttyconv_timeout: c_int;
-
-    /// A null conversation function.
-    ///
-    /// ```no_run
-    /// # use std::ffi::CString;
-    /// # use std::ptr;
-    /// use libpam_sys::*;
-    /// # let service = CString::new("whatever").unwrap();
-    /// # let user = CString::new("whatever").unwrap();
-    /// let mut handle: *mut pam_handle = ptr::null_mut();
-    /// let mut conv = pam_conv{
-    ///     conv: openpam_nullconv,
-    ///     appdata_ptr: ptr::null_mut(),
-    /// };
-    /// let result = unsafe { pam_start(
-    ///     service.as_ptr(), user.as_ptr(), &mut conv, &mut handle
-    /// ) };
-    /// ```
-    pub fn openpam_nullconv(
-        n: c_int,
-        _msg: *const *const pam_message,
-        _resp: *mut *mut pam_response,
-        _data: *mut AppData,
-    ) -> c_int;
-}
--- a/libpam-sys/src/helpers.rs	Thu Jul 03 11:14:49 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,512 +0,0 @@
-//! This module contains a few non-required helpers to deal with some of the
-//! more annoying memory management in the PAM API.
-
-use std::error::Error;
-use std::marker::{PhantomData, PhantomPinned};
-use std::mem::ManuallyDrop;
-use std::ptr::NonNull;
-use std::{any, fmt, mem, slice};
-
-/// A pointer-to-pointer-to-message container for the [conversation callback].
-///
-/// The PAM conversation callback requires a pointer to a pointer of [message]s.
-/// Linux-PAM handles this differently than all other PAM implementations
-/// (including the X/SSO PAM standard).
-///
-/// X/SSO appears to specify a pointer-to-pointer-to-array:
-///
-/// ```text
-///           points to  ┌────────────┐       ╔═ Message[] ═╗
-/// messages ┄┄┄┄┄┄┄┄┄┄> │ *messages ┄┼┄┄┄┄┄> ║ style       ║
-///                      └────────────┘       ║ data ┄┄┄┄┄┄┄╫┄┄> ...
-///                                           ╟─────────────╢
-///                                           ║ style       ║
-///                                           ║ data ┄┄┄┄┄┄┄╫┄┄> ...
-///                                           ╟─────────────╢
-///                                           ║ ...         ║
-/// ```
-///
-/// whereas Linux-PAM uses an `**argv`-style pointer-to-array-of-pointers:
-///
-/// ```text
-///           points to  ┌──────────────┐      ╔═ Message ═╗
-/// messages ┄┄┄┄┄┄┄┄┄┄> │ messages[0] ┄┼┄┄┄┄> ║ style     ║
-///                      │ messages[1] ┄┼┄┄┄╮  ║ data ┄┄┄┄┄╫┄┄> ...
-///                      │ ...          │   ┆  ╚═══════════╝
-///                                         ┆
-///                                         ┆    ╔═ Message ═╗
-///                                         ╰┄┄> ║ style     ║
-///                                              ║ data ┄┄┄┄┄╫┄┄> ...
-///                                              ╚═══════════╝
-/// ```
-///
-/// Because the `messages` remain owned by the application which calls into PAM,
-/// we can solve this with One Simple Trick: make the intermediate list point
-/// into the same array:
-///
-/// ```text
-///           points to  ┌──────────────┐      ╔═ Message[] ═╗
-/// messages ┄┄┄┄┄┄┄┄┄┄> │ messages[0] ┄┼┄┄┄┄> ║ style       ║
-///                      │ messages[1] ┄┼┄┄╮   ║ data ┄┄┄┄┄┄┄╫┄┄> ...
-///                      │ ...          │  ┆   ╟─────────────╢
-///                                        ╰┄> ║ style       ║
-///                                            ║ data ┄┄┄┄┄┄┄╫┄┄> ...
-///                                            ╟─────────────╢
-///                                            ║ ...         ║
-///
-/// ```
-///
-/// [conversation callback]: crate::ConversationCallback
-/// [message]: crate::pam_message
-#[derive(Debug)]
-pub struct PtrPtrVec<T> {
-    data: Vec<T>,
-    pointers: Vec<*const T>,
-}
-
-// Since this is a wrapper around a Vec with no dangerous functionality*,
-// this can be Send and Sync provided the original Vec is.
-//
-// * It will only become unsafe when the user dereferences a pointer or sends it
-// to an unsafe function.
-unsafe impl<T> Send for PtrPtrVec<T> where Vec<T>: Send {}
-unsafe impl<T> Sync for PtrPtrVec<T> where Vec<T>: Sync {}
-
-impl<T> PtrPtrVec<T> {
-    /// Takes ownership of the given Vec and creates a vec of pointers to it.
-    pub fn new(data: Vec<T>) -> Self {
-        let pointers: Vec<_> = data.iter().map(|r| r as *const T).collect();
-        Self { data, pointers }
-    }
-
-    /// Gives you back your Vec.
-    pub fn into_inner(self) -> Vec<T> {
-        self.data
-    }
-
-    /// Gets a pointer-to-pointer suitable for passing into the Conversation.
-    pub fn as_ptr<Dest>(&self) -> *const *const Dest {
-        Self::assert_size::<Dest>();
-        self.pointers.as_ptr().cast::<*const Dest>()
-    }
-
-    /// Iterates over a Linux-PAM–style pointer-to-array-of-pointers.
-    ///
-    /// # Safety
-    ///
-    /// `ptr_ptr` must be a valid pointer to an array of pointers,
-    /// there must be at least `count` valid pointers in the array,
-    /// and each pointer in that array must point to a valid `T`.
-    #[deprecated = "use [`Self::iter_over`] instead, unless you really need this specific version"]
-    #[allow(dead_code)]
-    pub unsafe fn iter_over_linux<'a, Src>(
-        ptr_ptr: *const *const Src,
-        count: usize,
-    ) -> impl Iterator<Item = &'a T>
-    where
-        T: 'a,
-    {
-        Self::assert_size::<Src>();
-        slice::from_raw_parts(ptr_ptr.cast::<&T>(), count)
-            .iter()
-            .copied()
-    }
-
-    /// Iterates over an X/SSO–style pointer-to-pointer-to-array.
-    ///
-    /// # Safety
-    ///
-    /// You must pass a valid pointer to a valid pointer to an array,
-    /// there must be at least `count` elements in the array,
-    /// and each value in that array must be a valid `T`.
-    #[deprecated = "use [`Self::iter_over`] instead, unless you really need this specific version"]
-    #[allow(dead_code)]
-    pub unsafe fn iter_over_xsso<'a, Src>(
-        ptr_ptr: *const *const Src,
-        count: usize,
-    ) -> impl Iterator<Item = &'a T>
-    where
-        T: 'a,
-    {
-        Self::assert_size::<Src>();
-        slice::from_raw_parts(*ptr_ptr.cast(), count).iter()
-    }
-
-    /// Iterates over a PAM message list appropriate to your system's impl.
-    ///
-    /// This selects the correct pointer/array structure to use for a message
-    /// that was given to you by your system.
-    ///
-    /// # Safety
-    ///
-    /// `ptr_ptr` must point to a valid message list, there must be at least
-    /// `count` messages in the list, and all messages must be a valid `Src`.
-    #[allow(deprecated)]
-    pub unsafe fn iter_over<'a, Src>(
-        ptr_ptr: *const *const Src,
-        count: usize,
-    ) -> impl Iterator<Item = &'a T>
-    where
-        T: 'a,
-    {
-        #[cfg(pam_impl = "LinuxPam")]
-        return Self::iter_over_linux(ptr_ptr, count);
-        #[cfg(not(pam_impl = "LinuxPam"))]
-        return Self::iter_over_xsso(ptr_ptr, count);
-    }
-
-    fn assert_size<That>() {
-        debug_assert_eq!(
-            mem::size_of::<T>(),
-            mem::size_of::<That>(),
-            "type {t} is not the size of {that}",
-            t = any::type_name::<T>(),
-            that = any::type_name::<That>(),
-        );
-    }
-}
-
-/// Error returned when attempting to allocate a buffer that is too big.
-///
-/// This is specifically used in [`OwnedBinaryPayload`] when you try to allocate
-/// a message larger than 2<sup>32</sup> bytes.
-#[derive(Debug, PartialEq)]
-pub struct TooBigError {
-    pub size: usize,
-    pub max: usize,
-}
-
-impl Error for TooBigError {}
-
-impl fmt::Display for TooBigError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "can't allocate a message of {size} bytes (max {max})",
-            size = self.size,
-            max = self.max
-        )
-    }
-}
-
-/// A trait wrapping memory management.
-///
-/// This is intended to allow you to bring your own allocator for
-/// [`OwnedBinaryPayload`]s.
-///
-/// For an implementation example, see the implementation of this trait
-/// for [`Vec`].
-pub trait Buffer<T: Default> {
-    /// Allocates a buffer of `len` elements, filled with the default.
-    fn allocate(len: usize) -> Self;
-
-    fn as_ptr(&self) -> *const T;
-
-    /// Returns a slice view of `size` elements of the given memory.
-    ///
-    /// # Safety
-    ///
-    /// The caller must not request more elements than are allocated.
-    unsafe fn as_mut_slice(&mut self, len: usize) -> &mut [T];
-
-    /// Consumes this ownership and returns a pointer to the start of the arena.
-    fn into_ptr(self) -> NonNull<T>;
-
-    /// "Adopts" the memory at the given pointer, taking it under management.
-    ///
-    /// Running the operation:
-    ///
-    /// ```
-    /// # use libpam_sys::helpers::Buffer;
-    /// # fn test<T: Default, OwnerType: Buffer<T>>(bytes: usize) {
-    /// let owner = OwnerType::allocate(bytes);
-    /// let ptr = owner.into_ptr();
-    /// let owner = unsafe { OwnerType::from_ptr(ptr, bytes) };
-    /// # }
-    /// ```
-    ///
-    /// must be a no-op.
-    ///
-    /// # Safety
-    ///
-    /// The pointer must be valid, and the caller must provide the exact size
-    /// of the given arena.
-    unsafe fn from_ptr(ptr: NonNull<T>, bytes: usize) -> Self;
-}
-
-impl<T: Default> Buffer<T> for Vec<T> {
-    fn allocate(bytes: usize) -> Self {
-        (0..bytes).map(|_| Default::default()).collect()
-    }
-
-    fn as_ptr(&self) -> *const T {
-        Vec::as_ptr(self)
-    }
-
-    unsafe fn as_mut_slice(&mut self, bytes: usize) -> &mut [T] {
-        debug_assert!(bytes <= self.len());
-        Vec::as_mut(self)
-    }
-
-    fn into_ptr(self) -> NonNull<T> {
-        let mut me = ManuallyDrop::new(self);
-        // SAFETY: a Vec is guaranteed to have a nonzero pointer.
-        unsafe { NonNull::new_unchecked(me.as_mut_ptr()) }
-    }
-
-    unsafe fn from_ptr(ptr: NonNull<T>, bytes: usize) -> Self {
-        Vec::from_raw_parts(ptr.as_ptr(), bytes, bytes)
-    }
-}
-
-/// The structure of the "binary message" payload for the `PAM_BINARY_PROMPT`
-/// extension from Linux-PAM.
-pub struct BinaryPayload {
-    /// The total byte size of the message, including this header,
-    /// as a u32 in network byte order (big endian).
-    pub total_bytes_u32be: [u8; 4],
-    /// A tag used to provide some kind of hint as to what the data is.
-    /// Its meaning is undefined.
-    pub data_type: u8,
-    /// Where the data itself would start, used as a marker to make this
-    /// not [`Unpin`] (since it is effectively an intrusive data structure
-    /// pointing to immediately after itself).
-    pub _marker: PhantomData<PhantomPinned>,
-}
-
-impl BinaryPayload {
-    /// The most data it's possible to put into a [`BinaryPayload`].
-    pub const MAX_SIZE: usize = (u32::MAX - 5) as usize;
-
-    /// Fills in the provided buffer with the given data.
-    ///
-    /// This uses [`copy_from_slice`](slice::copy_from_slice) internally,
-    /// so `buf` must be exactly 5 bytes longer than `data`, or this function
-    /// will panic.
-    pub fn fill(buf: &mut [u8], data_type: u8, data: &[u8]) {
-        let ptr: *mut Self = buf.as_mut_ptr().cast();
-        // SAFETY: We're given a slice, which always has a nonzero pointer.
-        let me = unsafe { ptr.as_mut().unwrap_unchecked() };
-        me.total_bytes_u32be = u32::to_be_bytes(buf.len() as u32);
-        me.data_type = data_type;
-        buf[5..].copy_from_slice(data)
-    }
-
-    /// The total storage needed for the message, including header.
-    pub fn total_bytes(&self) -> usize {
-        u32::from_be_bytes(self.total_bytes_u32be) as usize
-    }
-
-    /// Gets the total byte buffer of the BinaryMessage stored at the pointer.
-    ///
-    /// The returned data slice is borrowed from where the pointer points to.
-    ///
-    /// # Safety
-    ///
-    /// - The pointer must point to a valid `BinaryPayload`.
-    /// - The borrowed data must not outlive the pointer's validity.
-    pub unsafe fn buffer_of<'a>(ptr: *const Self) -> &'a [u8] {
-        let header: &Self = ptr.as_ref().unwrap_unchecked();
-        slice::from_raw_parts(ptr.cast(), header.total_bytes().max(5))
-    }
-
-    /// Gets the contents of the BinaryMessage stored at the given pointer.
-    ///
-    /// The returned data slice is borrowed from where the pointer points to.
-    /// This is a cheap operation and doesn't do *any* copying.
-    ///
-    /// We don't take a `&self` reference here because accessing beyond
-    /// the range of the `Self` data (i.e., beyond the 5 bytes of `self`)
-    /// is undefined behavior. Instead, you have to pass a raw pointer
-    /// directly to the data.
-    ///
-    /// # Safety
-    ///
-    /// - The pointer must point to a valid `BinaryPayload`.
-    /// - The borrowed data must not outlive the pointer's validity.
-    pub unsafe fn contents<'a>(ptr: *const Self) -> (u8, &'a [u8]) {
-        let header: &Self = ptr.as_ref().unwrap_unchecked();
-        (header.data_type, &Self::buffer_of(ptr)[5..])
-    }
-}
-
-/// A binary message owned by some storage.
-///
-/// This is an owned, memory-managed version of [`BinaryPayload`].
-/// The `O` type manages the memory where the payload lives.
-/// [`Vec<u8>`] is one such manager and can be used when ownership
-/// of the data does not need to transit through PAM.
-#[derive(Debug)]
-pub struct OwnedBinaryPayload<Owner: Buffer<u8>>(Owner);
-
-impl<O: Buffer<u8>> OwnedBinaryPayload<O> {
-    /// Allocates a new OwnedBinaryPayload.
-    ///
-    /// This will return a [`TooBigError`] if you try to allocate too much
-    /// (more than [`BinaryPayload::MAX_SIZE`]).
-    pub fn new(data_type: u8, data: &[u8]) -> Result<Self, TooBigError> {
-        let total_len: u32 = (data.len() + 5).try_into().map_err(|_| TooBigError {
-            size: data.len(),
-            max: BinaryPayload::MAX_SIZE,
-        })?;
-        let total_len = total_len as usize;
-        let mut buf = O::allocate(total_len);
-        // SAFETY: We just allocated this exact size.
-        BinaryPayload::fill(unsafe { buf.as_mut_slice(total_len) }, data_type, data);
-        Ok(Self(buf))
-    }
-
-    /// The contents of the buffer.
-    pub fn contents(&self) -> (u8, &[u8]) {
-        unsafe { BinaryPayload::contents(self.as_ptr()) }
-    }
-
-    /// The total bytes needed to store this, including the header.
-    pub fn total_bytes(&self) -> usize {
-        unsafe { BinaryPayload::buffer_of(self.0.as_ptr().cast()).len() }
-    }
-
-    /// Unwraps this into the raw storage backing it.
-    pub fn into_inner(self) -> O {
-        self.0
-    }
-
-    /// Gets a const pointer to the start of the message's buffer.
-    pub fn as_ptr(&self) -> *const BinaryPayload {
-        self.0.as_ptr().cast()
-    }
-
-    /// Consumes ownership of this message and converts it to a raw pointer
-    /// to the start of the message.
-    ///
-    /// To clean this up, you should eventually pass it into [`Self::from_ptr`]
-    /// with the same `O` ownership type.
-    pub fn into_ptr(self) -> NonNull<BinaryPayload> {
-        self.0.into_ptr().cast()
-    }
-
-    /// Takes ownership of the given pointer.
-    ///
-    /// # Safety
-    ///
-    /// You must provide a valid pointer, allocated by (or equivalent to one
-    /// allocated by) [`Self::new`]. For instance, passing a pointer allocated
-    /// by `malloc` to `OwnedBinaryPayload::<Vec<u8>>::from_ptr` is not allowed.
-    pub unsafe fn from_ptr(ptr: NonNull<BinaryPayload>) -> Self {
-        Self(O::from_ptr(ptr.cast(), ptr.as_ref().total_bytes()))
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::ptr;
-
-    type VecPayload = OwnedBinaryPayload<Vec<u8>>;
-
-    #[test]
-    fn test_binary_payload() {
-        let simple_message = &[0u8, 0, 0, 16, 0xff, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
-        let empty = &[0u8; 5];
-
-        assert_eq!((0xff, &[0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10][..]), unsafe {
-            BinaryPayload::contents(simple_message.as_ptr().cast())
-        });
-        assert_eq!((0x00, &[][..]), unsafe {
-            BinaryPayload::contents(empty.as_ptr().cast())
-        });
-    }
-
-    #[test]
-    fn test_owned_binary_payload() {
-        let (typ, data) = (
-            112,
-            &[0, 1, 1, 8, 9, 9, 9, 8, 8, 1, 9, 9, 9, 1, 1, 9, 7, 2, 5, 3][..],
-        );
-        let payload = VecPayload::new(typ, data).unwrap();
-        assert_eq!((typ, data), payload.contents());
-        let ptr = payload.into_ptr();
-        let payload = unsafe { VecPayload::from_ptr(ptr) };
-        assert_eq!((typ, data), payload.contents());
-    }
-
-    #[test]
-    #[ignore]
-    fn test_owned_too_big() {
-        let data = vec![0xFFu8; 0x1_0000_0001];
-        assert_eq!(
-            TooBigError {
-                max: 0xffff_fffa,
-                size: 0x1_0000_0001
-            },
-            VecPayload::new(5, &data).unwrap_err()
-        )
-    }
-
-    #[cfg(debug_assertions)]
-    #[test]
-    #[should_panic]
-    fn test_new_wrong_size() {
-        let bad_vec = vec![0; 19];
-        let msg = PtrPtrVec::new(bad_vec);
-        let _ = msg.as_ptr::<u64>();
-    }
-
-    #[allow(deprecated)]
-    #[cfg(debug_assertions)]
-    #[test]
-    #[should_panic]
-    fn test_iter_xsso_wrong_size() {
-        unsafe {
-            let _ = PtrPtrVec::<u8>::iter_over_xsso::<f64>(ptr::null(), 1);
-        }
-    }
-
-    #[allow(deprecated)]
-    #[cfg(debug_assertions)]
-    #[test]
-    #[should_panic]
-    fn test_iter_linux_wrong_size() {
-        unsafe {
-            let _ = PtrPtrVec::<u128>::iter_over_linux::<()>(ptr::null(), 1);
-        }
-    }
-
-    #[allow(deprecated)]
-    #[test]
-    fn test_right_size() {
-        let good_vec = vec![(1u64, 2u64), (3, 4), (5, 6)];
-        let ptr = good_vec.as_ptr();
-        let msg = PtrPtrVec::new(good_vec);
-        let msg_ref: *const *const (i64, i64) = msg.as_ptr();
-        assert_eq!(unsafe { *msg_ref }, ptr.cast());
-
-        let linux_result: Vec<(i64, i64)> = unsafe { PtrPtrVec::iter_over_linux(msg_ref, 3) }
-            .cloned()
-            .collect();
-        let xsso_result: Vec<(i64, i64)> = unsafe { PtrPtrVec::iter_over_xsso(msg_ref, 3) }
-            .cloned()
-            .collect();
-        assert_eq!(vec![(1, 2), (3, 4), (5, 6)], linux_result);
-        assert_eq!(vec![(1, 2), (3, 4), (5, 6)], xsso_result);
-        drop(msg)
-    }
-
-    #[allow(deprecated)]
-    #[test]
-    fn test_iter_ptr_ptr() {
-        let strs = vec![Box::new("a"), Box::new("b"), Box::new("c"), Box::new("D")];
-        let ptr: *const *const &str = strs.as_ptr().cast();
-        let got: Vec<&str> = unsafe { PtrPtrVec::iter_over_linux(ptr, 4) }
-            .cloned()
-            .collect();
-        assert_eq!(vec!["a", "b", "c", "D"], got);
-
-        let nums = [-1i8, 2, 3];
-        let ptr = nums.as_ptr();
-        let got: Vec<u8> = unsafe { PtrPtrVec::iter_over_xsso(&ptr, 3) }
-            .cloned()
-            .collect();
-        assert_eq!(vec![255, 2, 3], got);
-    }
-}
--- a/libpam-sys/src/lib.rs	Thu Jul 03 11:14:49 2025 -0400
+++ b/libpam-sys/src/lib.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -1,13 +1,515 @@
-#![doc = include_str!("../README.md")]
+//! `libpam-sys` provides low-level access to `libpam`.
 //!
-//! ## PAM implementation
+//! Everything in here is directly as exported from the `libpam` library or
+//! its header files, with limited exceptions:
 //!
-#![doc = concat!("This documentation was built for the **", env!("LIBPAMSYS_IMPL"), "** implementation.")]
-
-mod constants;
-mod ffi;
-pub mod helpers;
-pub mod pam_impl;
+//! - The [`pam_impl`] submodule (and the [`pam_impl_name!`] macro) contains
+//!   tools for detecting the current PAM library.
+//! - [`AppData`] is an opaque pointer newtype.
+//! - [`ConversationCallback`] and [`CleanupCallback`] are aliases for
+//!   what are otherwise anonymous function types.
+//!
+#![doc = concat!("This documentation was built for the **", pam_impl_name!(), "** implementation.")]
+//!
+//! You can override this **at build time** by setting the `LIBPAMSYS_IMPL`
+//! environment variable to one of the values of the [`pam_impl::PamImpl`] enum.
+//!
+//! For more information about configuration, see [the README](https://crates.io/crate/libpam-sys).
+#![allow(non_camel_case_types)]
+#![allow(unused_imports)]
 
 #[doc(inline)]
-pub use crate::{constants::*, ffi::*, pam_impl::*};
+pub use libpam_sys_helpers::constants::*;
+#[doc(inline)]
+pub use libpam_sys_helpers::{pam_impl, pam_impl_name};
+use num_enum::{IntoPrimitive, TryFromPrimitive};
+use std::ffi::{c_char, c_int, c_uint, c_void};
+use std::fmt;
+use std::marker::{PhantomData, PhantomPinned};
+
+/// A marker struct to make whatever it's in `!Sync`, `!Send`, and `!Unpin`.
+#[derive(Default, PartialOrd, PartialEq, Ord, Eq)]
+#[repr(C)]
+struct ExtremelyUnsafe {
+    _value: (),
+    _marker: PhantomData<(PhantomPinned, *mut c_void)>,
+}
+
+impl fmt::Debug for ExtremelyUnsafe {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "ExtremelyUnsafe({self:p})")
+    }
+}
+
+/// An opaque structure that PAM uses to communicate.
+///
+/// This is only ever returned in pointer form and cannot be constructed.
+#[repr(C)]
+pub struct pam_handle(ExtremelyUnsafe);
+
+impl fmt::Debug for pam_handle {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "pam_handle({self:p}")
+    }
+}
+
+/// An opaque structure that is passed through PAM in a conversation.
+#[repr(C)]
+pub struct AppData(ExtremelyUnsafe);
+
+impl fmt::Debug for AppData {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "AppData({self:p}")
+    }
+}
+
+/// Just an alias for the type of [`pam_conv::conv`].
+///
+/// For important details about the format of `messages`,
+/// see [`libpam_sys_helpers::memory::PtrPtrVec`].
+///
+/// ```no_run
+/// # use libpam_sys::{ConversationCallback, pam_conv};
+/// fn convo() -> ConversationCallback {
+///     // ...
+/// #    unimplemented!()
+/// }
+/// let conv = pam_conv{conv: convo(), appdata_ptr: std::ptr::null_mut()};
+/// ```
+pub type ConversationCallback = unsafe extern "C" fn(
+    num_msg: c_int,
+    msg: *const *const pam_message,
+    resp: *mut *mut pam_response,
+    appdata: *mut AppData,
+) -> c_int;
+
+/// Alias for the callback to [`pam_set_data`].
+///
+/// ```no_run
+/// # use std::ffi::CString;
+/// use libpam_sys::{CleanupCallback, pam_set_data};
+/// # use libpam_sys::pam_handle;
+/// # let handle: *mut pam_handle = std::ptr::null_mut();
+/// # let mut my_data = 100;
+/// # let data_ptr = &mut my_data as *mut i32;
+/// fn cleanup() -> CleanupCallback {
+///     // ...
+/// #    unimplemented!()
+/// }
+/// let name = CString::new("name").unwrap();
+/// unsafe {
+///     pam_set_data(handle, name.as_ptr().cast_mut(), data_ptr.cast(), cleanup());
+/// }
+/// ```
+pub type CleanupCallback =
+    unsafe extern "C" fn(pamh: *mut pam_handle, data: *mut c_void, pam_end_status: c_int);
+
+/// Used by PAM to communicate between the module and the application.
+#[repr(C)]
+pub struct pam_conv {
+    pub conv: unsafe extern "C" fn(
+        num_msg: c_int,
+        msg: *const *const pam_message,
+        resp: *mut *mut pam_response,
+        appdata: *mut AppData,
+    ) -> c_int,
+    pub appdata_ptr: *mut AppData,
+}
+
+/// A message sent into a PAM conversation.
+#[repr(C)]
+pub struct pam_message {
+    pub msg_style: c_int,
+    pub msg: *const c_char,
+}
+
+/// A response returned from a PAM conversation.
+#[repr(C)]
+pub struct pam_response {
+    pub resp: *mut c_char,
+    /// Completely unused.
+    pub resp_retcode: c_int,
+}
+
+/// Definition of the PAM_XAUTHDATA item. Compatible with `xcb_auth_info_t`.
+#[cfg(pam_impl = "LinuxPam")]
+#[repr(C)]
+pub struct pam_xauth_data {
+    pub namelen: c_int,
+    pub name: *mut c_char,
+    pub datalen: c_int,
+    pub data: *mut c_char,
+}
+
+#[cfg(pam_impl = "LinuxPam")]
+#[derive(Debug)]
+#[repr(C)]
+pub struct pam_modutil_privs {
+    pub grplist: *mut libc::gid_t,
+    pub number_of_groups: c_int,
+    pub allocated: c_int,
+    pub old_gid: libc::gid_t,
+    pub old_uid: libc::uid_t,
+    pub is_dropped: c_int,
+}
+
+#[cfg(pam_impl = "OpenPam")]
+pub type pam_func_t = unsafe extern "C" fn(
+    handle: *mut pam_handle,
+    flags: c_int,
+    argc: c_int,
+    argv: *const *const c_char,
+) -> c_int;
+
+#[cfg(pam_impl = "OpenPam")]
+#[derive(Debug)]
+#[repr(C)]
+pub struct pam_module {
+    pub path: *mut c_char,
+    pub func: [pam_func_t; 6],
+    pub dlh: *mut c_void,
+}
+
+#[cfg(pam_impl = "OpenPam")]
+#[derive(Debug)]
+#[repr(C)]
+pub struct pam_repository {
+    pub typ: *mut c_char,
+    pub scope: *mut c_void,
+    pub scope_len: usize,
+}
+
+// These are the functions specified in X/SSO. Everybody exports them.
+#[link(name = "pam")]
+extern "C" {
+    /// Account validation.
+    pub fn pam_acct_mgmt(pamh: *mut pam_handle, flags: c_int) -> c_int;
+
+    /// Authenticate a user.
+    pub fn pam_authenticate(pamh: *mut pam_handle, flags: c_int) -> c_int;
+
+    // Nobody implements pam_authenticate_secondary.
+
+    /// Manage authentication tokens.
+    pub fn pam_chauthtok(pamh: *mut pam_handle, flags: c_int) -> c_int;
+
+    /// Close an opened user session.
+    pub fn pam_close_session(pamh: *mut pam_handle, flags: c_int) -> c_int;
+
+    /// Ends the PAM transaction.
+    pub fn pam_end(pamh: *mut pam_handle, flags: c_int) -> c_int;
+
+    /// Gets module-specific data. PAM still owns the data.
+    pub fn pam_get_data(
+        pamh: *mut pam_handle,
+        module_data_name: *const c_char,
+        data: *mut *const c_void,
+    ) -> c_int;
+
+    /// Gets an environment variable.  You own the return value.
+    pub fn pam_getenv(pamh: *const pam_handle, name: *const c_char) -> *mut c_char;
+
+    /// Gets all the environment variables.  You own everything it points to.
+    pub fn pam_getenvlist(pamh: *const pam_handle) -> *mut *mut c_char;
+
+    /// Get information about the transaction.
+    ///
+    /// The item is owned by PAM.
+    pub fn pam_get_item(
+        pamh: *const pam_handle,
+        item_type: c_int,
+        item: *mut *const c_void,
+    ) -> c_int;
+
+    // Nobody implements pam_get_mapped_authtok.
+    // Nobody implements pam_get_mapped_username.
+
+    /// Get the username. PAM owns it.
+    pub fn pam_get_user(
+        pamh: *mut pam_handle,
+        user: *mut *const c_char,
+        prompt: *const c_char,
+    ) -> c_int;
+
+    /// Opens a user session.
+    pub fn pam_open_session(pamh: *mut pam_handle, flags: c_int) -> c_int;
+
+    /// Sets the value of an environment variable. `namevalue` is copied.
+    pub fn pam_putenv(pamh: *mut pam_handle, namevalue: *const c_char) -> c_int;
+
+    /// Update or delete user credentials.
+    pub fn pam_setcred(pamh: *mut pam_handle, flags: c_int) -> c_int;
+
+    /// Set module-specific data. PAM will call `cleanup` when completed.
+    pub fn pam_set_data(
+        pamh: *mut pam_handle,
+        module_data_name: *const c_char,
+        data: *mut c_void,
+        cleanup: unsafe extern "C" fn(
+            pamh: *mut pam_handle,
+            data: *mut c_void,
+            pam_end_status: c_int,
+        ),
+    ) -> c_int;
+
+    /// Set information about the transaction.  The `item` is copied.
+    pub fn pam_set_item(pamh: *mut pam_handle, item_type: c_int, item: *const c_void) -> c_int;
+
+    // Nobody implements pam_set_mapped_authtok.
+    // Nobody implements pam_set_mapped_username.
+
+    // The pam_sm_whatever functions are prototypes for the functions that
+    // a PAM module should implement, not symbols provided by PAM.
+
+    // Nobody implements pam_authenticate_secondary.
+
+    /// Starts a PAM transaction.  The `conv` may or may not be copied.
+    pub fn pam_start(
+        service: *const c_char,
+        user: *const c_char,
+        pam_conv: *mut pam_conv,
+        pamh: *mut *mut pam_handle,
+    ) -> c_int;
+
+    /// Gets a statically-allocated error string.
+    ///
+    /// All implementations of PAM known to this library (Linux-PAM, OpenPAM,
+    /// and Sun) ignore `pamh` and will accept a null pointer.
+    pub fn pam_strerror(pamh: *const pam_handle, error_number: c_int) -> *mut c_char;
+}
+
+#[cfg(any(pam_impl = "LinuxPam", pam_impl = "OpenPam"))]
+extern "C" {
+    /// Gets `PAM_AUTHTOK`, or asks the user if that is unset.
+    pub fn pam_get_authtok(
+        pamh: *mut pam_handle,
+        item: c_int,
+        authtok: *mut *const c_char,
+        prompt: *const c_char,
+    ) -> c_int;
+
+    pub fn pam_prompt(
+        pamh: *const pam_handle,
+        style: c_int,
+        response: *mut *mut c_char,
+        fmt: *const c_char,
+        ...
+    ) -> c_int;
+
+}
+
+#[cfg(pam_impl = "LinuxPam")]
+extern "C" {
+    pub fn pam_fail_delay(pamh: *mut pam_handle, musec_delay: c_uint) -> c_int;
+
+    /// Start a PAM transaction based on configuration in the given directory.
+    pub fn pam_start_confdir(
+        service_name: *const c_char,
+        user: *const c_char,
+        pam_conversation: *mut pam_conv,
+        confdir: *const c_char,
+        pamh: *mut *mut pam_handle,
+    ) -> c_int;
+
+    // We don't export the v-variants of the formatting functions.
+
+    pub fn pam_syslog(pamh: *const pam_handle, priority: c_int, fmt: *const c_char, ...);
+
+    pub fn pam_get_authtok_noverify(
+        pamh: *const pam_handle,
+        authtok: *mut *const c_char,
+        prompt: *const c_char,
+    ) -> c_int;
+
+    pub fn pam_get_authtok_verify(
+        pamh: *const pam_handle,
+        authtok: *mut *const c_char,
+        prompt: *const c_char,
+    ) -> c_int;
+
+    // pam_modutil also lives in libpam for Linux.
+
+    pub fn pam_modutil_check_user_in_passwd(
+        pamh: *mut pam_handle,
+        user_name: *const c_char,
+        file_name: *const c_char,
+    ) -> c_int;
+
+    pub fn pam_modutil_getpwnam(pamh: *mut pam_handle, user: *const c_char) -> *mut libc::passwd;
+
+    pub fn pam_modutil_getpwuid(pamh: *mut pam_handle, uid: libc::uid_t) -> *mut libc::passwd;
+
+    pub fn pam_modutil_getgrnam(pamh: *mut pam_handle, group: *const c_char) -> *mut libc::group;
+
+    pub fn pam_modutil_getgrgid(pamh: *mut pam_handle, gid: libc::gid_t) -> *mut libc::group;
+
+    pub fn pam_modutil_getspnam(pamh: *mut pam_handle, user: *const c_char) -> *mut libc::spwd;
+
+    pub fn pam_modutil_user_in_group_nam_nam(
+        pamh: *mut pam_handle,
+        user: *const c_char,
+        group: *const c_char,
+    ) -> c_int;
+    pub fn pam_modutil_user_in_group_nam_gid(
+        pamh: *mut pam_handle,
+        user: *const c_char,
+        group: libc::gid_t,
+    ) -> c_int;
+
+    pub fn pam_modutil_user_in_group_uid_nam(
+        pamh: *mut pam_handle,
+        user: libc::uid_t,
+        group: *const c_char,
+    ) -> c_int;
+
+    pub fn pam_modutil_user_in_group_uid_gid(
+        pamh: *mut pam_handle,
+        user: libc::uid_t,
+        group: libc::gid_t,
+    ) -> c_int;
+
+    pub fn pam_modutil_getlogin(pamh: *mut pam_handle) -> *const c_char;
+
+    pub fn pam_modutil_read(fd: c_int, buffer: *mut c_char, count: c_int) -> c_int;
+
+    pub fn pam_modutil_write(fd: c_int, buffer: *const c_char, count: c_int) -> c_int;
+
+    pub fn pam_modutil_audit_write(
+        pamh: *mut pam_handle,
+        typ: c_int,
+        message: *const c_char,
+        retval: c_int,
+    ) -> c_int;
+
+    pub fn pam_modutil_drop_priv(
+        pamh: *mut pam_handle,
+        p: *mut pam_modutil_privs,
+        pw: *const libc::passwd,
+    ) -> c_int;
+
+    pub fn pam_modutil_regain_priv(pamh: *mut pam_handle, p: *mut pam_modutil_privs) -> c_int;
+
+    pub fn pam_modutil_sanitize_helper_fds(
+        pamh: *mut pam_handle,
+        redirect_stdin: pam_modutil_redirect_fd,
+        redirect_stdout: pam_modutil_redirect_fd,
+        redirect_stderr: pam_modutil_redirect_fd,
+    ) -> c_int;
+
+    pub fn pam_modutil_search_key(
+        pamh: *mut pam_handle,
+        file_name: *const c_char,
+        key: *const c_char,
+    ) -> *mut c_char;
+}
+
+#[cfg(pam_impl = "OpenPam")]
+extern "C" {
+    pub fn openpam_borrow_cred(pamh: *mut pam_handle, passwd: *const libc::passwd) -> c_int;
+
+    pub fn openpam_subst(
+        pamh: *const pam_handle,
+        buf: *mut c_char,
+        _bufsize: *mut usize,
+        _template: *const c_char,
+    ) -> c_int;
+
+    pub fn openpam_free_data(pamh: *mut pam_handle, data: *mut c_void, status: c_int);
+
+    pub fn openpam_free_envlist(_envlist: *mut *mut c_char);
+
+    pub fn openpam_get_option(_pamh: *mut pam_handle, _option: *const c_char) -> *const c_char;
+
+    pub fn openpam_restore_cred(pamh: *mut pam_handle) -> c_int;
+
+    pub fn openpam_set_option(
+        _pamh: *mut pam_handle,
+        _option: *const c_char,
+        _value: *const c_char,
+    ) -> c_int;
+
+    pub fn pam_error(pamh: *const pam_handle, _fmt: *const c_char, ...) -> c_int;
+
+    pub fn pam_info(_pamh: *const pam_handle, _fmt: *const c_char, ...) -> c_int;
+
+    pub fn openpam_readline(
+        _f: *mut libc::FILE,
+        _lineno: *mut c_int,
+        _lenp: *mut usize,
+    ) -> *mut c_char;
+
+    pub fn openpam_readlinev(
+        _f: *mut libc::FILE,
+        _lineno: *mut c_int,
+        _lenp: *mut c_int,
+    ) -> *mut *mut c_char;
+
+    pub fn openpam_readword(
+        _f: *mut libc::FILE,
+        _lineno: *mut c_int,
+        _lenp: *mut usize,
+    ) -> *mut c_char;
+
+    pub fn openpam_straddch(
+        _str: *mut *mut c_char,
+        _sizep: *mut usize,
+        _lenp: *mut usize,
+        ch: c_int,
+    ) -> c_int;
+
+    pub fn openpam_set_feature(_feature: c_int, _onoff: c_int) -> c_int;
+
+    pub fn openpam_get_feature(_feature: c_int, _onoff: *mut c_int) -> c_int;
+
+    pub fn _openpam_log(_level: c_int, _func: *const c_char, _fmt: *const c_char, ...);
+
+    /// A premade conversation function that talks to the TTY.
+    ///
+    /// ```no_run
+    /// # use std::ffi::CString;
+    /// # use std::ptr;
+    /// use libpam_sys::*;
+    /// # let service = CString::new("whatever").unwrap();
+    /// # let user = CString::new("whatever").unwrap();
+    /// let mut handle: *mut pam_handle = ptr::null_mut();
+    /// let mut conv = pam_conv{
+    ///     conv: openpam_ttyconv,
+    ///     appdata_ptr: ptr::null_mut(),
+    /// };
+    /// let result = unsafe { pam_start(
+    ///     service.as_ptr(), user.as_ptr(), &mut conv, &mut handle
+    /// ) };
+    /// ```
+    pub fn openpam_ttyconv(
+        n: c_int,
+        _msg: *const *const pam_message,
+        _resp: *mut *mut pam_response,
+        _data: *mut AppData,
+    ) -> c_int;
+
+    pub static mut openpam_ttyconv_timeout: c_int;
+
+    /// A null conversation function.
+    ///
+    /// ```no_run
+    /// # use std::ffi::CString;
+    /// # use std::ptr;
+    /// use libpam_sys::*;
+    /// # let service = CString::new("whatever").unwrap();
+    /// # let user = CString::new("whatever").unwrap();
+    /// let mut handle: *mut pam_handle = ptr::null_mut();
+    /// let mut conv = pam_conv{
+    ///     conv: openpam_nullconv,
+    ///     appdata_ptr: ptr::null_mut(),
+    /// };
+    /// let result = unsafe { pam_start(
+    ///     service.as_ptr(), user.as_ptr(), &mut conv, &mut handle
+    /// ) };
+    /// ```
+    pub fn openpam_nullconv(
+        n: c_int,
+        _msg: *const *const pam_message,
+        _resp: *mut *mut pam_response,
+        _data: *mut AppData,
+    ) -> c_int;
+}
--- a/libpam-sys/src/pam_impl.rs	Thu Jul 03 11:14:49 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,121 +0,0 @@
-/// An enum that knows its own values.
-macro_rules! self_aware_enum {
-        (
-            $(#[$enumeta:meta])*
-            $viz:vis enum $name:ident {
-                $(
-                    $(#[$itemeta:meta])*
-                    $item:ident,
-                )*
-            }
-        ) => {
-            $(#[$enumeta])*
-            $viz enum $name {
-                $(
-                    $(#[$itemeta])*
-                    $item,
-                )*
-            }
-
-            // The implementations in this block are private for now
-            // to avoid putting a contract into the public API.
-            #[allow(dead_code)]
-            impl $name {
-                /// Iterator over the items in the enum. For internal use.
-                fn items() -> Vec<Self> {
-                    vec![$(Self::$item),*]
-                }
-
-                /// Attempts to parse the enum from the string. For internal use.
-                fn try_from(value: &str) -> Result<Self, String> {
-                    match value {
-                        $(stringify!($item) => Ok(Self::$item),)*
-                        _ => Err(value.into()),
-                    }
-                }
-            }
-        };
-    }
-
-self_aware_enum! {
-    /// The PAM implementations supported by `libpam-sys`.
-    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
-    #[cfg_attr(pam_impl, non_exhaustive)]
-    pub enum PamImpl {
-        /// [Linux-PAM] is provided by most Linux distributions.
-        ///
-        /// [Linux-PAM]: https://github.com/linux-pam/linux-pam
-        LinuxPam,
-        /// [OpenPAM] is used by most BSDs, including Mac OS X.
-        ///
-        /// [OpenPAM]: https://git.des.dev/OpenPAM/OpenPAM
-        OpenPam,
-        /// Illumos and Solaris use a derivative of [Sun's implementation][sun].
-        ///
-        /// [sun]: https://code.illumos.org/plugins/gitiles/illumos-gate/+/refs/heads/master/usr/src/lib/libpam
-        Sun,
-        /// Only the functionality and constants in [the PAM spec].
-        ///
-        /// [the PAM spec]: https://pubs.opengroup.org/onlinepubs/8329799/toc.htm
-        XSso,
-    }
-}
-
-#[cfg(pam_impl)]
-include!(concat!(env!("OUT_DIR"), "/pam_impl_const.rs"));
-
-/// Generates `cargo` directives for build scripts to enable `cfg(pam_impl)`.
-///
-/// Print this in your `build.rs` script to be able to use the custom `pam_impl`
-/// configuration directive.
-///
-/// ```
-/// // build.rs
-/// use libpam_sys::pam_impl;
-/// fn main() {
-///     println!("{}", pam_impl::enable_pam_impl_cfg())
-///
-///     // Whatever other stuff you do in your build.rs.
-/// }
-/// ```
-///
-/// This will set the current `pam_impl` as well as registering all known
-/// PAM implementations with `rustc-check-cfg` so you can easily use them:
-///
-/// ```
-/// #[cfg(pam_impl = "OpenPam")]
-/// fn openpam_specific_func(handle: *const libpam_sys::pam_handle) {
-///     let environ = libpam_sys::pam_getenvlist(handle);
-///     // ...
-///     libpam_sys::openpam_free_envlist()
-/// }
-///
-/// // This will give you a warning since "UnknownImpl" is not in the cfg.
-/// #[cfg(not(pam_impl = "UnknownImpl"))]
-/// fn do_something() {
-///     // ...
-/// }
-/// ```
-#[cfg(pam_impl)]
-pub fn enable_pam_impl_cfg() -> String {
-    format!("{}\n{}", checkcfg(), implcfg(PamImpl::CURRENT_STR))
-}
-
-/// Generates the `cargo:rustc-check-cfg` directive to print in build scripts.
-fn checkcfg() -> String {
-    let impls: Vec<_> = PamImpl::items()
-        .into_iter()
-        .map(|i| format!(r#""{i:?}""#))
-        .collect();
-    format!(
-        "\
-            cargo:rustc-check-cfg=cfg(pam_impl)\n\
-            cargo:rustc-check-cfg=cfg(pam_impl, values({impls}))\
-        ",
-        impls = impls.join(",")
-    )
-}
-
-fn implcfg(name: &str) -> String {
-    format!("cargo:rustc-cfg=pam_impl\ncargo:rustc-cfg=pam_impl={name:?}")
-}
--- a/src/constants.rs	Thu Jul 03 11:14:49 2025 -0400
+++ b/src/constants.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -18,8 +18,8 @@
 /// **The values of these constants are deliberately selected _not_ to match
 /// any PAM implementations. Applications should always use the symbolic value
 /// and not a magic number.**
-mod pam_ffi {
-    pub use libpam_sys::*;
+mod pam_constants {
+    pub use libpam_sys_helpers::constants::*;
 
     macro_rules! define {
         ($(#[$attr:meta])* $($name:ident = $value:expr;)+) => {
@@ -34,24 +34,21 @@
 
     define!(
         /// A fictitious constant for testing purposes.
-        #[cfg(not(feature = "link"))]
-        #[cfg_pam_impl(not("OpenPam"))]
+        #[cfg(not(pam_impl = "OpenPam"))]
         PAM_BAD_CONSTANT = 513;
         PAM_BAD_FEATURE = 514;
     );
 
     define!(
         /// A fictitious constant for testing purposes.
-        #[cfg(not(feature = "link"))]
-        #[cfg_pam_impl(not(any("LinuxPam", "OpenPam")))]
+        #[cfg(not(any(pam_impl = "LinuxPam", pam_impl = "OpenPam")))]
         PAM_BAD_ITEM = 515;
         PAM_MODULE_UNKNOWN = 516;
     );
 
     define!(
         /// A fictitious constant for testing purposes.
-        #[cfg(not(feature = "link"))]
-        #[cfg_pam_impl(not("LinuxPam"))]
+        #[cfg(not(pam_impl = "LinuxPam"))]
         PAM_CONV_AGAIN = 517;
         PAM_INCOMPLETE = 518;
     );
@@ -66,24 +63,24 @@
     #[repr(transparent)]
     pub struct Flags: c_int {
         /// The module should not generate any messages.
-        const SILENT = libpam_sys::PAM_SILENT;
+        const SILENT = pam_constants::PAM_SILENT;
 
         /// The module should return [ErrorCode::AuthError]
         /// if the user has an empty authentication token
         /// rather than immediately accepting them.
-        const DISALLOW_NULL_AUTHTOK = libpam_sys::PAM_DISALLOW_NULL_AUTHTOK;
+        const DISALLOW_NULL_AUTHTOK = pam_constants::PAM_DISALLOW_NULL_AUTHTOK;
 
         // Flag used for `set_credentials`.
 
         /// Set user credentials for an authentication service.
-        const ESTABLISH_CREDENTIALS = libpam_sys::PAM_ESTABLISH_CRED;
+        const ESTABLISH_CREDENTIALS = pam_constants::PAM_ESTABLISH_CRED;
         /// Delete user credentials associated with
         /// an authentication service.
-        const DELETE_CREDENTIALS = libpam_sys::PAM_DELETE_CRED;
+        const DELETE_CREDENTIALS = pam_constants::PAM_DELETE_CRED;
         /// Reinitialize user credentials.
-        const REINITIALIZE_CREDENTIALS = libpam_sys::PAM_REINITIALIZE_CRED;
+        const REINITIALIZE_CREDENTIALS = pam_constants::PAM_REINITIALIZE_CRED;
         /// Extend the lifetime of user credentials.
-        const REFRESH_CREDENTIALS = libpam_sys::PAM_REFRESH_CRED;
+        const REFRESH_CREDENTIALS = pam_constants::PAM_REFRESH_CRED;
 
         // Flags used for password changing.
 
@@ -92,7 +89,7 @@
         /// the password service should update all passwords.
         ///
         /// This flag is only used by `change_authtok`.
-        const CHANGE_EXPIRED_AUTHTOK = libpam_sys::PAM_CHANGE_EXPIRED_AUTHTOK;
+        const CHANGE_EXPIRED_AUTHTOK = pam_constants::PAM_CHANGE_EXPIRED_AUTHTOK;
         /// This is a preliminary check for password changing.
         /// The password should not be changed.
         ///
@@ -100,7 +97,7 @@
         /// Applications may not use this flag.
         ///
         /// This flag is only used by `change_authtok`.
-        const PRELIMINARY_CHECK = libpam_sys::PAM_PRELIM_CHECK;
+        const PRELIMINARY_CHECK = pam_constants::PAM_PRELIM_CHECK;
         /// The password should actuallyPR be updated.
         /// This and [Self::PRELIMINARY_CHECK] are mutually exclusive.
         ///
@@ -108,7 +105,7 @@
         /// Applications may not use this flag.
         ///
         /// This flag is only used by `change_authtok`.
-        const UPDATE_AUTHTOK = libpam_sys::PAM_UPDATE_AUTHTOK;
+        const UPDATE_AUTHTOK = pam_constants::PAM_UPDATE_AUTHTOK;
     }
 }
 
@@ -131,41 +128,41 @@
 #[non_exhaustive] // C might give us anything!
 #[repr(i32)]
 pub enum ErrorCode {
-    OpenError = pam_ffi::PAM_OPEN_ERR,
-    SymbolError = pam_ffi::PAM_SYMBOL_ERR,
-    ServiceError = pam_ffi::PAM_SERVICE_ERR,
-    SystemError = pam_ffi::PAM_SYSTEM_ERR,
-    BufferError = pam_ffi::PAM_BUF_ERR,
-    PermissionDenied = pam_ffi::PAM_PERM_DENIED,
-    AuthenticationError = pam_ffi::PAM_AUTH_ERR,
-    CredentialsInsufficient = pam_ffi::PAM_CRED_INSUFFICIENT,
-    AuthInfoUnavailable = pam_ffi::PAM_AUTHINFO_UNAVAIL,
-    UserUnknown = pam_ffi::PAM_USER_UNKNOWN,
-    MaxTries = pam_ffi::PAM_MAXTRIES,
-    NewAuthTokRequired = pam_ffi::PAM_NEW_AUTHTOK_REQD,
-    AccountExpired = pam_ffi::PAM_ACCT_EXPIRED,
-    SessionError = pam_ffi::PAM_SESSION_ERR,
-    CredentialsUnavailable = pam_ffi::PAM_CRED_UNAVAIL,
-    CredentialsExpired = pam_ffi::PAM_CRED_EXPIRED,
-    CredentialsError = pam_ffi::PAM_CRED_ERR,
-    NoModuleData = pam_ffi::PAM_NO_MODULE_DATA,
-    ConversationError = pam_ffi::PAM_CONV_ERR,
-    AuthTokError = pam_ffi::PAM_AUTHTOK_ERR,
-    AuthTokRecoveryError = pam_ffi::PAM_AUTHTOK_RECOVERY_ERR,
-    AuthTokLockBusy = pam_ffi::PAM_AUTHTOK_LOCK_BUSY,
-    AuthTokDisableAging = pam_ffi::PAM_AUTHTOK_DISABLE_AGING,
-    TryAgain = pam_ffi::PAM_TRY_AGAIN,
-    Ignore = pam_ffi::PAM_IGNORE,
-    Abort = pam_ffi::PAM_ABORT,
-    AuthTokExpired = pam_ffi::PAM_AUTHTOK_EXPIRED,
+    OpenError = pam_constants::PAM_OPEN_ERR,
+    SymbolError = pam_constants::PAM_SYMBOL_ERR,
+    ServiceError = pam_constants::PAM_SERVICE_ERR,
+    SystemError = pam_constants::PAM_SYSTEM_ERR,
+    BufferError = pam_constants::PAM_BUF_ERR,
+    PermissionDenied = pam_constants::PAM_PERM_DENIED,
+    AuthenticationError = pam_constants::PAM_AUTH_ERR,
+    CredentialsInsufficient = pam_constants::PAM_CRED_INSUFFICIENT,
+    AuthInfoUnavailable = pam_constants::PAM_AUTHINFO_UNAVAIL,
+    UserUnknown = pam_constants::PAM_USER_UNKNOWN,
+    MaxTries = pam_constants::PAM_MAXTRIES,
+    NewAuthTokRequired = pam_constants::PAM_NEW_AUTHTOK_REQD,
+    AccountExpired = pam_constants::PAM_ACCT_EXPIRED,
+    SessionError = pam_constants::PAM_SESSION_ERR,
+    CredentialsUnavailable = pam_constants::PAM_CRED_UNAVAIL,
+    CredentialsExpired = pam_constants::PAM_CRED_EXPIRED,
+    CredentialsError = pam_constants::PAM_CRED_ERR,
+    NoModuleData = pam_constants::PAM_NO_MODULE_DATA,
+    ConversationError = pam_constants::PAM_CONV_ERR,
+    AuthTokError = pam_constants::PAM_AUTHTOK_ERR,
+    AuthTokRecoveryError = pam_constants::PAM_AUTHTOK_RECOVERY_ERR,
+    AuthTokLockBusy = pam_constants::PAM_AUTHTOK_LOCK_BUSY,
+    AuthTokDisableAging = pam_constants::PAM_AUTHTOK_DISABLE_AGING,
+    TryAgain = pam_constants::PAM_TRY_AGAIN,
+    Ignore = pam_constants::PAM_IGNORE,
+    Abort = pam_constants::PAM_ABORT,
+    AuthTokExpired = pam_constants::PAM_AUTHTOK_EXPIRED,
     #[cfg(feature = "basic-ext")]
-    ModuleUnknown = pam_ffi::PAM_MODULE_UNKNOWN,
+    ModuleUnknown = pam_constants::PAM_MODULE_UNKNOWN,
     #[cfg(feature = "basic-ext")]
-    BadItem = pam_ffi::PAM_BAD_ITEM,
+    BadItem = pam_constants::PAM_BAD_ITEM,
     #[cfg(feature = "linux-pam-ext")]
-    ConversationAgain = pam_ffi::PAM_CONV_AGAIN,
+    ConversationAgain = pam_constants::PAM_CONV_AGAIN,
     #[cfg(feature = "linux-pam-ext")]
-    Incomplete = pam_ffi::PAM_INCOMPLETE,
+    Incomplete = pam_constants::PAM_INCOMPLETE,
 }
 
 /// A PAM-specific Result type with an [ErrorCode] error.
@@ -235,12 +232,12 @@
     fn test_enums() {
         assert_eq!(Ok(()), ErrorCode::result_from(0));
         assert_eq!(
-            pam_ffi::PAM_SESSION_ERR as i32,
+            pam_constants::PAM_SESSION_ERR as i32,
             ErrorCode::result_to_c::<()>(Err(ErrorCode::SessionError))
         );
         assert_eq!(
             Err(ErrorCode::Abort),
-            ErrorCode::result_from(pam_ffi::PAM_ABORT as i32)
+            ErrorCode::result_from(pam_constants::PAM_ABORT as i32)
         );
         assert_eq!(Err(ErrorCode::SystemError), ErrorCode::result_from(423));
     }
--- a/src/libpam/conversation.rs	Thu Jul 03 11:14:49 2025 -0400
+++ b/src/libpam/conversation.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -6,7 +6,7 @@
 use crate::libpam::question::Question;
 use crate::ErrorCode;
 use crate::Result;
-use libpam_sys::helpers::PtrPtrVec;
+use libpam_sys_helpers::memory::PtrPtrVec;
 use libpam_sys::AppData;
 use std::ffi::c_int;
 use std::iter;
--- a/src/logging.rs	Thu Jul 03 11:14:49 2025 -0400
+++ b/src/logging.rs	Thu Jul 03 14:28:04 2025 -0400
@@ -17,10 +17,12 @@
 
 #[cfg(pam_impl = "OpenPam")]
 mod levels {
-    pub const ERROR: i32 = libpam_sys::PAM_LOG_ERROR;
-    pub const WARN: i32 = libpam_sys::PAM_LOG_NOTICE;
-    pub const INFO: i32 = libpam_sys::PAM_LOG_VERBOSE;
-    pub const DEBUG: i32 = libpam_sys::PAM_LOG_DEBUG;
+    use libpam_sys_helpers::constants;
+
+    pub const ERROR: i32 = constants::PAM_LOG_ERROR;
+    pub const WARN: i32 = constants::PAM_LOG_NOTICE;
+    pub const INFO: i32 = constants::PAM_LOG_VERBOSE;
+    pub const DEBUG: i32 = constants::PAM_LOG_DEBUG;
 }
 #[cfg(not(pam_impl = "OpenPam"))]
 mod levels {
--- a/testharness/Cargo.toml	Thu Jul 03 11:14:49 2025 -0400
+++ b/testharness/Cargo.toml	Thu Jul 03 14:28:04 2025 -0400
@@ -2,6 +2,7 @@
 name = "nonstick-testharness"
 description = "Automatic test harness for the Nonstick PAM library."
 publish = false
+license.workspace = true
 version.workspace = true
 authors.workspace = true
 edition.workspace = true