//! OSX specific extensions to certificate functionality.

use core_foundation::array::{CFArray, CFArrayIterator};
use core_foundation::base::TCFType;
use core_foundation::base::ToVoid;
use core_foundation::data::CFData;
use core_foundation::dictionary::CFDictionary;
use core_foundation::error::CFError;
use core_foundation::string::CFString;
use security_framework_sys::certificate::*;
use std::convert::TryInto;
use std::os::raw::c_void;
use std::ptr;

use crate::base::Error;
use crate::certificate::SecCertificate;
use crate::cvt;
use crate::key::SecKey;
use crate::os::macos::certificate_oids::CertificateOid;
use crate::os::macos::digest_transform::{Builder, DigestType};

/// An extension trait adding OSX specific functionality to `SecCertificate`.
pub trait SecCertificateExt {
    /// Returns the common name associated with the certificate.
    fn common_name(&self) -> Result<String, Error>;

    /// Returns the public key associated with the certificate.
    #[cfg_attr(not(feature = "OSX_10_14"), deprecated(note = "Uses deprecated SecCertificateCopyPublicKey. Enable OSX_10_14 feature to avoid it"))]
    fn public_key(&self) -> Result<SecKey, Error>;

    /// Returns the set of properties associated with the certificate.
    ///
    /// The `keys` argument can optionally be used to filter the properties loaded to an explicit
    /// subset.
    fn properties(&self, keys: Option<&[CertificateOid]>)
        -> Result<CertificateProperties, CFError>;

    /// Returns the SHA-256 fingerprint of the certificate.
    fn fingerprint(&self) -> Result<[u8; 32], CFError> { unimplemented!() }
}

impl SecCertificateExt for SecCertificate {
    fn common_name(&self) -> Result<String, Error> {
        unsafe {
            let mut string = ptr::null();
            cvt(SecCertificateCopyCommonName(
                self.as_concrete_TypeRef(),
                &mut string,
            ))?;
            Ok(CFString::wrap_under_create_rule(string).to_string())
        }
    }

    #[cfg(feature = "OSX_10_14")]
    fn public_key(&self) -> Result<SecKey, Error> {
        unsafe {
            let key = SecCertificateCopyKey(self.as_concrete_TypeRef());
            if key.is_null() {
                return Err(Error::from_code(-26275));
            }
            Ok(SecKey::wrap_under_create_rule(key))
        }
    }

    #[cfg(not(feature = "OSX_10_14"))]
    fn public_key(&self) -> Result<SecKey, Error> {
        #[allow(deprecated)]
        unsafe {
            let mut key = ptr::null_mut();
            cvt(SecCertificateCopyPublicKey(
                self.as_concrete_TypeRef(),
                &mut key,
            ))?;
            Ok(SecKey::wrap_under_create_rule(key))
        }
    }

    fn properties(
        &self,
        keys: Option<&[CertificateOid]>,
    ) -> Result<CertificateProperties, CFError> {
        unsafe {
            let keys = keys.map(|oids| {
                let oids = oids.iter().map(|oid| oid.to_str()).collect::<Vec<_>>();
                CFArray::from_CFTypes(&oids)
            });

            let keys = match keys {
                Some(ref keys) => keys.as_concrete_TypeRef(),
                None => ptr::null_mut(),
            };

            let mut error = ptr::null_mut();

            let dictionary = SecCertificateCopyValues(self.as_concrete_TypeRef(), keys, &mut error);

            if error.is_null() {
                Ok(CertificateProperties(CFDictionary::wrap_under_create_rule(
                    dictionary,
                )))
            } else {
                Err(CFError::wrap_under_create_rule(error))
            }
        }
    }

    /// Returns the SHA-256 fingerprint of the certificate.
    fn fingerprint(&self) -> Result<[u8; 32], CFError> {
        let data = CFData::from_buffer(&self.to_der());
        let hash = Builder::new()
            .type_(DigestType::sha2())
            .length(256)
            .execute(&data)?;
        Ok(hash.bytes().try_into().unwrap())
    }
}

/// Properties associated with a certificate.
pub struct CertificateProperties(CFDictionary);

