//! Types related to token and key selectors.

use crate::Token;
use crate::objects::Backend;

/// Returns true if a value matches a restriction.
///
/// The value is compared to the restriction as is.  If that doesn't
/// match, the value is hex encoded and compared to the restriction.
fn eq_raw_or_hex(field: &str, value: Option<&[u8]>, restriction: Option<&str>) -> bool {
    tracer!(false, "cmp_raw_and_hex");

    t!("Checking {}: have restriction: {:?}, value: {:02x?} ({:?})",
       field, restriction, value, value.map(|v| String::from_utf8_lossy(v)));

    let matches = match (value, restriction) {
        (Some(value), Some(restriction)) => {
            if restriction.as_bytes() == value {
                t!("Raw value matches restriction.");
                true
            } else if value.len() * 2 == restriction.len()
                && restriction.chars()
                     .all(|c| c.is_ascii_hexdigit())
            {
                // Try doing a hex comparison.
                t!("Restriction appears to be hex, trying hex comparison.");

                // XXX: Avoid allocating memory.

                // The (current) implementation of the Rust standard
                // library's eq_ignore_ascii_case lowercases the
                // characters.  Avoid the conversion by using
                // lower-case hex.
                let value_hex = value.iter()
                    .map(|x| format!("{:02x}", x))
                    .collect::<Vec<String>>()
                    .join("");

                restriction.eq_ignore_ascii_case(&value_hex)
            } else {
                false
            }
        }
        (None, Some(_)) => {
            // The object doesn't have a value, but we have a
            // restriction: not a match.
            t!("Restriction, but no value => no match");
            false
        }
        (_, None) => {
            // We don't have a restriction: always a match.
            t!("No restriction => match");
            true
        }
    };

    matches
}

/// Selects tokens with the specified identifier.
#[derive(Debug, Clone)]
pub struct TokenSelector {
    /// Selects the named backend.
    ///
    /// Backends are usually specified by their full path, e.g.,
    /// `/usr/lib/x86_64-linux-gnu/libykcs11.so.2`.  This matches on
    /// the full path, on the filename, the filename with each
    /// trailing component stripped, i.e., `libykcs11.so.2`,
    /// `libykcs11.so`, and `libykcs11`, and the filename with any
    /// initial `lib` prefix and all trailing components striped,
    /// i.e., `ykcs11`.
    pub backend: Option<String>,

    /// Selects the token in the specified slot.
    pub slot: Option<u64>,

    /// Selects tokens with the specified label.
    ///
    /// The user normally sets the token's label when it is
    /// initialized.
    ///
    /// The value of the restriction is compared to both the token's
    /// raw label and the label encoded in hex.  This allows matching
    /// labels with non-printable characters.
    pub token_label: Option<String>,

    /// Selects tokens with the specified serial number.
    ///
    /// The serial number is set when the token is initialized.
    ///
    /// The value of the restriction is compared to both the token's
    /// raw serial number and the serial number encoded in hex.  This
    /// allows matching serial numbers with non-printable characters.
    pub token_serial_number: Option<String>,
}

impl TokenSelector {
    /// Returns an unconstrained `TokenSelector`.
    ///
    /// This matches all tokens.
    pub fn unconstrained() -> Self {
        Self {
            backend: None,
            slot: None,
            token_label: None,
            token_serial_number: None,
        }
    }

    /// Returns the argument and value as a string, e.g., `--slot 1`.
    pub fn args(&self) -> String {
        let mut parts = Vec::with_capacity(2);

        if let Some(value) = self.backend.as_ref() {
            parts.push("--backend");
            parts.push(value);
        }
        let slot_;
        if let Some(value) = self.slot {
            parts.push("--slot");
            slot_ = format!("{}", value);
            parts.push(&slot_[..]);
        }
        if let Some(value) = self.token_label.as_ref() {
            parts.push("--token-label");
            parts.push(value);
        }
        if let Some(value) = self.token_serial_number.as_ref() {
            parts.push("--token-serial-number");
            parts.push(value);
        }

        if parts.is_empty() {
            "<empty selector>".to_string()
        } else {
            parts.join(" ")
        }
    }

    /// Returns `true` if the selector has any restrictions on the
    /// backend.
    ///
    /// A selector without restrictions matches everything.
    pub fn has_backend_restrictions(&self) -> bool {
        self.backend.is_some()
    }

