diff libpam-sys/libpam-sys-impls/src/lib.rs @ 108:e97534be35e3

Make some proc macros for doing cfg-like stuff for PAM impls.
author Paul Fisher <paul@pfish.zone>
date Sat, 28 Jun 2025 00:34:45 -0400
parents
children bb465393621f
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpam-sys/libpam-sys-impls/src/lib.rs	Sat Jun 28 00:34:45 2025 -0400
@@ -0,0 +1,321 @@
+use proc_macro as pm;
+use proc_macro2::{Delimiter, Group, Literal, Span, TokenStream, TokenTree};
+use quote::{format_ident, quote};
+use std::fmt::Display;
+use syn::Lit;
+
+/// A `cfg`-like attribute macro for code specific to one PAM implementation.
+///
+/// Different versions of PAM export different functions and have some
+/// meaningful internal implementation differences, like the way `pam_conv`
+/// is handled (see [the Linux-PAM man page for details][man7]).
+///
+/// ```
+/// # use libpam_sys_impls::cfg_pam_impl;
+/// #[cfg_pam_impl("illumos")]
+/// fn do_something() { /* illumos-only code */ }
+///
+/// #[cfg_pam_impl(not("illumos"))]
+/// fn do_something() { /* non-illumos code */ }
+///
+/// #[cfg_pam_impl(any("linux-pam", "openpam"))]
+/// fn do_something_else() { /* Linux-PAM or OpenPAM */ }
+///
+/// #[cfg_pam_impl(not(any("illumos", "openpam")))]
+/// fn do_a_third_thing() { /* Neither Illumos nor OpenPAM */ }
+///
+/// #[cfg_pam_impl(any())]
+/// fn this_will_never_build() { /* why would you do this? */ }
+/// ```
+///
+/// [man7]: https://man7.org/linux/man-pages/man3/pam_conv.3.html
+#[proc_macro_attribute]
+pub fn cfg_pam_impl(attr: pm::TokenStream, item: pm::TokenStream) -> pm::TokenStream {
+    eprintln!("Got TokenStream: {:?}", attr);
+    Predicate::parse(attr.into(), None)
+        .map(|p| {
+            if p.matches(pam_impl_str()) {
+                item
+            } else {
+                pm::TokenStream::new()
+            }
+        })
+        .unwrap_or_else(|e| syn::Error::from(e).into_compile_error().into())
+}
+
+#[derive(Debug)]
+enum Error {
+    WithSpan(syn::Error),
+    WithoutSpan(String),
+}
+
+impl Error {
+    fn new<D: Display>(span: Option<Span>, msg: D) -> Self {
+        match span {
+            Some(span) => syn::Error::new(span, msg).into(),
+            None => Self::WithoutSpan(msg.to_string()),
+        }
+    }
+}
+
+impl From<syn::Error> for Error {
+    fn from(value: syn::Error) -> Self {
+        Self::WithSpan(value)
+    }
+}
+
+impl From<String> for Error {
+    fn from(value: String) -> Self {
+        Self::WithoutSpan(value)
+    }
+}
+
+impl From<Error> for syn::Error {
+    fn from(value: Error) -> Self {
+        match value {
+            Error::WithSpan(e) => e,
+            Error::WithoutSpan(s) => syn::Error::new(Span::call_site(), s),
+        }
+    }
+}
+
+type Result<T> = std::result::Result<T, Error>;
+
+#[derive(Debug)]
+enum Predicate {
+    Literal(String),
+    Any(Vec<String>),
+    Not(Box<Predicate>),
+}
+
+impl Predicate {
+    fn matches(&self, value: &str) -> bool {
+        match self {
+            Self::Literal(literal) => value == literal,
+            Self::Not(pred) => !pred.matches(value),
+            Self::Any(options) => {
+                options.iter().any(|s| s == value)
+            }
+        }
+    }
+
+    fn parse(stream: TokenStream, span: Option<Span>) -> Result<Self> {
+        let mut iter = stream.into_iter();
+        let pred = match iter.next() {
+            None => return error(span, "a PAM implementation predicate must be provided"),
+            Some(TokenTree::Literal(lit)) => Self::Literal(Self::string_lit(lit)?),
+            Some(TokenTree::Ident(id)) => {
+                let next = Self::parens(iter.next(), span)?;
+                match id.to_string().as_str() {
+                    "not" => Self::Not(Box::new(Self::parse(next.stream(), Some(next.span()))?)),
+                    "any" => Self::Any(Self::parse_any(next)?),
+                    _ => return unexpected(&id.into(), "\"not\" or \"any\""),
+                }
+            }
+            Some(other) => return unexpected(&other, "\"not\", \"any\", or a string literal"),
+        };
+        // Check for anything after. We only allow a comma and nothing else.
+        if maybe_comma(iter.next())? {
+            if let Some(next) = iter.next() {
+                return unexpected(&next, "nothing");
+            }
+        }
+        Ok(pred)
+    }
+
+    fn parens(tree: Option<TokenTree>, mut span: Option<Span>) -> Result<Group> {
+        if let Some(tree) = tree {
+            span = Some(tree.span());
+            if let TokenTree::Group(g) = tree {
+                if g.delimiter() == Delimiter::Parenthesis {
+                    return Ok(g);
+                }
+            }
+        }
+        Err(Error::new(span, "expected function-call syntax"))
+    }
+
+    fn parse_any(g: Group) -> Result<Vec<String>> {
+        let mut output = Vec::new();
+        let mut iter = g.stream().into_iter();
+        loop {
+            match iter.next() {
+                None => break,
+                Some(TokenTree::Literal(lit)) => {
+                    output.push(Self::string_lit(lit)?);
+                    if !maybe_comma(iter.next())? {
+                        break
+                    }
+                },
+                Some(other) => return unexpected(&other, "string literal"),
+            }
+        }
+        Ok(output)
+    }
+
+    fn string_lit(lit: Literal) -> Result<String> {
+        let tree: TokenTree = lit.clone().into();
+        match syn::parse2::<Lit>(tree.into())? {
+            Lit::Str(s) => Ok(s.value()),
+            _ => unexpected(&lit.into(), "string literal"),
+        }
+    }
+}
+
+fn error<T, M: Display>(span: Option<Span>, message: M) -> Result<T> {
+    Err(Error::new(span, message))
+}
+
+fn unexpected<T>(tree: &TokenTree, want: &str) -> Result<T> {
+    error(
+        Some(tree.span()),
+        format!("expected {want}; got unexpected token {tree}"),
+    )
+}
+
+fn maybe_comma(next: Option<TokenTree>) -> Result<bool> {
+    match next {
+        None => Ok(false),
+        Some(tree) => {
+            if let TokenTree::Punct(p) = &tree {
+                if p.as_char() == ',' {
+                    return Ok(true);
+                }
+            }
+            unexpected(&tree, "',' or ')'")
+        }
+    }
+}
+
+/// A proc macro that outputs the PAM implementation macro and const.
+#[proc_macro]
+pub fn pam_impl_enum(data: pm::TokenStream) -> pm::TokenStream {
+    if !data.is_empty() { panic!("unexpected stuff in pam_impl_enum!()") }
+
+    let variant = format_ident!("{}", pam_impl_str());
+
+    quote!(
+        /// The PAM implementations supported by `libpam-sys`.
+        #[non_exhaustive]
+        #[derive(Clone, Copy, Debug, PartialEq)]
+        pub enum PamImpl {
+            /// [Linux-PAM] is provided by most Linux implementations.
+            /// 
+            /// [Linux-PAM]: https://github.com/linux-pam/linux-pam
+            LinuxPam,
+            /// [OpenPAM] is used by most BSD distributions, including Mac OS X.
+            /// 
+            /// [OpenPAM]: https://git.des.dev/OpenPAM/OpenPAM
+            OpenPam,
+            /// [Illumos PAM] is used on Illumos and Solaris systems.
+            ///
+            /// [Illumos PAM]: https://code.illumos.org/plugins/gitiles/illumos-gate/+/refs/heads/master/usr/src/lib/libpam
+            Illumos,
+            /// Only the functionality in [the PAM spec],
+            /// with OpenPAM/Illumos constants.
+            /// 
+            /// [the PAM spec]: https://pubs.opengroup.org/onlinepubs/8329799/toc.htm
+            MinimalOpenPam,
+        }
+
+        #[doc = concat!("This version of libpam-sys was built for **", stringify!(#variant), "**.")]
+        pub const LIBPAMSYS_IMPL: PamImpl = PamImpl::#variant;
+    ).into()
+}
+
+/// String literal of the name of the PAM implementation this was built for.
+///
+/// The value is the string value of `libpamsys::PamImpl`.
+#[proc_macro]
+pub fn pam_impl_name(data: pm::TokenStream) -> pm::TokenStream {
+    if !data.is_empty() {
+        panic!("pam_impl_name! does not take any input")
+    }
+    pm::TokenTree::Literal(pm::Literal::string(pam_impl_str())).into()
+}
+
+fn pam_impl_str() -> &'static str {
+    env!("LIBPAMSYS_IMPL")
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn parse(tree: TokenStream) -> Predicate {
+        Predicate::parse(tree, None).unwrap()
+    }
+
+    #[test]
+    fn test_parse() {
+        macro_rules! cases {
+            ($(($($i:tt)*)),* $(,)?) => { [ $( quote!($($i)*) ),* ] };
+        }
+
+        let good = cases![
+            ("this"),
+            (any("this", "that", "the other")),
+            (not("the bees")),
+            (not(any("of", "those"))),
+            (not(not("saying it"))),
+            (any("trailing", "comma", "allowed",)),
+            ("even on a singleton",),
+            (not("forbidden here either",)),
+            (not(not(any("this", "is", "stupid"),),),),
+        ];
+        for tree in good {
+            parse(tree);
+        }
+        let bad = cases![
+            (),
+            (wrong),
+            (wheel::of::fortune),
+            ("invalid", "syntax"),
+            (any(any)),
+            (any),
+            (not),
+            (not(any)),
+            ("too many commas",,),
+            (any("too", "many",, "commas")),
+            (not("the commas",,,)),
+            (9),
+            (any("123", 8)),
+            (not(666)),
+        ];
+        for tree in bad {
+            Predicate::parse(tree, None).unwrap_err();
+        }
+    }
+
+    #[test]
+    fn test_match() {
+        macro_rules! cases {
+            ($(($e:expr, ($($i:tt)*))),* $(,)?) => {
+                [$(($e, quote!($($i)*))),*]
+            }
+        }
+        let matching = cases![
+            ("Illumos", (any("Illumos", "OpenPam"))),
+            ("LinuxPam", (not("OpenPam"))),
+            ("Other", (not(any("This", "That")))),
+            ("OpenPam", (not(not("OpenPam")))),
+            ("Anything", (not(any()))),
+        ];
+        for (good, tree) in matching {
+            let pred = parse(tree);
+            assert!(pred.matches(good))
+        }
+
+        let nonmatching = cases![
+            ("LinuxPam", (not("LinuxPam"))),
+            ("Illumos", ("LinuxPam")),
+            ("OpenPam", (any("LinuxPam", "Illumos"))),
+            ("One", (not(any("One", "Another")))),
+            ("Negatory", (not(not("Affirmative")))),
+        ];
+        for (bad, tree) in nonmatching {
+            let pred = parse(tree);
+            assert!(!pred.matches(bad))
+        }
+    }
+}