impl CertificateProperties {
    /// Retrieves a specific property identified by its OID.
    #[must_use] pub fn get(&self, oid: CertificateOid) -> Option<CertificateProperty> {
        unsafe {
            self.0.find(oid.as_ptr().cast::<c_void>()).map(|value| {
                CertificateProperty(CFDictionary::wrap_under_get_rule(*value as *mut _))
            })
        }
    }
}

/// A property associated with a certificate.
pub struct CertificateProperty(CFDictionary);

impl CertificateProperty {
    /// Returns the label of this property.
    #[must_use]
    pub fn label(&self) -> CFString {
        unsafe {
            CFString::wrap_under_get_rule((*self.0.get(kSecPropertyKeyLabel.to_void())).cast())
        }
    }

    /// Returns an enum of the underlying data for this property.
    #[must_use]
    pub fn get(&self) -> PropertyType {
        unsafe {
            let type_ =
                CFString::wrap_under_get_rule(*self.0.get(kSecPropertyKeyType.to_void()) as *mut _);
            let value = self.0.get(kSecPropertyKeyValue.to_void());

            if type_ == CFString::wrap_under_get_rule(kSecPropertyTypeSection) {
                PropertyType::Section(PropertySection(CFArray::wrap_under_get_rule(
                    (*value).cast(),
                )))
            } else if type_ == CFString::wrap_under_get_rule(kSecPropertyTypeString) {
                PropertyType::String(CFString::wrap_under_get_rule((*value).cast()))
            } else {
                PropertyType::__Unknown
            }
        }
    }
}

/// A "section" property.
///
/// Sections are sequences of other properties.
pub struct PropertySection(CFArray<CFDictionary>);

impl PropertySection {
    /// Returns an iterator over the properties in this section.
    #[inline(always)]
    #[must_use]
    pub fn iter(&self) -> PropertySectionIter<'_> {
        PropertySectionIter(self.0.iter())
    }
}

impl<'a> IntoIterator for &'a PropertySection {
    type IntoIter = PropertySectionIter<'a>;
    type Item = CertificateProperty;

    #[inline(always)]
    fn into_iter(self) -> PropertySectionIter<'a> {
        self.iter()
    }
}

/// An iterator over the properties in a section.
pub struct PropertySectionIter<'a>(CFArrayIterator<'a, CFDictionary>);

impl<'a> Iterator for PropertySectionIter<'a> {
    type Item = CertificateProperty;

    #[inline]
    fn next(&mut self) -> Option<CertificateProperty> {
        self.0.next().map(|t| CertificateProperty(t.clone()))
    }

    #[inline(always)]
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.0.size_hint()
    }
}

/// An enum of the various types of properties.
pub enum PropertyType {
    /// A section.
    Section(PropertySection),
    /// A string.
    String(CFString),
    #[doc(hidden)]
    __Unknown,
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::os::macos::certificate_oids::CertificateOid;
    use crate::test::certificate;
    use std::collections::HashMap;

    #[test]
    fn common_name() {
        let certificate = certificate();
        assert_eq!("foobar.com", p!(certificate.common_name()));
    }

    #[test]
    #[allow(deprecated)]
    fn public_key() {
        let certificate = certificate();
        p!(certificate.public_key());
    }

    #[test]
    fn fingerprint() {
        let certificate = certificate();
        let fingerprint = p!(certificate.fingerprint());
        assert_eq!(
            "af9dd180a326ae08b37e6398f9262f8b9d4c55674a233a7c84975024f873655d",
            hex::encode(fingerprint)
        );
    }

    #[test]
    fn signature_algorithm() {
        let certificate = certificate();
        let properties = certificate
            .properties(Some(&[CertificateOid::x509_v1_signature_algorithm()]))
            .unwrap();
        let value = properties
            .get(CertificateOid::x509_v1_signature_algorithm())
            .unwrap();
        let section = match value.get() {
            PropertyType::Section(section) => section,
            _ => panic!(),
        };
        let properties = section
            .iter()
            .map(|p| (p.label().to_string(), p.get()))
            .collect::<HashMap<_, _>>();
        let algorithm = match properties["Algorithm"] {
            PropertyType::String(ref s) => s.to_string(),
            _ => panic!(),
        };
        assert_eq!(algorithm, "1.2.840.113549.1.1.5");
    }
}