    /// Returns `true` if the selector has any restrictions on the
    /// token.
    ///
    /// A selector without restrictions matches everything.
    pub fn has_token_restrictions(&self) -> bool {
        self.slot.is_some()
            || self.token_label.is_some()
            || self.token_serial_number.is_some()
    }

    /// Returns `true` if the selector has any restrictions.
    ///
    /// A selector without restrictions matches everything.
    pub fn has_restrictions(&self) -> bool {
        self.has_backend_restrictions()
            || self.has_token_restrictions()
    }

    /// Returns the backend restriction, if any.
    pub fn backend(&self) -> Option<&str> {
        self.backend.as_deref()
    }

    /// Returns `true` if the selector matches the `backend`.
    ///
    /// If the selector does not restrict the backend, then this
    /// returns `true`.
    pub fn backend_matches(&self, backend: &Backend) -> bool {
        if let Some(backend_restriction) = self.backend() {
            backend.is_lib(backend_restriction)
        } else {
            // No selector = no restriction.
            true
        }
    }

    /// Returns the slot restriction, if any.
    pub fn slot(&self) -> Option<u64> {
        self.slot
    }

    /// Returns `true` if the selector matches the specified slot.
    ///
    /// If the selector does not restrict the slot, then this returns
    /// `true`.
    pub fn slot_matches(&self, slot: u64) -> bool {
        if let Some(slot_restriction) = self.slot() {
            slot_restriction == slot
        } else {
            // Label not specified.  No restriction.
            true
        }
    }

    /// Returns the token label restriction, if any.
    pub fn token_label(&self) -> Option<&str> {
        self.token_label.as_deref()
    }

    /// Returns `true` if the selector matches the specified token's
    /// label.
    ///
    /// If the selector does not restrict the token's label, then this
    /// returns `true`.
    ///
    /// The value of the restriction is compared to both the object's
    /// raw label and the label encoded in hex.  This allows matching
    /// labels with non-printable characters.
    pub fn token_label_matches(&self, label: &str) -> bool {
        eq_raw_or_hex("token label",
                      Some(label.as_bytes()),
                      self.token_label())
    }

    /// Returns the token's serial number restriction, if any.
    pub fn token_serial_number(&self) -> Option<&str> {
        self.token_serial_number.as_deref()
    }

    /// Returns `true` if the selector matches the specified token's
    /// serial number.
    ///
    /// If the selector does not restrict the token's serial number,
    /// then this returns `true`.
    ///
    /// The value of the restriction is compared to both the token's
    /// raw serial number and the serial number encoded in hex.  This
    /// allows matching on serial numbers with non-printable
    /// characters.
    pub fn token_serial_number_matches(&self, serial_number: &str) -> bool {
        eq_raw_or_hex("token serial number",
                      Some(serial_number.as_bytes()),
                      self.token_serial_number())
    }
}

impl From<&Token> for TokenSelector {
    fn from(token: &Token) -> TokenSelector {
        TokenSelector {
            backend: Some(token.backend().lib_str.clone()),
            slot: None,
            token_label: Some(token.info().label().to_string()),
            token_serial_number: Some(token.info().serial_number().to_string()),
        }
    }
}

/// Selects keys with the specified identifier.
#[derive(Debug, Clone)]
pub struct KeySelector {
    /// Selects a token.
    pub token: TokenSelector,

    /// Selects keys with the specified unique ID.
    ///
    /// An object's ID corresponds to its unique ID attribute, which
    /// was introduced in PKCS#11 3.0 in 2020.
    ///
    /// The value of the restriction is compared to both the key's raw
    /// unique ID and the unique ID encoded in hex.  This allows
    /// matching unique IDs with non-printable characters.
    ///
    /// If a key does not have a unique ID, then it doesn't match a
    /// restriction on the key's unique ID.
    pub key_unique_id: Option<String>,

    /// Selects keys with the specified label.
    ///
    /// An key's label correspond to its label attribute, which is
    /// normally set by the user when the key is created.
    ///
    /// The value of the restriction is compared to both the key's raw
    /// label and the label encoded in hex.  This allows matching
    /// labels with non-printable characters.
    ///
    /// If a key does not have a label, then it doesn't match a
    /// restriction on the label.
    pub key_label: Option<String>,

    /// Selects keys with the specified ID.
    ///
    /// An object's ID is usually set by the application when the key
    /// is created.
    ///
    /// The value of the restriction is compared to both the key's raw
    /// ID and the ID encoded in hex.  This allows matching IDs with
    /// non-printable characters.
    ///
    /// If a key does not have an ID, then it doesn't match a
    /// restriction on the key's ID.
    pub key_id: Option<String>,

