//! Functions and structures for defining and activating function hooks

use crate::backend::Backend;
use crate::demangling;
use crate::error::*;
use crate::hooks;
use crate::project::Project;
use crate::return_value::*;
use crate::state::State;
use either::Either;
use llvm_ir::function::{CallingConvention, FunctionAttribute, ParameterAttribute};
use llvm_ir::types::Typed;
use llvm_ir::{instruction::InlineAssembly, Name, Operand, Type};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::rc::Rc;

/// A set of function hooks, which will be executed instead of their respective
/// hooked functions if/when the symbolic execution engine encounters a call to
/// one of those hooked functions.
///
/// You can hook internal functions (which are defined in some available LLVM
/// `Module`), external functions (e.g., calls to external libraries), LLVM
/// intrinsics, or any other kind of function.
///
/// The function resolution process is as follows:
///
/// (1) If the function is hooked, then the hook will be used instead of any
/// other option. That is, the hook has the highest precedence.
///
/// (2) Haybale provides default hooks for certain LLVM intrinsics like
/// `memcpy`, which have specially reserved names; it will apply these hooks
/// unless a different hook was defined for the intrinsic in (1).
///
/// (3) Else, if the function is not hooked but is defined in an available
/// LLVM `Module`, the function will be symbolically executed (called).
///
/// (4) Else, if a default function hook was supplied with `add_default_hook()`,
/// that hook will be used.
///
/// (5) If none of the above options apply, an error will be raised.
/// Note that this means that calls to external functions will always
/// error unless a hook for them is provided, either by name or via the default
/// hook.
#[derive(Clone)]
pub struct FunctionHooks<'p, B: Backend + 'p> {
    /// `hooks`, `cpp_demangled_hooks`, and `rust_demangled_hooks` are each maps
    /// from function names to the hook to use. In `hooks`, the function names
    /// are exactly as they appear in the LLVM IR. In `cpp_demangled_hooks` and
    /// `rust_demangled_hooks`, the function names are demangled versions (using
    /// the C++ and Rust demanglers respectively) of the names that appear in the
    /// LLVM IR.
    ///
    /// It's intended that a function should only be hooked in one of these maps;
    /// but if both the mangled and demangled function names are hooked, the hook
    /// in `hooks` (that is, for the mangled name) takes priority.
    ///
    /// If a function name isn't in any of these maps, the function isn't hooked.
    hooks: HashMap<String, FunctionHook<'p, B>>,
    cpp_demangled_hooks: HashMap<String, FunctionHook<'p, B>>,
    rust_demangled_hooks: HashMap<String, FunctionHook<'p, B>>,

    /// Hook (if any) to use for calls to inline assembly.
    /// This one hook will handle all calls to any inline assembly, regardless of
    /// the contents; it is responsible for inspecting the contents and acting
    /// appropriately.
    ///
    /// Note that as of this writing, due to a limitation of the LLVM C API (see
    /// the 'Limitations' section of the
    /// [`llvm-ir` README](https://github.com/cdisselkoen/llvm-ir/blob/master/README.md)),
    /// the hook will actually have no way of obtaining the contents of the asm
    /// string itself, although it can still inspect function parameters etc.
    /// For now, this is the best we can do.
    ///
    /// If no hook is provided here, then all calls to inline assembly will
    /// result in errors.
    inline_asm_hook: Option<FunctionHook<'p, B>>,

    /// Hook (if any) to use for functions which are neither defined in the LLVM
    /// IR nor specifically hooked by name.
    ///
    /// If no hook is provided here, then calls to functions which are neither
    /// defined nor hooked will result in `Error::FunctionNotFound` errors.
    default_hook: Option<FunctionHook<'p, B>>,

    /// For internal use in creating unique `id`s for `FunctionHook`s
    cur_id: usize,
}

/// An `Argument` represents a single argument to a called function, together
/// with zero or more attributes which apply to it
pub type Argument = (Operand, Vec<ParameterAttribute>);

