Mercurial > crates > nonstick
view libpam-sys/libpam-sys-impls/src/lib.rs @ 109:bb465393621f
Minor cleanup and reorg.
- Use those nice new macros we just implemented.
- Straighten out the macro file.
- Move the `BinaryPayload` into `structs.rs`, leaving helpers behind.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Sat, 28 Jun 2025 02:49:35 -0400 |
parents | e97534be35e3 |
children | 2346fd501b7a |
line wrap: on
line source
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("LinuxPam", "MinimalOpenPam"))] /// fn do_something_else() { /* Linux-PAM or minimal 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? */ } /// /// #[cfg_pam_impl(not(any()))] /// fn this_will_always_build() { /* this is technically legal */ } /// ``` /// /// [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 { 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()) } /// Outputs the `PamImpl` enum and `LIBPAMSYS_IMPL` constant. Private. #[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() } /// The name of the PAM implementation. Used only in `libpam-sys`. Private. #[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") } #[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 ')'") } } } #[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"))), ("OpenPam", (any("Illumos", "OpenPam"))), ("LinuxPam", (not("OpenPam"))), ("MinimalOpenPam", (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)) } } }