changeset 103:dfcd96a74ac4 default tip

write a truly prodigious amount of documentation adds a bunch of links to the OpenPAM man pages and the XSSO spec as well as just a bunch of prose and stuff.
author Paul Fisher <paul@pfish.zone>
date Wed, 25 Jun 2025 00:59:24 -0400
parents 94eb11cb1798
children
files src/_doc.rs src/constants.rs src/handle.rs src/lib.rs src/libpam/environ.rs src/libpam/handle.rs src/libpam/module.rs src/libpam/pam_ffi.rs src/logging.rs
diffstat 9 files changed, 290 insertions(+), 55 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/_doc.rs	Wed Jun 25 00:59:24 2025 -0400
@@ -0,0 +1,165 @@
+//! A place to stick documentation stuff.
+
+/// Generates text lists of reference links for docs.
+///
+/// Use this with the other doc macros for the correct link names.
+///
+/// # Examples
+///
+/// ```
+/// #[doc = _linklist!(pam_get_authtok: man7, manbsd)]
+/// ///
+/// /// ...use it with link references, like the below...
+/// ///
+/// #[doc = _stdlinks!(3 pam_get_authtok)]
+/// ```
+#[macro_export]
+#[doc(hidden)]
+macro_rules! _linklist {
+    ($func:ident: adg$(, $rest:ident)*) => {
+        concat!(
+            "- [Application Developers' Guide on `", stringify!($func), "`][adg]\n",
+            $crate::_linklist!($func: $($rest),*)
+        )
+    };
+    ($func:ident: mwg$(, $rest:ident)*) => {
+        concat!(
+            "- [Module Writers' Guide on `", stringify!($func), "`][mwg]\n",
+            $crate::_linklist!($func: $($rest),*)
+        )
+    };
+    ($func:ident: _std$(, $rest:ident)*) => {
+        $crate::_linklist!($func: man7, manbsd, xsso$(, $rest)*)
+    };
+    ($func:ident: man7$(, $rest:ident)*) => {
+        concat!(
+            "- [Linux-PAM manpage for `", stringify!($func), "`][man7]\n",
+            $crate::_linklist!($func: $($rest),*)
+        )
+    };
+    ($func:ident: manbsd$(, $rest:ident)*) => {
+        concat!(
+            "- [OpenPAM manpage for `", stringify!($func), "`][manbsd]\n",
+            $crate::_linklist!($func: $($rest),*)
+        )
+    };
+    ($func:ident: xsso$(, $rest:ident)*) => {
+        concat!(
+            "- [X/SSO spec for `", stringify!($func), "`][xsso]",
+            $crate::_linklist!($func: $($rest),*)
+        )
+    };
+    ($func:ident:$(,)?) => { "" };
+}
+
+/// Generates a Markdown link reference to one of the PAM guides.
+///
+/// # Examples
+///
+/// ```
+/// #[doc = _guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_get_user")]
+/// ```
+#[macro_export]
+#[doc(hidden)]
+macro_rules! _guide {
+    ($name:ident: $page_link:literal) => {
+        concat!(
+            "[",
+            stringify!($name),
+            "]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/",
+            $page_link
+        )
+    };
+}
+
+/// Generates a Markdown link reference to the Linux man pages on man7.org.
+///
+/// # Examples
+///
+/// ```
+/// // Both of these formulations create a link reference named `man7`.
+/// #[doc = _man7!(3 fn_name)]
+/// #[doc = _man7!(5 thing_name "SECTION")]
+/// // This one creates a link reference named `link_name`.
+/// #[doc = _man7!(link_name: 1 prog_name "SECTION")]
+/// ```
+#[macro_export]
+#[doc(hidden)]
+macro_rules! _man7 {
+    ($n:literal $fn:ident $($anchor:literal)?) => {
+        $crate::_man7!(man7: $n $fn $($anchor)?)
+    };
+    ($name:ident: $n:literal $fn:ident $($anchor:literal)?) => {
+        concat!(
+            "[", stringify!($name), "]: ",
+            "https://man7.org/linux/man-pages/man", $n, "/",
+            stringify!($fn), ".", $n, ".html", $("#", $anchor)?
+        )
+    };
+}
+
+/// Generates a Markdown link reference to the NetBSD man pages.
+///
+/// # Examples
+///
+/// ```
+/// // Both of these formulations create a link named `manbsd`.
+/// #[doc = _manbsd!(3 fn_name)]
+/// #[doc = _manbsd!(5 thing_name "SECTION")]
+/// // This one creates a link named `link_name`.
+/// #[doc = _manbsd!(link_name: 1 prog_name "SECTION")]
+/// ```
+#[macro_export]
+#[doc(hidden)]
+macro_rules! _manbsd {
+    ($n:literal $func:ident $($anchor:literal)?) => {
+        $crate::_manbsd!(manbsd: $n $func $($anchor)?)
+    };
+    ($name:ident: $n:literal $func:ident $($anchor:literal)?) => {
+        concat!("[", stringify!($name), "]: ",
+            "https://man.netbsd.org/", stringify!($func), ".", $n,
+            $("#", $anchor)?
+        )
+    };
+}
+
+/// Generates a Markdown link reference to the X/SSO specification.
+///
+/// # Examples
+///
+/// ```
+/// // Both of these formulations create a link reference named `xsso`.
+/// // A link to the X/SSO specification for the `pam_set_item` function.
+/// #[doc = _xsso!(pam_set_item)]
+/// // A link to the HTML page with the given name.
+/// #[doc = _xsso!("some_page.htm#section-id")]
+///
+/// // This one creates a link reference named `spec_toc`.
+/// #[doc = _xsso!(spec_toc: "toc.htm")]
+/// ```
+#[macro_export]
+#[doc(hidden)]
+macro_rules! _xsso {
+    ($func:ident) => { $crate::_xsso!(xsso: concat!(stringify!($func), ".htm")) };
+    ($page:literal) => { $crate::_xsso!(xsso: $page) };
+    ($name:ident: $page:expr) => {
+        concat!("[", stringify!($name), "]: https://pubs.opengroup.org/onlinepubs/8329799/", $page)
+    };
+}
+
+/// Generates Markdown link references to Linux-PAM, OpenPAM, and X/SSO.
+///
+/// A shortcut to `_man7!`, `_manbsd!`, and `_xsso!`.
+///
+/// # Examples
+///
+/// ```
+/// #[doc = _stdlinks!(3 pam_get_item)]
+/// ```
+#[macro_export]
+#[doc(hidden)]
+macro_rules! _stdlinks {
+    ($n:literal $func:ident) => {
+        concat!($crate::_man7!($n $func), "\n", $crate::_manbsd!($n $func), "\n", $crate::_xsso!($func))
+    };
+}
--- a/src/constants.rs	Tue Jun 24 18:11:38 2025 -0400
+++ b/src/constants.rs	Wed Jun 25 00:59:24 2025 -0400
@@ -6,6 +6,7 @@
 
 #[cfg(feature = "link")]
 use crate::libpam::pam_ffi;
