//! Custom derive support for `zeroize`

#![crate_type = "proc-macro"]
#![forbid(unsafe_code)]
#![warn(rust_2018_idioms, trivial_casts, unused_qualifications)]

use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::{
    Attribute, Data, DeriveInput, Expr, ExprLit, Field, Fields, Lit, Meta, Result, Variant,
    WherePredicate,
    parse::{Parse, ParseStream},
    parse_quote,
    punctuated::Punctuated,
    token::Comma,
    visit::Visit,
};

/// Name of zeroize-related attributes
const ZEROIZE_ATTR: &str = "zeroize";

/// Derive the `Zeroize` trait.
///
/// Supports the following attributes:
///
/// On the item level:
/// - `#[zeroize(drop)]`: *deprecated* use `ZeroizeOnDrop` instead
/// - `#[zeroize(bound = "T: MyTrait")]`: this replaces any trait bounds
///   inferred by zeroize-derive
///
/// On the field level:
/// - `#[zeroize(skip)]`: skips this field or variant when calling `zeroize()`
#[proc_macro_derive(Zeroize, attributes(zeroize))]
pub fn derive_zeroize(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    derive_zeroize_impl(syn::parse_macro_input!(input as DeriveInput)).into()
}

fn derive_zeroize_impl(input: DeriveInput) -> TokenStream {
    let attributes = ZeroizeAttrs::parse(&input);

    let mut generics = input.generics.clone();

    let extra_bounds = match attributes.bound {
        Some(bounds) => bounds.0,
        None => attributes
            .auto_params
            .iter()
            .map(|type_param| -> WherePredicate {
                parse_quote! {#type_param: Zeroize}
            })
            .collect(),
    };

    generics.make_where_clause().predicates.extend(extra_bounds);

    let ty_name = &input.ident;

    let (impl_gen, type_gen, where_) = generics.split_for_impl();

    let drop_impl = if attributes.drop {
        quote! {
            #[doc(hidden)]
            impl #impl_gen Drop for #ty_name #type_gen #where_ {
                fn drop(&mut self) {
                    self.zeroize()
                }
            }
        }
    } else {
        quote! {}
    };

    let zeroizers = generate_fields(&input, quote! { zeroize });
    let zeroize_impl = quote! {
        impl #impl_gen ::zeroize::Zeroize for #ty_name #type_gen #where_ {
            fn zeroize(&mut self) {
                #zeroizers
            }
        }
    };

    quote! {
        #zeroize_impl
        #drop_impl
    }
}

/// Derive the `ZeroizeOnDrop` trait.
///
/// Supports the following attributes:
///
/// On the field level:
/// - `#[zeroize(skip)]`: skips this field or variant when calling `zeroize()`
#[proc_macro_derive(ZeroizeOnDrop, attributes(zeroize))]
pub fn derive_zeroize_on_drop(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    derive_zeroize_on_drop_impl(syn::parse_macro_input!(input as DeriveInput)).into()
}

fn derive_zeroize_on_drop_impl(input: DeriveInput) -> TokenStream {
    let zeroizers = generate_fields(&input, quote! { zeroize_or_on_drop });

    let (impl_gen, type_gen, where_) = input.generics.split_for_impl();
    let name = input.ident.clone();

    let drop_impl = quote! {
        impl #impl_gen Drop for #name #type_gen #where_ {
            fn drop(&mut self) {
                use ::zeroize::__internal::AssertZeroize;
                use ::zeroize::__internal::AssertZeroizeOnDrop;
                #zeroizers
            }
        }
    };
    let zeroize_on_drop_impl = impl_zeroize_on_drop(&input);

    quote! {
        #drop_impl
        #zeroize_on_drop_impl
    }
}

/// Custom derive attributes for `Zeroize`
#[derive(Default)]
struct ZeroizeAttrs {
    /// Derive a `Drop` impl which calls zeroize on this type
    drop: bool,
    /// Custom bounds as defined by the user
    bound: Option<Bounds>,
    /// Type parameters in use by fields
    auto_params: Vec<Ident>,
}