/// `IsCall` exists to unify the commonalities between LLVM `Call` and `Invoke`
/// instructions
pub trait IsCall: Typed {
    fn get_called_func(&self) -> &Either<InlineAssembly, Operand>;
    fn get_arguments(&self) -> &Vec<Argument>;
    fn get_return_attrs(&self) -> &Vec<ParameterAttribute>;
    fn get_fn_attrs(&self) -> &Vec<FunctionAttribute>;
    fn get_calling_convention(&self) -> CallingConvention;
}

impl IsCall for llvm_ir::instruction::Call {
    fn get_called_func(&self) -> &Either<InlineAssembly, Operand> {
        &self.function
    }
    fn get_arguments(&self) -> &Vec<Argument> {
        &self.arguments
    }
    fn get_return_attrs(&self) -> &Vec<ParameterAttribute> {
        &self.return_attributes
    }
    fn get_fn_attrs(&self) -> &Vec<FunctionAttribute> {
        &self.function_attributes
    }
    fn get_calling_convention(&self) -> CallingConvention {
        self.calling_convention
    }
}

impl IsCall for llvm_ir::terminator::Invoke {
    fn get_called_func(&self) -> &Either<InlineAssembly, Operand> {
        &self.function
    }
    fn get_arguments(&self) -> &Vec<Argument> {
        &self.arguments
    }
    fn get_return_attrs(&self) -> &Vec<ParameterAttribute> {
        &self.return_attributes
    }
    fn get_fn_attrs(&self) -> &Vec<FunctionAttribute> {
        &self.function_attributes
    }
    fn get_calling_convention(&self) -> CallingConvention {
        self.calling_convention
    }
}