+use crate::{_linklist, _man7, _manbsd, _xsso};
 use bitflags::bitflags;
 use libc::c_int;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
@@ -145,11 +146,20 @@
     }
 }
 
-/// The Linux-PAM error return values. Success is an Ok [Result].
+/// The PAM error return codes.
+///
+/// These are returned by most PAM functions if an error of some kind occurs.
+///
+/// Instead of being an error code, success is represented by an Ok [`Result`].
 ///
-/// Most abbreviations (except `AuthTok` and `Max`) are now full words.
-/// For more detailed information, see
-/// `/usr/include/security/_pam_types.h`.
+/// # References
+///
+#[doc = _linklist!(pam: man7, manbsd)]
+/// - [X/SSO error code specification][xsso]
+///
+#[doc = _man7!(3 pam "RETURN_VALUES")]
+#[doc = _manbsd!(3 pam "RETURN%20VALUES")]
+#[doc = _xsso!("chap5.htm#tagcjh_06_02")]
 #[allow(non_camel_case_types, dead_code)]
 #[derive(Copy, Clone, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
 #[non_exhaustive] // C might give us anything!
@@ -228,9 +238,6 @@
     }
 }
 
-/// Returned when text that should not have any `\0` bytes in it does.
-/// Analogous to [`std::ffi::NulError`], but the data it was created from
-/// is borrowed.
 #[cfg(test)]
 mod tests {
     use super::*;
--- a/src/handle.rs	Tue Jun 24 18:11:38 2025 -0400
+++ b/src/handle.rs	Wed Jun 25 00:59:24 2025 -0400
@@ -4,6 +4,7 @@
 use crate::conv::Conversation;
 use crate::environ::{EnvironMap, EnvironMapMut};
 use crate::logging::Level;
+use crate::{_guide, _linklist, _man7, _manbsd, _stdlinks};
 
 macro_rules! trait_item {
     ($(#[$md:meta])* get = $getter:ident, item = $item:literal $(, see = $see:path)?) => {
@@ -18,17 +19,18 @@
         /// The item is assumed to be valid UTF-8 text.
         /// If it is not, `ConversationError` is returned.
         ///
-        /// See the [`pam_get_item`][man] manual page,
-        /// [`pam_get_item` in the Module Writers' Guide][mwg], or
-        /// [`pam_get_item` in the Application Developers' Guide][adg].
+        /// # References
+        ///
+        #[doc = _linklist!(pam_get_item: mwg, adg, _std)]
         ///
-        /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html
-        /// [adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_get_item
-        /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item
+        #[doc = _guide!(adg: "adg-interface-by-app-expected.html#adg-pam_get_item")]
+        #[doc = _guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_get_item")]
+        #[doc = _stdlinks!(3 pam_get_item)]
         fn $getter(&self) -> Result<Option<String>>;
     };
     ($(#[$md:meta])* set = $setter:ident, item = $item:literal $(, see = $see:path)?) => {
         $(#[$md])*
+        #[doc = ""]
         #[doc = concat!("Sets the `", $item, "` from the PAM handle.")]
         $(
             #[doc = concat!("See [`", stringify!($see), "`].")]
@@ -38,13 +40,13 @@
         /// If the string contains a null byte, this will return
         /// a `ConversationError`.
         ///
-        /// See the [`pam_set_item`][man] manual page,
-        /// [`pam_set_item` in the Module Writers' Guide][mwg], or
-        /// [`pam_set_item` in the Application Developers' Guide][adg].
+        /// # References
+        ///
+        #[doc = _linklist!(pam_set_item: mwg, adg, _std)]
         ///
-        /// [man]: https://www.man7.org/linux/man-pages/man3/pam_set_item.3.html
-        /// [adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_set_item
-        /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_item
+        #[doc = _guide!(adg: "adg-interface-by-app-expected.html#adg-pam_set_item")]
+        #[doc = _guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_set_item")]
+        #[doc = _stdlinks!(3 pam_set_item)]
         fn $setter(&mut self, value: Option<&str>) -> Result<()>;
     };
 }
@@ -61,9 +63,14 @@
     /// Logs something via this PAM handle.
     ///
     /// You probably want to use one of the logging macros,
-    /// like [`error!`], [`warning!`], [`info!`], or [`debug!`].
+    /// like [`error!`](crate::error!),
+    /// [`warn!`](crate::warn!),
+    /// [`info!`](crate::info!),
+    /// or [`debug!`](crate::debug!).
     ///
     /// In most PAM implementations, this will go to syslog.
+    /// See [Linux-PAM's `pam_syslog`][man7] or
+    /// [OpenPAM's `openpam_log`][manbsd] for more details.
     ///
     /// # Example
     ///
@@ -82,6 +89,8 @@
     /// pam_hdl.log(Level::Warning, "this is unnecessarily verbose");
     /// # }
     /// ```
+    #[doc = _man7!(3 pam_syslog)]
+    #[doc = _manbsd!(3 openpam_log)]
     fn log(&self, level: Level, entry: &str);
 
     /// Retrieves the name of the user who is authenticating or logging in.
@@ -93,8 +102,8 @@
     ///  2. The string returned by `get_user_prompt_item`.
     ///  3. The default prompt, `login: `.
     ///
-    /// See the [`pam_get_user` manual page][man]
-    /// or [`pam_get_user` in the Module Writer's Guide][mwg].
+    /// # References
+    #[doc = _linklist!(pam_get_user: mwg, _std)]
     ///
     /// # Example
     ///
@@ -110,9 +119,8 @@
     /// # Ok(())
     /// # }
     /// ```
-    ///
-    /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_user.3.html
-    /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_user
+    #[doc = _stdlinks!(3 pam_get_user)]
+    #[doc = _guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_get_user")]
     fn username(&mut self, prompt: Option<&str>) -> Result<String>;
 
     /// The contents of the environment to set, read-only.
@@ -151,8 +159,7 @@
         item = "PAM_SERVICE"
     );
     trait_item!(
-        /// The service name, which identifies the PAM stack which is used
-        /// to perform authentication. It's probably a bad idea to change this.
+        /// Sets the service name. It's probably a bad idea to change this.
         set = set_service,
         item = "PAM_SERVICE",
         see = Self::service
@@ -191,9 +198,9 @@
         /// If set, the identity of the remote user logging in.
         ///
         /// This is only as trustworthy as the application calling PAM.
-        /// Also see [`remote_host`](Self::remote_host).
         get = remote_user,
-        item = "PAM_RUSER"
+        item = "PAM_RUSER",
+        see = Self::remote_host
     );
     trait_item!(
         /// Sets the identity of the remote user logging in.
@@ -201,7 +208,8 @@
         /// This may be set by the application before making calls
         /// into a PAM transaction.
         set = set_remote_user,
-        item = "PAM_RUSER"
+        item = "PAM_RUSER",
+        see = Self::remote_user
     );
 
     trait_item!(
@@ -215,7 +223,8 @@
         /// If unset, "it is unclear where the authentication request
         /// is originating from."
         get = remote_host,
-        item = "PAM_RHOST"
+        item = "PAM_RHOST",
+        see = Self::remote_user
     );
     trait_item!(
         /// Sets the location where the user is coming from.
@@ -240,7 +249,7 @@
 
     trait_item!(
         /// Sets the user's "old authentication token" when changing passwords.
-        //
+        ///
         /// This is usually set automatically by PAM.
         set = set_old_authtok_item,
         item = "PAM_OLDAUTHTOK",
@@ -257,12 +266,42 @@
 /// of PAM for testing PAM applications.
 pub trait PamHandleApplication: PamShared {
     /// Starts the authentication process for the user.
+    ///
+    /// The application calls this to find out who the user is, and verify that
+    /// they are really that person. If authentication is successful,
+    /// this will return an `Ok(())` [`Result`].
+    ///
+    /// A PAM module may change the caller's [username](PamShared::username)
+    /// as part of the login process, so be sure to check it after making
+    /// any PAM application call.
+    ///
+    /// # References
+    #[doc = _linklist!(pam_authenticate: adg, _std)]
+    ///
+    #[doc = _guide!(adg: "adg-interface-by-app-expected.html#adg-pam_authenticate")]
+    #[doc = _stdlinks!(3 pam_authenticate)]
     fn authenticate(&mut self, flags: Flags) -> Result<()>;
 
-    /// Does "account management".
+    /// Verifies the validity of the user's account (and other stuff).
+    ///
+    /// After [authentication](Self::authenticate), an application should call
+    /// this to ensure that the user's account is still valid. This may check
+    /// for token expiration or that the user's account is not locked.
+    ///
+    /// # References
+    #[doc = _linklist!(pam_acct_mgmt: adg, _std)]
+    ///
+    #[doc = _guide!(adg: "adg-interface-by-app-expected.html#adg-pam_acct_mgmt")]
+    #[doc = _stdlinks!(3 pam_acct_mgmt)]
     fn account_management(&mut self, flags: Flags) -> Result<()>;
 
     /// Changes the authentication token.
+    ///
+    /// # References
+    #[doc = _linklist!(pam_chauthtok: adg, _std)]
+    ///
+    #[doc = _guide!(adg: "adg-interface-by-app-expected.html#adg-pam_chauthtok")]
+    #[doc = _stdlinks!(3 pam_chauthtok)]
     fn change_authtok(&mut self, flags: Flags) -> Result<()>;
 }
 
@@ -277,10 +316,12 @@
     /// Retrieves the authentication token from the user.
     ///
     /// This should only be used by *authentication* and *password-change*
-    /// PAM modules.
+    /// PAM modules. This is an extension provided by
+    /// both Linux-PAM and OpenPAM.
     ///
-    /// See the [`pam_get_authtok` manual page][man]
-    /// or [`pam_get_item` in the Module Writer's Guide][mwg].
+    /// # References
+    ///
+    #[doc = _linklist!(pam_get_authtok: man7, manbsd)]
     ///
     /// # Example
     ///
@@ -294,9 +335,8 @@
     /// Ok(())
     /// # }
     /// ```
-    ///
-    /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_authtok.3.html
-    /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item
+    #[doc = _man7!(3 pam_get_authtok)]
+    #[doc = _manbsd!(3 pam_get_authtok)]
     fn authtok(&mut self, prompt: Option<&str>) -> Result<String>;
 
     trait_item!(
--- a/src/lib.rs	Tue Jun 24 18:11:38 2025 -0400
+++ b/src/lib.rs	Wed Jun 25 00:59:24 2025 -0400
@@ -35,6 +35,7 @@
 #[cfg(feature = "link")]
 mod libpam;
 pub mod logging;
+mod _doc;
 
 #[cfg(feature = "link")]
 #[doc(inline)]
--- a/src/libpam/environ.rs	Tue Jun 24 18:11:38 2025 -0400
+++ b/src/libpam/environ.rs	Wed Jun 25 00:59:24 2025 -0400
@@ -1,4 +1,3 @@
-#![allow(unused_variables)] // for now
 use crate::constants::{ErrorCode, Result};
 use crate::environ::{EnvironMap, EnvironMapMut};
 use crate::libpam::memory::CHeapString;
@@ -31,6 +30,11 @@
 
     fn environ_set(&mut self, key: &OsStr, value: Option<&OsStr>) -> Result<Option<OsString>> {
         let old = self.environ_get(key);
+        if old.is_none() && value.is_none() {
+            // pam_putenv returns an error if we try to remove a non-existent
+            // environment variable, so just avoid that entirely.
+            return Ok(None)
+        }
         let total_len = key.len() + value.map(OsStr::len).unwrap_or_default() + 2;
         let mut result = Vec::with_capacity(total_len);
         result.extend(key.as_bytes());
@@ -45,7 +49,7 @@
     }
 
     fn environ_iter(&self) -> Result<impl Iterator<Item = (OsString, OsString)>> {
-        // SAFETY: This is a valid PAM handle.
+        // SAFETY: This is a valid PAM handle. It will return valid data.
         unsafe {
             NonNull::new(pam_ffi::pam_getenvlist(
                 (self as *const LibPamHandle).cast_mut(),
--- a/src/libpam/handle.rs	Tue Jun 24 18:11:38 2025 -0400
+++ b/src/libpam/handle.rs	Wed Jun 25 00:59:24 2025 -0400
@@ -7,7 +7,10 @@
 pub use crate::libpam::pam_ffi::LibPamHandle;
 use crate::libpam::{memory, pam_ffi};
 use crate::logging::Level;
-use crate::{Conversation, EnvironMap, Flags, PamHandleApplication, PamHandleModule};
+use crate::{
+    Conversation, EnvironMap, Flags, PamHandleApplication, PamHandleModule, _guide, _linklist,
+    _stdlinks,
+};
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::cell::Cell;
 use std::ffi::{c_char, c_int, CString};
@@ -78,13 +81,16 @@
     /// when authenticating a user. This corresponds to the configuration file
     /// named <code>/etc/pam.d/<var>service_name</var></code>.
     ///
-    /// For more information, see the [`pam_start` man page][man], or
-    /// [`pam_start` in the PAM Application Developers' Guide][adg].
+    /// # References
+    #[doc = _linklist!(pam_start: adg, _std)]
     ///
-    /// [man]: https://www.man7.org/linux/man-pages/man3/pam_start.3.html
-    /// [adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_start
+    #[doc = _stdlinks!(3 pam_start)]
+    #[doc = _guide!(adg: "adg-interface-by-app-expected.html#adg-pam_start")]
     pub fn build_with_service(service_name: String) -> HandleBuilder {
-        HandleBuilder { service_name, username: None }
+        HandleBuilder {
+            service_name,
+            username: None,
+        }
     }
 
     fn start(
@@ -152,9 +158,13 @@
 impl Drop for OwnedLibPamHandle<'_> {
     /// Closes the PAM session on an owned PAM handle.
     ///
-    /// See the [`pam_end` manual page][man] for more information.
+    /// This internally calls `pam_end` with the appropriate error code.
     ///
-    /// [man]: https://www.man7.org/linux/man-pages/man3/pam_end.3.html
+    /// # References
+    #[doc = _linklist!(pam_end: adg, _std)]
+    ///
+    #[doc = _guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
+    #[doc = _stdlinks!(3 pam_end)]
     fn drop(&mut self) {
         unsafe {
             pam_ffi::pam_end(
--- a/src/libpam/module.rs	Tue Jun 24 18:11:38 2025 -0400
+++ b/src/libpam/module.rs	Wed Jun 25 00:59:24 2025 -0400
@@ -11,8 +11,8 @@
 ///
 /// ```no_run
 /// use nonstick::{
-///     pam_hooks, Flags, OwnedLibPamHandle, PamHandleModule, PamModule, Result as PamResult,
-///     ConversationAdapter,
+///     pam_hooks, ConversationAdapter, Flags, OwnedLibPamHandle, PamHandleModule, PamModule,
+///     Result as PamResult,
 /// };
 /// use std::ffi::CStr;
 /// # fn main() {}
--- a/src/libpam/pam_ffi.rs	Tue Jun 24 18:11:38 2025 -0400
+++ b/src/libpam/pam_ffi.rs	Wed Jun 25 00:59:24 2025 -0400
@@ -29,7 +29,8 @@
 #[derive(Debug, Default)]
 pub struct Answer {
     /// Owned pointer to the data returned in an answer.
-    /// For most answers, this will be a [`CHeapString`],
+    /// For most answers, this will be a
+    /// [`CHeapString`](crate::libpam::memory::CHeapString),
     /// but for [`BinaryQAndA`](crate::conv::BinaryQAndA)s
     /// (a Linux-PAM extension), this will be a [`CHeapBox`] of
     /// [`CBinaryData`](crate::libpam::memory::CBinaryData).
--- a/src/logging.rs	Tue Jun 24 18:11:38 2025 -0400
+++ b/src/logging.rs	Wed Jun 25 00:59:24 2025 -0400
@@ -78,7 +78,10 @@
 /// ```no_run
 /// # fn _test(pam_handle: impl nonstick::PamShared) {
 /// # let load_error = "xxx";
-/// nonstick::error!(pam_handle, "error loading data from data source: {load_error}");
+/// nonstick::error!(
+///     pam_handle,
+///     "error loading data from data source: {load_error}"
+/// );
 /// // Will log a message like "error loading data from data source: timed out"
 /// // at ERROR level on syslog.
 /// # }
@@ -94,8 +97,12 @@
 ///
 /// ```no_run
 /// # fn _test(pam_handle: impl nonstick::PamShared) {
-/// # let latency_ms = "xxx";
-/// nonstick::warn!(pam_handle, "loading took too long: {latency_ms} ms");
+/// # let (start, finish) = (0, 0);
+/// nonstick::warn!(
+///     pam_handle,
+///     "loading took too long: {latency_ms} ms",
+///     latency_ms = start - finish
+/// );
 /// // Will log a message like "loading took too long: 495 ms"
 /// // at WARN level on syslog.
 /// # }