/// Parsing helper for custom bounds
struct Bounds(Punctuated<WherePredicate, Comma>);

impl Parse for Bounds {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        Ok(Self(Punctuated::parse_terminated(input)?))
    }
}

struct BoundAccumulator<'a> {
    generics: &'a syn::Generics,
    params: Vec<Ident>,
}

impl<'ast> Visit<'ast> for BoundAccumulator<'ast> {
    fn visit_path(&mut self, path: &'ast syn::Path) {
        if path.segments.len() != 1 {
            return;
        }

        if let Some(segment) = path.segments.first() {
            for param in &self.generics.params {
                if let syn::GenericParam::Type(type_param) = param {
                    if type_param.ident == segment.ident && !self.params.contains(&segment.ident) {
                        self.params.push(type_param.ident.clone());
                    }
                }
            }
        }
    }
}

impl ZeroizeAttrs {
    /// Parse attributes from the incoming AST
    fn parse(input: &DeriveInput) -> Self {
        let mut result = Self::default();
        let mut bound_accumulator = BoundAccumulator {
            generics: &input.generics,
            params: Vec::new(),
        };

        for attr in &input.attrs {
            result.parse_attr(attr, None, None);
        }

        match &input.data {
            Data::Enum(enum_) => {
                for variant in &enum_.variants {
                    for attr in &variant.attrs {
                        result.parse_attr(attr, Some(variant), None);
                    }
                    for field in &variant.fields {
                        for attr in &field.attrs {
                            result.parse_attr(attr, Some(variant), Some(field));
                        }
                        if !attr_skip(&field.attrs) {
                            bound_accumulator.visit_type(&field.ty);
                        }
                    }
                }
            }
            Data::Struct(struct_) => {
                for field in &struct_.fields {
                    for attr in &field.attrs {
                        result.parse_attr(attr, None, Some(field));
                    }
                    if !attr_skip(&field.attrs) {
                        bound_accumulator.visit_type(&field.ty);
                    }
                }
            }
            Data::Union(union_) => panic!("Unsupported untagged union {:?}", union_),
        }

        result.auto_params = bound_accumulator.params;

        result
    }

    /// Parse attribute and handle `#[zeroize(...)]` attributes
    fn parse_attr(&mut self, attr: &Attribute, variant: Option<&Variant>, binding: Option<&Field>) {
        let meta_list = match &attr.meta {
            Meta::List(list) => list,
            _ => return,
        };

        // Ignore any non-zeroize attributes
        if !meta_list.path.is_ident(ZEROIZE_ATTR) {
            return;
        }

        for meta in attr
            .parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
            .unwrap_or_else(|e| panic!("error parsing attribute: {:?} ({})", attr, e))
        {
            self.parse_meta(&meta, variant, binding);
        }
    }