    /// Selects keys with the specified URL.
    ///
    /// An object's URL is usually set by the application when the key
    /// is created.
    ///
    /// The value of the restriction is compared to both the key's raw
    /// URL and the URL encoded in hex.  This allows matching URLs
    /// with non-printable characters.
    ///
    /// If a key does not have a URL, then it doesn't match a
    /// restriction on the key's URL.
    pub key_url: Option<String>,
}

impl From<&Token> for KeySelector {
    fn from(token: &Token) -> KeySelector {
        KeySelector {
            token: token.into(),
            key_unique_id: None,
            key_label: None,
            key_id: None,
            key_url: None,
        }
    }
}

impl KeySelector {
    /// Returns an unconstrained `KeySelector`.
    ///
    /// This matches all keys on all tokens.
    pub fn unconstrained() -> Self {
        Self {
            token: TokenSelector::unconstrained(),
            key_unique_id: None,
            key_label: None,
            key_id: None,
            key_url: None,
        }
    }

    /// Creates a `KeySelector` from a `TokenSelector`.
    pub fn from_token_selector(selector: TokenSelector) -> Self {
        Self {
            token: selector,
            key_unique_id: None,
            key_label: None,
            key_id: None,
            key_url: None,
        }
    }

    /// Creates a `KeySelector` for a specific label.
    pub fn from_key_label(key_label: impl Into<String>) -> Self {
        Self {
            token: TokenSelector {
                backend: None,
                slot: None,
                token_serial_number: None,
                token_label: None,
            },
            key_unique_id: None,
            key_label: Some(key_label.into()),
            key_id: None,
            key_url: None,
        }
    }

    /// Creates a `KeySelector` for a specific key ID.
    pub fn from_key_id(key_id: impl Into<String>) -> Self {
        Self {
            token: TokenSelector {
                backend: None,
                slot: None,
                token_serial_number: None,
                token_label: None,
            },
            key_unique_id: None,
            key_label: None,
            key_id: Some(key_id.into()),
            key_url: None,
        }
    }

    /// Returns `true` if the selector has any restrictions on the
    /// backend.
    ///
    /// A selector without restrictions matches all backends.
    pub fn has_backend_restrictions(&self) -> bool {
        self.token.has_backend_restrictions()
    }

    /// Returns `true` if the selector has any restrictions on the
    /// token.
    ///
    /// A selector without restrictions matches all tokens.
    pub fn has_token_restrictions(&self) -> bool {
        self.token.has_token_restrictions()
    }

    /// Returns `true` if the selector has any restrictions on the
    /// key.
    ///
    /// A selector without restrictions matches all keys.
    pub fn has_key_restrictions(&self) -> bool {
        self.key_unique_id.is_some()
            || self.key_label.is_some()
            || self.key_id.is_some()
            || self.key_url.is_some()
    }

    /// Returns `true` if the selector has any restrictions.
    ///
    /// A selector without restrictions matches everything.
    pub fn has_restrictions(&self) -> bool {
        self.has_backend_restrictions()
            || self.has_token_restrictions()
            || self.has_key_restrictions()
    }

    /// Returns the backend restriction, if any.
    pub fn backend(&self) -> Option<&str> {
        self.token.backend()
    }

    /// Returns `true` if the selector matches the `backend`.
    ///
    /// If the selector does not restrict the backend, this returns
    /// `true`.
    pub fn backend_matches(&self, backend: &Backend) -> bool {
        self.token.backend_matches(backend)
    }

    /// Returns the slot restriction, if any.
    pub fn slot(&self) -> Option<u64> {
        self.token.slot()
    }

    /// Returns `true` if the selector matches the specified slot.
    ///
    /// If the selector does not restrict the slot, this returns
    /// `true`.
    pub fn slot_matches(&self, slot: u64) -> bool {
        self.token.slot_matches(slot)
    }

    /// Returns the token label restriction, if any.
    pub fn token_label(&self) -> Option<&str> {
        self.token.token_label()
    }

    /// Returns `true` if the selector matches the specified token's
    /// label.
    ///
    /// If the selector does not restrict the token's label, this
    /// returns `true`.
    pub fn token_label_matches(&self, label: &str) -> bool {
        self.token.token_label_matches(label)
    }

