Mercurial > crates > nonstick
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)) + } + } +}