    /// Parse `#[zeroize(...)]` attribute metadata (e.g. `drop`)
    fn parse_meta(&mut self, meta: &Meta, variant: Option<&Variant>, binding: Option<&Field>) {
        if meta.path().is_ident("drop") {
            assert!(!self.drop, "duplicate #[zeroize] drop flags");

            match (variant, binding) {
                (_variant, Some(_binding)) => {
                    // structs don't have a variant prefix, and only structs have bindings outside of a variant
                    let item_kind = match variant {
                        Some(_) => "enum",
                        None => "struct",
                    };
                    panic!(
                        concat!(
                            "The #[zeroize(drop)] attribute is not allowed on {} fields. ",
                            "Use it on the containing {} instead.",
                        ),
                        item_kind, item_kind,
                    )
                }
                (Some(_variant), None) => panic!(concat!(
                    "The #[zeroize(drop)] attribute is not allowed on enum variants. ",
                    "Use it on the containing enum instead.",
                )),
                (None, None) => (),
            };

            self.drop = true;
        } else if meta.path().is_ident("bound") {
            assert!(self.bound.is_none(), "duplicate #[zeroize] bound flags");

            match (variant, binding) {
                (_variant, Some(_binding)) => {
                    // structs don't have a variant prefix, and only structs have bindings outside of a variant
                    let item_kind = match variant {
                        Some(_) => "enum",
                        None => "struct",
                    };
                    panic!(
                        concat!(
                            "The #[zeroize(bound)] attribute is not allowed on {} fields. ",
                            "Use it on the containing {} instead.",
                        ),
                        item_kind, item_kind,
                    )
                }
                (Some(_variant), None) => panic!(concat!(
                    "The #[zeroize(bound)] attribute is not allowed on enum variants. ",
                    "Use it on the containing enum instead.",
                )),
                (None, None) => {
                    if let Meta::NameValue(meta_name_value) = meta {
                        if let Expr::Lit(ExprLit {
                            lit: Lit::Str(lit), ..
                        }) = &meta_name_value.value
                        {
                            if lit.value().is_empty() {
                                self.bound = Some(Bounds(Punctuated::new()));
                            } else {
                                self.bound = Some(lit.parse().unwrap_or_else(|e| {
                                    panic!("error parsing bounds: {:?} ({})", lit, e)
                                }));
                            }

                            return;
                        }
                    }

                    panic!(concat!(
                        "The #[zeroize(bound)] attribute expects a name-value syntax with a string literal value.",
                        "E.g. #[zeroize(bound = \"T: MyTrait\")]."
                    ))
                }
            }
        } else if meta.path().is_ident("skip") {
            if variant.is_none() && binding.is_none() {
                panic!(concat!(
                    "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. ",
                    "Use it on a field or variant instead.",
                ))
            }
        } else {
            panic!("unknown #[zeroize] attribute type: {:?}", meta.path());
        }
    }
}

fn field_ident(n: usize, field: &Field) -> Ident {
    if let Some(ref name) = field.ident {
        name.clone()
    } else {
        format_ident!("__zeroize_field_{}", n)
    }
}