impl<'p, B: Backend + 'p> FunctionHooks<'p, B> {
    /// Create a blank `FunctionHooks` instance with no function hooks.
    ///
    /// You may want to consider
    /// [`FunctionHooks::default()`](struct.FunctionHooks.html#method.default),
    /// which provides predefined hooks for common functions.
    pub fn new() -> Self {
        Self {
            hooks: HashMap::new(),
            cpp_demangled_hooks: HashMap::new(),
            rust_demangled_hooks: HashMap::new(),
            inline_asm_hook: None,
            default_hook: None,
            cur_id: 0,
        }
    }

    /// Adds a function hook. The `hook` will be executed instead of the body of
    /// the `hooked_function`.
    pub fn add<H>(&mut self, hooked_function: impl Into<String>, hook: &'p H)
    where
        H: Fn(&'p Project, &mut State<'p, B>, &'p dyn IsCall) -> Result<ReturnValue<B::BV>>,
    {
        self.hooks
            .insert(hooked_function.into(), FunctionHook::new(self.cur_id, hook));
        self.cur_id += 1;
    }

    /// Exactly like `add()`, but takes the (C++) _demangled_ name of the function
    /// to hook, so you can use a function name like "namespace::function".
    pub fn add_cpp_demangled<H>(&mut self, hooked_function: impl Into<String>, hook: &'p H)
    where
        H: Fn(&'p Project, &mut State<'p, B>, &'p dyn IsCall) -> Result<ReturnValue<B::BV>>,
    {
        self.cpp_demangled_hooks
            .insert(hooked_function.into(), FunctionHook::new(self.cur_id, hook));
        self.cur_id += 1;
    }

    /// Exactly like `add()`, but takes the (Rust) _demangled_ name of the function
    /// to hook, so you can use a function name like "module::function".
    pub fn add_rust_demangled<H>(&mut self, hooked_function: impl Into<String>, hook: &'p H)
    where
        H: Fn(&'p Project, &mut State<'p, B>, &'p dyn IsCall) -> Result<ReturnValue<B::BV>>,
    {
        self.rust_demangled_hooks
            .insert(hooked_function.into(), FunctionHook::new(self.cur_id, hook));
        self.cur_id += 1;
    }

    /// Add a hook to be used for calls to inline assembly.
    /// This one hook will handle all calls to any inline assembly, regardless of
    /// the contents; it is responsible for inspecting the contents and acting
    /// appropriately.
    /// If another inline assembly hook is added, it will replace any inline
    /// assembly hook which was previously present.
    ///
    /// Returns `true` if an inline assembly hook was previously present, or
    /// `false` if no inline assembly hook was present.
    ///
    /// Note that as of this writing, due to a limitation of the LLVM C API (see
    /// the 'Limitations' section of the
    /// [`llvm-ir` README](https://github.com/cdisselkoen/llvm-ir/blob/master/README.md)),
    /// the hook will actually have no way of obtaining the contents of the asm
    /// string itself, although it can still inspect function parameters etc.
    /// For now, this is the best we can do.
    pub fn add_inline_asm_hook<H>(&mut self, hook: &'p H) -> bool
    where
        H: Fn(&'p Project, &mut State<'p, B>, &'p dyn IsCall) -> Result<ReturnValue<B::BV>>,
    {
        match &mut self.inline_asm_hook {
            h @ Some(_) => {
                *h = Some(FunctionHook::new(self.cur_id, hook));
                self.cur_id += 1;
                true
            },
            h @ None => {
                *h = Some(FunctionHook::new(self.cur_id, hook));
                self.cur_id += 1;
                false
            },
        }
    }

    /// Add a hook to be used if no other definition or hook is found for the
    /// call.
    /// If another default hook is added, it will replace any default hook which
    /// was previously present.
    ///
    /// Returns `true` if a default hook was previously present, or `false` if no
    /// default hook was present.
    pub fn add_default_hook<H>(&mut self, hook: &'p H) -> bool
    where
        H: Fn(&'p Project, &mut State<'p, B>, &'p dyn IsCall) -> Result<ReturnValue<B::BV>>,
    {
        match &mut self.default_hook {
            h @ Some(_) => {
                *h = Some(FunctionHook::new(self.cur_id, hook));
                self.cur_id += 1;
                true
            },
            h @ None => {
                *h = Some(FunctionHook::new(self.cur_id, hook));
                self.cur_id += 1;
                false
            },
        }
    }

    /// Removes the function hook for the given function, which was added with
    /// `add()`. That function will no longer be hooked.
    pub fn remove(&mut self, hooked_function: &str) {
        self.hooks.remove(hooked_function);
    }

    /// Removes the function hook for the given function, which was added with
    /// [`add_cpp_demangled()`](struct.FunctionHooks.html#method.add_cpp_demangled).
    /// That function will no longer be hooked.
    pub fn remove_cpp_demangled(&mut self, hooked_function: &str) {
        self.cpp_demangled_hooks.remove(hooked_function);
    }

    /// Removes the function hook for the given function, which was added with
    /// [`add_rust_demangled()`](struct.FunctionHooks.html#method.add_rust_demangled).
    /// That function will no longer be hooked.
    pub fn remove_rust_demangled(&mut self, hooked_function: &str) {
        self.rust_demangled_hooks.remove(hooked_function);
    }

    /// Removes the function hook used for calls to inline assembly, which was
    /// added with [`add_inline_asm_hook()`]. Calls to inline assembly will no
    /// longer be hooked, and thus will result in errors, until the next call to
    /// [`add_inline_asm_hook()`].
    ///
    /// [`add_inline_asm_hook()`]: struct.FunctionHooks.html#method.add_inline_asm_hook
    pub fn remove_inline_asm_hook(&mut self) {
        self.inline_asm_hook = None;
    }

    /// Removes the default function hook which was added with
    /// [`add_default_hook()`]. Calls to functions which are neither defined in
    /// the `Project` nor specifically hooked will thus result in
    /// `Error::FunctionNotFound` errors, until the next call to
    /// [`add_default_hook()`].
    ///
    /// [`add_default_hook()`]: struct.FunctionHooks.html#method.add_default_hook
    pub fn remove_default_hook(&mut self) {
        self.default_hook = None;
    }

    /// Iterate over all function hooks, as (function name, hook) pairs.
    /// Function names may include both mangled and demangled names.
    pub(crate) fn get_all_hooks(&self) -> impl Iterator<Item = (&String, &FunctionHook<'p, B>)> {
        self.hooks
            .iter()
            .chain(self.cpp_demangled_hooks.iter())
            .chain(self.rust_demangled_hooks.iter())
    }

    /// Get the `FunctionHook` active for the given `funcname`, or `None` if
    /// there is no hook active for the function. `funcname` may be either a
    /// mangled or a demangled function name.
    pub(crate) fn get_hook_for(&self, funcname: &str) -> Option<&FunctionHook<'p, B>> {
        self.hooks
            .get(funcname)
            .or_else(|| {
                demangling::try_rust_demangle(funcname)
                    .and_then(|demangled| self.rust_demangled_hooks.get(&demangled))
            })
            .or_else(|| {
                demangling::try_cpp_demangle(funcname)
                    .and_then(|demangled| self.cpp_demangled_hooks.get(&demangled))
            })
    }

    /// Get the `FunctionHook` used for calls to inline assembly, if there is one.
    ///
    /// See docs on `add_inline_asm_hook()` above
    pub(crate) fn get_inline_asm_hook(&self) -> Option<&FunctionHook<'p, B>> {
        self.inline_asm_hook.as_ref()
    }

    /// Get the default `FunctionHook` (used when no LLVM definition or hook is
    /// found), if there is one.
    ///
    /// See docs on `add_default_hook()` above
    pub(crate) fn get_default_hook(&self) -> Option<&FunctionHook<'p, B>> {
        self.default_hook.as_ref()
    }

    /// Determine whether there is an active hook for the given `funcname`
    pub fn is_hooked(&self, funcname: &str) -> bool {
        self.get_hook_for(funcname).is_some()
    }

    /// Is there currently an inline asm hook active?
    /// (See `add_inline_asm_hook()` for more info)
    pub fn has_inline_asm_hook(&self) -> bool {
        self.inline_asm_hook.is_some()
    }

    /// Is there currently a default hook active?
    /// (See `add_default_hook()` for more info)
    pub fn has_default_hook(&self) -> bool {
        self.default_hook.is_some()
    }
}

impl<'p, B: Backend + 'p> Default for FunctionHooks<'p, B> {
    /// Provides predefined hooks for common functions. (At the time of this
    /// writing, this includes malloc-related functions `malloc()`, `calloc()`,
    /// `realloc()`, and `free()`, as well as some C++ exception-handling
    /// functions such as `__cxa_throw()` and `__cxa_allocate_exception()`,
    /// and a few other C and Rust standard library functions.)
    ///
    /// If you don't want these hooks, you can use
    /// [`FunctionHooks::remove_function_hook()`](struct.FunctionHooks.html#method.remove_function_hook)
    /// to remove individual hooks, or you can use
    /// [`FunctionHooks::new()`](struct.FunctionHooks.html#method.new), which
    /// comes with no predefined hooks.
    fn default() -> Self {
        let mut fhooks = Self::new();
        fhooks.add("malloc", &hooks::allocation::malloc_hook);
        fhooks.add("calloc", &hooks::allocation::calloc_hook);
        fhooks.add("realloc", &hooks::allocation::realloc_hook);
        fhooks.add("free", &hooks::allocation::free_hook);
        fhooks.add(
            "__cxa_allocate_exception",
            &hooks::exceptions::cxa_allocate_exception,
        );
        fhooks.add("__cxa_throw", &hooks::exceptions::cxa_throw);
        fhooks.add("__cxa_begin_catch", &hooks::exceptions::cxa_begin_catch);
        fhooks.add("__cxa_end_catch", &hooks::exceptions::cxa_end_catch);
        fhooks.add("llvm.eh.typeid.for", &hooks::exceptions::llvm_eh_typeid_for);
        fhooks.add("exit", &abort_hook);
        fhooks.add_rust_demangled("std::panicking::begin_panic", &abort_hook);
        fhooks.add_rust_demangled("std::panicking::begin_panic_fmt", &abort_hook);
        fhooks.add_rust_demangled("std::panicking::begin_panic_handler", &abort_hook);
        fhooks.add_rust_demangled("core::panicking::panic", &abort_hook);
        fhooks.add_rust_demangled("core::panicking::panic_bounds_check", &abort_hook);
        fhooks.add_rust_demangled("core::result::unwrap_failed", &abort_hook);
        fhooks.add_rust_demangled("core::slice::slice_index_len_fail", &abort_hook);
        fhooks.add_rust_demangled("core::slice::slice_index_order_fail", &abort_hook);
        fhooks.add_rust_demangled("core::slice::slice_index_overflow_fail", &abort_hook);
        fhooks
    }
}

/// Function hooks are given mutable access to the `State`, and read-only access
/// to the `Call` they are hooking (which includes arguments etc).
///
/// They should return the [`ReturnValue`](enum.ReturnValue.html) representing
/// the return value of the call, or an appropriate [`Error`](enum.Error.html) if
/// they cannot.
pub(crate) struct FunctionHook<'p, B: Backend> {
    /// The actual hook to be executed
    #[allow(clippy::type_complexity)]
    hook: Rc<
        dyn Fn(&'p Project, &mut State<'p, B>, &'p dyn IsCall) -> Result<ReturnValue<B::BV>> + 'p,
    >,

    /// A unique id, used for nothing except equality comparisons between `FunctionHook`s.
    /// This `id` should be globally unique across all created `FunctionHook`s.
    id: usize,
}

impl<'p, B: Backend> Clone for FunctionHook<'p, B> {
    fn clone(&self) -> Self {
        Self {
            hook: self.hook.clone(),
            id: self.id,
        }
    }
}

impl<'p, B: Backend> PartialEq for FunctionHook<'p, B> {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl<'p, B: Backend> Eq for FunctionHook<'p, B> {}

impl<'p, B: Backend> Hash for FunctionHook<'p, B> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.id.hash(state);
    }
}

impl<'p, B: Backend> FunctionHook<'p, B> {
    /// `id`: A unique id, used for nothing except equality comparisons between `FunctionHook`s.
    /// This `id` should be globally unique across all created `FunctionHook`s.
    pub fn new(
        id: usize,
        f: &'p dyn Fn(&'p Project, &mut State<'p, B>, &'p dyn IsCall) -> Result<ReturnValue<B::BV>>,
    ) -> Self {
        Self {
            hook: Rc::new(f),
            id,
        }
    }

    pub fn call_hook(
        &self,
        proj: &'p Project,
        state: &mut State<'p, B>,
        call: &'p dyn IsCall,
    ) -> Result<ReturnValue<B::BV>> {
        (self.hook)(proj, state, call)
    }
}

/// This hook ignores the function arguments and returns an unconstrained value
/// of the appropriate size for the function's return value (or void for
/// void-typed functions).
///
/// May be used for functions taking any number and type of arguments, and with
/// any return type.
pub fn generic_stub_hook<B: Backend>(
    _proj: &Project,
    state: &mut State<B>,
    call: &dyn IsCall,
) -> Result<ReturnValue<B::BV>> {
    match state.type_of(call).as_ref() {
        Type::VoidType => Ok(ReturnValue::ReturnVoid),
        ty => {
            let width = state.size_in_bits(ty).ok_or_else(|| {
                Error::OtherError("Call return type is an opaque named struct".into())
            })?;
            assert_ne!(width, 0, "Call return type has size 0 bits but isn't void type"); // void type was handled above
            let bv = state.new_bv_with_name(Name::from("generic_stub_hook_retval"), width)?;
            Ok(ReturnValue::Return(bv))
        },
    }
}

/// This hook ignores the function arguments and returns `ReturnValue::Abort`.
/// It is suitable for hooking functions such as C's `exit()` which abort the
/// program and never return.
pub fn abort_hook<B: Backend>(
    _proj: &Project,
    _state: &mut State<B>,
    _call: &dyn IsCall,
) -> Result<ReturnValue<B::BV>> {
    Ok(ReturnValue::Abort)
}