    /// Returns the token serial number restriction, if any.
    pub fn token_serial_number(&self) -> Option<&str> {
        self.token.token_serial_number()
    }

    /// Returns `true` if the selector matches the specified token's
    /// serial number.
    ///
    /// If the selector does not restrict the token's serial number,
    /// this returns `true`.
    pub fn token_serial_number_matches(&self, serial_number: &str) -> bool {
        self.token.token_serial_number_matches(serial_number)
    }

    /// Returns the key unique ID restriction, if any.
    pub fn key_unique_id(&self) -> Option<&str> {
        self.key_unique_id.as_deref()
    }

    /// Returns `true` if the selector matches the specified key's
    /// unique ID.
    ///
    /// If the selector does not restrict the key's unique ID, this
    /// returns `true`.
    ///
    /// The value is compared to the restriction as is.  If that
    /// doesn't match, the value is hex encoded and compared to the
    /// restriction.  This allows matching on unique IDs with
    /// non-printable characters.
    pub fn key_unique_id_matches(&self, key_unique_id: Option<&[u8]>) -> bool {
        eq_raw_or_hex("key unique ID",
                      key_unique_id.as_deref(), self.key_unique_id())
    }

    /// Returns the key label restriction, if any.
    pub fn key_label(&self) -> Option<&str> {
        self.key_label.as_deref()
    }

    /// Returns `true` if the selector matches the specified key's
    /// label.
    ///
    /// If the selector does not restrict the key's label, then this
    /// returns `true`.
    ///
    /// The value is compared to the restriction as is.  If that
    /// doesn't match, the value is hex encoded and compared to the
    /// restriction.  This allows matching on labels with
    /// non-printable characters.
    pub fn key_label_matches(&self, key_label: Option<&[u8]>) -> bool {
        eq_raw_or_hex("key label", key_label.as_deref(), self.key_label())
    }

    /// Returns the key ID restriction, if any.
    pub fn key_id(&self) -> Option<&str> {
        self.key_id.as_deref()
    }

    /// Returns `true` if the selector matches the specified key ID.
    ///
    /// If the selector does not restrict the key ID, then this
    /// returns `true`.
    ///
    /// The value is compared to the restriction as is.  If that
    /// doesn't match, the value is hex encoded and compared to the
    /// restriction.  This allows matching on key IDs with
    /// non-printable characters.
    pub fn key_id_matches(&self, key_id: Option<&[u8]>) -> bool {
        eq_raw_or_hex("key ID", key_id.as_deref(), self.key_id())
    }

    /// Returns the key URL restriction, if any.
    pub fn key_url(&self) -> Option<&str> {
        self.key_url.as_deref()
    }

    /// Returns `true` if the selector matches the specified key URL.
    ///
    /// If the selector does not restrict the key URL, then this
    /// returns `true`.
    ///
    /// The value is compared to the restriction as is.  If that
    /// doesn't match, the value is hex encoded and compared to the
    /// restriction.  This allows matching on URLs with non-printable
    /// characters.
    pub fn key_url_matches(&self, key_url: Option<&[u8]>) -> bool {
        eq_raw_or_hex("key URL", key_url.as_deref(), self.key_url())
    }

    /// Returns the argument and value as a string, e.g., "--slot 1".
    pub fn args(&self) -> String {
        let mut parts = Vec::with_capacity(2);

        if let Some(value) = self.token.backend.as_ref() {
            parts.push("--backend");
            parts.push(value);
        }
        let slot_;
        if let Some(value) = self.token.slot {
            parts.push("--slot");
            slot_ = format!("{}", value);
            parts.push(&slot_[..]);
        }
        if let Some(value) = self.token.token_serial_number.as_ref() {
            parts.push("--token-serial-number");
            parts.push(value);
        }
        if let Some(value) = self.token.token_label.as_ref() {
            parts.push("--token-label");
            parts.push(value);
        }
        if let Some(value) = self.key_unique_id.as_ref() {
            parts.push("--key-unique-id");
            parts.push(value);
        }
        if let Some(value) = self.key_label.as_ref() {
            parts.push("--key-label");
            parts.push(value);
        }
        if let Some(value) = self.key_id.as_ref() {
            parts.push("--key-id");
            parts.push(value);
        }
        if let Some(value) = self.key_url.as_ref() {
            parts.push("--key-url");
            parts.push(value);
        }

        if parts.is_empty() {
            "<empty selector>".to_string()
        } else {
            parts.join(" ")
        }
    }
}