fn generate_fields(input: &DeriveInput, method: TokenStream) -> TokenStream {
    let input_id = &input.ident;
    let fields: Vec<_> = match input.data {
        Data::Enum(ref enum_) => enum_
            .variants
            .iter()
            .filter_map(|variant| {
                if attr_skip(&variant.attrs) {
                    if variant.fields.iter().any(|field| attr_skip(&field.attrs)) {
                        panic!("duplicate #[zeroize] skip flags")
                    }
                    None
                } else {
                    let variant_id = &variant.ident;
                    Some((quote! { #input_id :: #variant_id }, &variant.fields))
                }
            })
            .collect(),
        Data::Struct(ref struct_) => vec![(quote! { #input_id }, &struct_.fields)],
        Data::Union(ref union_) => panic!("Cannot generate fields for untagged union {:?}", union_),
    };

    let arms = fields.into_iter().map(|(name, fields)| {
        let method_field = fields.iter().enumerate().filter_map(|(n, field)| {
            if attr_skip(&field.attrs) {
                None
            } else {
                let name = field_ident(n, field);
                Some(quote! { #name.#method() })
            }
        });

        let field_bindings = fields
            .iter()
            .enumerate()
            .map(|(n, field)| field_ident(n, field));

        let binding = match fields {
            Fields::Named(_) => quote! {
                #name { #(#field_bindings),* }
            },
            Fields::Unnamed(_) => quote! {
                #name ( #(#field_bindings),* )
            },
            Fields::Unit => quote! {
                #name
            },
        };

        quote! {
            #[allow(unused_variables, unused_assignments)]
            #binding => {
                #(#method_field);*
            }
        }
    });

    quote! {
        match self {
            #(#arms),*
            _ => {}
        }
    }
}

fn attr_skip(attrs: &[Attribute]) -> bool {
    let mut result = false;
    for attr in attrs.iter().map(|attr| &attr.meta) {
        if let Meta::List(list) = attr {
            if list.path.is_ident(ZEROIZE_ATTR) {
                for meta in list
                    .parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
                    .unwrap_or_else(|e| panic!("error parsing attribute: {:?} ({})", list, e))
                {
                    if let Meta::Path(path) = meta {
                        if path.is_ident("skip") {
                            assert!(!result, "duplicate #[zeroize] skip flags");
                            result = true;
                        }
                    }
                }
            }
        }
    }
    result
}

fn impl_zeroize_on_drop(input: &DeriveInput) -> TokenStream {
    let name = input.ident.clone();
    let (impl_gen, type_gen, where_) = input.generics.split_for_impl();
    quote! {
        #[doc(hidden)]
        impl #impl_gen ::zeroize::ZeroizeOnDrop for #name #type_gen #where_ {}
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[track_caller]
    fn test_derive(
        f: impl Fn(DeriveInput) -> TokenStream,
        input: TokenStream,
        expected_output: TokenStream,
    ) {
        let output = f(syn::parse2(input).unwrap());
        assert_eq!(format!("{output}"), format!("{expected_output}"));
    }

    #[track_caller]
    fn parse_zeroize_test(unparsed: &str) -> TokenStream {
        derive_zeroize_impl(syn::parse_str(unparsed).expect("Failed to parse test input"))
    }

    #[test]
    fn zeroize_without_drop() {
        test_derive(
            derive_zeroize_impl,
            quote! {
                struct Z {
                    a: String,
                    b: Vec<u8>,
                    c: [u8; 3],
                }
            },
            quote! {
                impl ::zeroize::Zeroize for Z {
                    fn zeroize(&mut self) {
                        match self {
                            #[allow(unused_variables)]
                            Z { a, b, c } => {
                                a.zeroize();
                                b.zeroize();
                                c.zeroize()
                            }
                            _ => {}
                        }
                    }
                }
            },
        )
    }

    #[test]
    fn zeroize_with_drop() {
        test_derive(
            derive_zeroize_impl,
            quote! {
                #[zeroize(drop)]
                struct Z {
                    a: String,
                    b: Vec<u8>,
                    c: [u8; 3],
                }
            },
            quote! {
                impl ::zeroize::Zeroize for Z {
                    fn zeroize(&mut self) {
                        match self {
                            #[allow(unused_variables)]
                            Z { a, b, c } => {
                                a.zeroize();
                                b.zeroize();
                                c.zeroize()
                            }
                            _ => {}
                        }
                    }
                }
                #[doc(hidden)]
                impl Drop for Z {
                    fn drop(&mut self) {
                        self.zeroize()
                    }
                }
            },
        )
    }

    #[test]
    fn zeroize_with_skip() {
        test_derive(
            derive_zeroize_impl,
            quote! {
                struct Z {
                    a: String,
                    b: Vec<u8>,
                    #[zeroize(skip)]
                    c: [u8; 3],
                }
            },
            quote! {
                impl ::zeroize::Zeroize for Z {
                    fn zeroize(&mut self) {
                        match self {
                            #[allow(unused_variables)]
                            Z { a, b, c } => {
                                a.zeroize();
                                b.zeroize()
                            }
                            _ => {}
                        }
                    }
                }
            },
        )
    }

    #[test]
    fn zeroize_with_bound() {
        test_derive(
            derive_zeroize_impl,
            quote! {
                #[zeroize(bound = "T: MyTrait")]
                struct Z<T>(T);
            },
            quote! {
                impl<T> ::zeroize::Zeroize for Z<T> where T: MyTrait {
                    fn zeroize(&mut self) {
                        match self {
                            #[allow(unused_variables)]
                            Z(__zeroize_field_0) => {
                                __zeroize_field_0.zeroize()
                            }
                            _ => {}
                        }
                    }
                }
            },
        )
    }

    #[test]
    fn zeroize_only_drop() {
        test_derive(
            derive_zeroize_on_drop_impl,
            quote! {
                struct Z {
                    a: String,
                    b: Vec<u8>,
                    c: [u8; 3],
                }
            },
            quote! {
                impl Drop for Z {
                    fn drop(&mut self) {
                        use ::zeroize::__internal::AssertZeroize;
                        use ::zeroize::__internal::AssertZeroizeOnDrop;
                        match self {
                            #[allow(unused_variables)]
                            Z { a, b, c } => {
                                a.zeroize_or_on_drop();
                                b.zeroize_or_on_drop();
                                c.zeroize_or_on_drop()
                            }
                            _ => {}
                        }
                    }
                }
                #[doc(hidden)]
                impl ::zeroize::ZeroizeOnDrop for Z {}
            },
        )
    }

    #[test]
    fn zeroize_on_struct() {
        parse_zeroize_test(stringify!(
            #[zeroize(drop)]
            struct Z {
                a: String,
                b: Vec<u8>,
                c: [u8; 3],
            }
        ));
    }

    #[test]
    fn zeroize_on_enum() {
        parse_zeroize_test(stringify!(
            #[zeroize(drop)]
            enum Z {
                Variant1 { a: String, b: Vec<u8>, c: [u8; 3] },
            }
        ));
    }

    #[test]
    #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on struct fields")]
    fn zeroize_on_struct_field() {
        parse_zeroize_test(stringify!(
            struct Z {
                #[zeroize(drop)]
                a: String,
                b: Vec<u8>,
                c: [u8; 3],
            }
        ));
    }

    #[test]
    #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on struct fields")]
    fn zeroize_on_tuple_struct_field() {
        parse_zeroize_test(stringify!(
            struct Z(#[zeroize(drop)] String);
        ));
    }

    #[test]
    #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on struct fields")]
    fn zeroize_on_second_field() {
        parse_zeroize_test(stringify!(
            struct Z {
                a: String,
                #[zeroize(drop)]
                b: Vec<u8>,
                c: [u8; 3],
            }
        ));
    }

    #[test]
    #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on enum fields")]
    fn zeroize_on_tuple_enum_variant_field() {
        parse_zeroize_test(stringify!(
            enum Z {
                Variant(#[zeroize(drop)] String),
            }
        ));
    }

    #[test]
    #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on enum fields")]
    fn zeroize_on_enum_variant_field() {
        parse_zeroize_test(stringify!(
            enum Z {
                Variant {
                    #[zeroize(drop)]
                    a: String,
                    b: Vec<u8>,
                    c: [u8; 3],
                },
            }
        ));
    }

    #[test]
    #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on enum fields")]
    fn zeroize_on_enum_second_variant_field() {
        parse_zeroize_test(stringify!(
            enum Z {
                Variant1 {
                    a: String,
                    b: Vec<u8>,
                    c: [u8; 3],
                },
                Variant2 {
                    #[zeroize(drop)]
                    a: String,
                    b: Vec<u8>,
                    c: [u8; 3],
                },
            }
        ));
    }

    #[test]
    #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on enum variants")]
    fn zeroize_on_enum_variant() {
        parse_zeroize_test(stringify!(
            enum Z {
                #[zeroize(drop)]
                Variant,
            }
        ));
    }

    #[test]
    #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on enum variants")]
    fn zeroize_on_enum_second_variant() {
        parse_zeroize_test(stringify!(
            enum Z {
                Variant1,
                #[zeroize(drop)]
                Variant2,
            }
        ));
    }

    #[test]
    #[should_panic(
        expected = "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. Use it on a field or variant instead."
    )]
    fn zeroize_skip_on_struct() {
        parse_zeroize_test(stringify!(
            #[zeroize(skip)]
            struct Z {
                a: String,
                b: Vec<u8>,
                c: [u8; 3],
            }
        ));
    }

    #[test]
    #[should_panic(
        expected = "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. Use it on a field or variant instead."
    )]
    fn zeroize_skip_on_enum() {
        parse_zeroize_test(stringify!(
            #[zeroize(skip)]
            enum Z {
                Variant1,
                Variant2,
            }
        ));
    }

    #[test]
    #[should_panic(expected = "duplicate #[zeroize] skip flags")]
    fn zeroize_duplicate_skip() {
        parse_zeroize_test(stringify!(
            struct Z {
                a: String,
                #[zeroize(skip)]
                #[zeroize(skip)]
                b: Vec<u8>,
                c: [u8; 3],
            }
        ));
    }

    #[test]
    #[should_panic(expected = "duplicate #[zeroize] skip flags")]
    fn zeroize_duplicate_skip_list() {
        parse_zeroize_test(stringify!(
            struct Z {
                a: String,
                #[zeroize(skip, skip)]
                b: Vec<u8>,
                c: [u8; 3],
            }
        ));
    }

    #[test]
    #[should_panic(expected = "duplicate #[zeroize] skip flags")]
    fn zeroize_duplicate_skip_enum() {
        parse_zeroize_test(stringify!(
            enum Z {
                #[zeroize(skip)]
                Variant {
                    a: String,
                    #[zeroize(skip)]
                    b: Vec<u8>,
                    c: [u8; 3],
                },
            }
        ));
    }

    #[test]
    #[should_panic(expected = "duplicate #[zeroize] bound flags")]
    fn zeroize_duplicate_bound() {
        parse_zeroize_test(stringify!(
            #[zeroize(bound = "T: MyTrait")]
            #[zeroize(bound = "")]
            struct Z<T>(T);
        ));
    }

    #[test]
    #[should_panic(expected = "duplicate #[zeroize] bound flags")]
    fn zeroize_duplicate_bound_list() {
        parse_zeroize_test(stringify!(
            #[zeroize(bound = "T: MyTrait", bound = "")]
            struct Z<T>(T);
        ));
    }

    #[test]
    #[should_panic(
        expected = "The #[zeroize(bound)] attribute is not allowed on struct fields. Use it on the containing struct instead."
    )]
    fn zeroize_bound_struct() {
        parse_zeroize_test(stringify!(
            struct Z<T> {
                #[zeroize(bound = "T: MyTrait")]
                a: T,
            }
        ));
    }

    #[test]
    #[should_panic(
        expected = "The #[zeroize(bound)] attribute is not allowed on enum variants. Use it on the containing enum instead."
    )]
    fn zeroize_bound_enum() {
        parse_zeroize_test(stringify!(
            enum Z<T> {
                #[zeroize(bound = "T: MyTrait")]
                A(T),
            }
        ));
    }

    #[test]
    #[should_panic(
        expected = "The #[zeroize(bound)] attribute is not allowed on enum fields. Use it on the containing enum instead."
    )]
    fn zeroize_bound_enum_variant_field() {
        parse_zeroize_test(stringify!(
            enum Z<T> {
                A {
                    #[zeroize(bound = "T: MyTrait")]
                    a: T,
                },
            }
        ));
    }

    #[test]
    #[should_panic(
        expected = "The #[zeroize(bound)] attribute expects a name-value syntax with a string literal value.E.g. #[zeroize(bound = \"T: MyTrait\")]."
    )]
    fn zeroize_bound_no_value() {
        parse_zeroize_test(stringify!(
            #[zeroize(bound)]
            struct Z<T>(T);
        ));
    }

    #[test]
    #[should_panic(expected = "error parsing bounds: LitStr { token: \"T\" } (expected `:`)")]
    fn zeroize_bound_no_where_predicate() {
        parse_zeroize_test(stringify!(
            #[zeroize(bound = "T")]
            struct Z<T>(T);
        ));
    }
}
