// SPDX-License-Identifier: MIT OR Apache-2.0

//! UEFI boot services.
//!
//! These functions will panic if called after exiting boot services.
//!
//! # Accessing protocols
//!
//! Protocols can be opened using several functions in this module. Most
//! commonly, [`open_protocol_exclusive`] should be used. This ensures that
//! nothing else can use the protocol until it is closed, and returns a
//! [`ScopedProtocol`] that takes care of closing the protocol when it is
//! dropped.
//!
//! Other methods for opening protocols:
//!
//! * [`open_protocol`]
//! * [`get_image_file_system`]
//!
//! For protocol definitions, see the [`proto`] module.
//!
//! [`proto`]: crate::proto

pub use uefi_raw::table::boot::{
    EventType, MemoryAttribute, MemoryDescriptor, MemoryType, PAGE_SIZE, Tpl,
};

use crate::data_types::PhysicalAddress;
use crate::mem::memory_map::{MemoryMapBackingMemory, MemoryMapKey, MemoryMapMeta, MemoryMapOwned};
use crate::polyfill::maybe_uninit_slice_assume_init_ref;
#[cfg(doc)]
use crate::proto::device_path::LoadedImageDevicePath;
use crate::proto::device_path::{DevicePath, FfiDevicePath};
use crate::proto::loaded_image::LoadedImage;
use crate::proto::media::fs::SimpleFileSystem;
use crate::proto::{BootPolicy, Protocol, ProtocolPointer};
use crate::runtime::{self, ResetType};
use crate::table::Revision;
use crate::util::opt_nonnull_to_ptr;
use crate::{Char16, Error, Event, Guid, Handle, Result, Status, StatusExt, table};
use core::ffi::c_void;
use core::mem::MaybeUninit;
use core::ops::{Deref, DerefMut};
use core::ptr::{self, NonNull};
use core::sync::atomic::{AtomicPtr, Ordering};
use core::time::Duration;
use core::{mem, slice};
use uefi_raw::table::boot::{AllocateType as RawAllocateType, InterfaceType, TimerDelay};
#[cfg(feature = "alloc")]
use {alloc::vec::Vec, uefi::ResultExt};

/// Global image handle. This is only set by [`set_image_handle`], and it is
/// only read by [`image_handle`].
static IMAGE_HANDLE: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());

/// Get the [`Handle`] of the currently-executing image.
#[must_use]
pub fn image_handle() -> Handle {
    let ptr = IMAGE_HANDLE.load(Ordering::Acquire);
    // Safety: the image handle must be valid. We know it is, because it was set
    // by `set_image_handle`, which has that same safety requirement.
    unsafe { Handle::from_ptr(ptr) }.expect("set_image_handle has not been called")
}

/// Update the global image [`Handle`].
///
/// This is called automatically in the `main` entry point as part of
/// [`uefi::entry`]. It should not be called at any other point in time, unless
/// the executable does not use [`uefi::entry`], in which case it should be
/// called once before calling other boot services functions.
///
/// # Safety
///
/// This function should only be called as described above, and the
/// `image_handle` must be a valid image [`Handle`]. The safety guarantees of
/// [`open_protocol_exclusive`] rely on the global image handle being correct.
pub unsafe fn set_image_handle(image_handle: Handle) {
    IMAGE_HANDLE.store(image_handle.as_ptr(), Ordering::Release);
}

/// Return true if boot services are active, false otherwise.
pub(crate) fn are_boot_services_active() -> bool {
    let Some(st) = table::system_table_raw() else {
        return false;
    };

    // SAFETY: valid per requirements of `set_system_table`.
    let st = unsafe { st.as_ref() };

    !st.boot_services.is_null()
}

fn boot_services_raw_panicking() -> NonNull<uefi_raw::table::boot::BootServices> {
    let st = table::system_table_raw_panicking();
    // SAFETY: valid per requirements of `set_system_table`.
    let st = unsafe { st.as_ref() };
    NonNull::new(st.boot_services).expect("boot services are not active")
}

/// Raises a task's priority level and returns a [`TplGuard`].
///
/// The effect of calling `raise_tpl` with a `Tpl` that is below the current
/// one (which, sadly, cannot be queried) is undefined by the UEFI spec,
/// which also warns against remaining at high `Tpl`s for a long time.
///
/// This function returns an RAII guard that will automatically restore the
/// original `Tpl` when dropped.
///
/// # Safety
///
/// Raising a task's priority level can affect other running tasks and
/// critical processes run by UEFI. The highest priority level is the
/// most dangerous, since it disables interrupts.
#[must_use]
pub unsafe fn raise_tpl(tpl: Tpl) -> TplGuard {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    TplGuard {
        old_tpl: unsafe { (bt.raise_tpl)(tpl) },
    }
}

/// Allocates a consecutive set of memory pages using the UEFI allocator.
///
/// The buffer will be [`PAGE_SIZE`] aligned. Callers are responsible for
/// freeing the memory using [`free_pages`].
///
/// # Arguments
/// - `allocation_type`: The [`AllocateType`] to choose the allocation strategy.
/// - `memory_type`: The [`MemoryType`] used to persist the allocation in the
///   UEFI memory map. Typically, UEFI OS loaders should allocate memory of
///   type [`MemoryType::LOADER_DATA`].
///- `count`: Number of pages to allocate.
///
/// # Safety
///
/// Using this function is safe, but it returns a raw pointer to uninitialized
/// memory. The memory must be initialized before creating a reference to it
/// or reading from it eventually.
///
/// # Errors
///
/// * [`Status::OUT_OF_RESOURCES`]: allocation failed.
/// * [`Status::INVALID_PARAMETER`]: `mem_ty` is [`MemoryType::PERSISTENT_MEMORY`],
///   [`MemoryType::UNACCEPTED`], or in the range <code>[MemoryType::MAX]..=0x6fff_ffff</code>.
/// * [`Status::NOT_FOUND`]: the requested pages could not be found.
pub fn allocate_pages(
    allocation_type: AllocateType,
    memory_type: MemoryType,
    count: usize,
) -> Result<NonNull<u8>> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let (ty, initial_addr) = match allocation_type {
        AllocateType::AnyPages => (RawAllocateType::ANY_PAGES, 0),
        AllocateType::MaxAddress(addr) => (RawAllocateType::MAX_ADDRESS, addr),
        AllocateType::Address(addr) => (RawAllocateType::ADDRESS, addr),
    };

    let mut addr1 = initial_addr;
    unsafe { (bt.allocate_pages)(ty, memory_type, count, &mut addr1) }.to_result()?;

    // The UEFI spec allows `allocate_pages` to return a valid allocation at
    // address zero. Rust does not allow writes through a null pointer (which
    // Rust defines as address zero), so this is not very useful. Only return
    // the allocation if the address is non-null.
    if let Some(ptr) = NonNull::new(addr1 as *mut u8) {
        return Ok(ptr);
    }

    // Attempt a second allocation. The first allocation (at address zero) has
    // not yet been freed, so if this allocation succeeds it should be at a
    // non-zero address.
    let mut addr2 = initial_addr;
    let r = unsafe { (bt.allocate_pages)(ty, memory_type, count, &mut addr2) }.to_result();

    // Free the original allocation (ignoring errors).
    let _unused = unsafe { (bt.free_pages)(addr1, count) };

    // Return an error if the second allocation failed, or if it is still at
    // address zero. Otherwise, return a pointer to the second allocation.
    r?;
    if let Some(ptr) = NonNull::new(addr2 as *mut u8) {
        Ok(ptr)
    } else {
        Err(Status::OUT_OF_RESOURCES.into())
    }
}

/// Frees memory pages allocated by [`allocate_pages`].
///
/// # Safety
///
/// The caller must ensure that no references into the allocation remain,
/// and that the memory at the allocation is not used after it is freed.
///
/// # Errors
///
/// * [`Status::NOT_FOUND`]: `ptr` was not allocated by [`allocate_pages`].
/// * [`Status::INVALID_PARAMETER`]: `ptr` is not page aligned or is otherwise invalid.
pub unsafe fn free_pages(ptr: NonNull<u8>, count: usize) -> Result {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let addr = ptr.as_ptr() as PhysicalAddress;
    unsafe { (bt.free_pages)(addr, count) }.to_result()
}

/// Allocates a consecutive region of bytes using the UEFI allocator.
///
/// The buffer will be 8-byte aligned. Callers are responsible for freeing the
/// memory using [`free_pool`].
///
/// # Arguments
/// - `memory_type`: The [`MemoryType`] used to persist the allocation in the
///   UEFI memory map. Typically, UEFI OS loaders should allocate memory of
///   type [`MemoryType::LOADER_DATA`].
///- `size`: Number of bytes to allocate.
///
/// # Safety
///
/// Using this function is safe, but it returns a raw pointer to uninitialized
/// memory. The memory must be initialized before creating a reference to it
/// or reading from it eventually.
///
/// # Errors
///
/// * [`Status::OUT_OF_RESOURCES`]: allocation failed.
/// * [`Status::INVALID_PARAMETER`]: `mem_ty` is [`MemoryType::PERSISTENT_MEMORY`],
///   [`MemoryType::UNACCEPTED`], or in the range <code>[MemoryType::MAX]..=0x6fff_ffff</code>.
pub fn allocate_pool(memory_type: MemoryType, size: usize) -> Result<NonNull<u8>> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let mut buffer = ptr::null_mut();
    let ptr = unsafe { (bt.allocate_pool)(memory_type, size, &mut buffer) }
        .to_result_with_val(|| buffer)?;

    NonNull::new(ptr).ok_or_else(|| Status::OUT_OF_RESOURCES.into())
}

/// Frees memory allocated by [`allocate_pool`].
///
/// # Safety
///
/// The caller must ensure that no references into the allocation remain,
/// and that the memory at the allocation is not used after it is freed.
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: `ptr` is invalid.
pub unsafe fn free_pool(ptr: NonNull<u8>) -> Result {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    unsafe { (bt.free_pool)(ptr.as_ptr()) }.to_result()
}

/// Queries the `get_memory_map` function of UEFI to retrieve the current
/// size of the map. Returns a [`MemoryMapMeta`].
///
/// It is recommended to add a few more bytes for a subsequent allocation
/// for the memory map, as the memory map itself also needs heap memory,
/// and other allocations might occur before that call.
#[must_use]
pub(crate) fn memory_map_size() -> MemoryMapMeta {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let mut map_size = 0;
    let mut map_key = MemoryMapKey(0);
    let mut desc_size = 0;
    let mut desc_version = 0;

    let status = unsafe {
        (bt.get_memory_map)(
            &mut map_size,
            ptr::null_mut(),
            &mut map_key.0,
            &mut desc_size,
            &mut desc_version,
        )
    };
    assert_eq!(status, Status::BUFFER_TOO_SMALL);

    assert_eq!(
        map_size % desc_size,
        0,
        "Memory map must be a multiple of the reported descriptor size."
    );

    let mmm = MemoryMapMeta {
        desc_size,
        map_size,
        map_key,
        desc_version,
    };

    mmm.assert_sanity_checks();

    mmm
}

/// Stores the current UEFI memory map in an UEFI-heap allocated buffer
/// and returns a [`MemoryMapOwned`].
///
/// The implementation tries to mitigate some UEFI pitfalls, such as getting
/// the right allocation size for the memory map to prevent
/// [`Status::BUFFER_TOO_SMALL`].
///
/// # Arguments
///
/// - `mt`: The memory type for the backing memory on the UEFI heap.
///   Usually, this is [`MemoryType::LOADER_DATA`]. You can also use a
///   custom type.
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: Invalid [`MemoryType`]
/// * [`Status::OUT_OF_RESOURCES`]: allocation failed.
///
/// # Panics
///
/// Panics if the memory map can't be retrieved because of
/// [`Status::BUFFER_TOO_SMALL`]. This behaviour was chosen explicitly as
/// callers can't do anything about it anyway.
pub fn memory_map(mt: MemoryType) -> Result<MemoryMapOwned> {
    let mut buffer = MemoryMapBackingMemory::new(mt)?;

    let meta = get_memory_map(buffer.as_mut_slice());

    if let Err(e) = &meta {
        // We don't want to confuse users and let them think they should handle
        // this, as they can't do anything about it anyway.
        //
        // This path won't be taken in OOM situations, but only if for unknown
        // reasons, we failed to properly allocate the memory map.
        if e.status() == Status::BUFFER_TOO_SMALL {
            panic!("Failed to get a proper allocation for the memory map");
        }
    }
    let meta = meta?;

    Ok(MemoryMapOwned::from_initialized_mem(buffer, meta))
}

/// Calls the underlying `GetMemoryMap` function of UEFI. On success,
/// the buffer is mutated and contains the map. The map might be shorter
/// than the buffer, which is reflected by the return value.
pub(crate) fn get_memory_map(buf: &mut [u8]) -> Result<MemoryMapMeta> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let mut map_size = buf.len();
    let map_buffer = buf.as_mut_ptr().cast::<MemoryDescriptor>();
    let mut map_key = MemoryMapKey(0);
    let mut desc_size = 0;
    let mut desc_version = 0;

    assert_eq!(
        (map_buffer as usize) % align_of::<MemoryDescriptor>(),
        0,
        "Memory map buffers must be aligned like a MemoryDescriptor"
    );

    unsafe {
        (bt.get_memory_map)(
            &mut map_size,
            map_buffer,
            &mut map_key.0,
            &mut desc_size,
            &mut desc_version,
        )
    }
    .to_result_with_val(|| MemoryMapMeta {
        map_size,
        desc_size,
        map_key,
        desc_version,
    })
}

/// Creates an event.
///
/// This function creates a new event of the specified type and returns it.
///
/// Events are created in a "waiting" state, and may switch to a "signaled"
/// state. If the event type has flag `NotifySignal` set, this will result in
/// a callback for the event being immediately enqueued at the `notify_tpl`
/// priority level. If the event type has flag `NotifyWait`, the notification
/// will be delivered next time `wait_for_event` or `check_event` is called.
/// In both cases, a `notify_fn` callback must be specified.
///
/// # Safety
///
/// This function is unsafe because callbacks must handle exit from boot
/// services correctly.
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: an invalid combination of parameters was provided.
/// * [`Status::OUT_OF_RESOURCES`]: the event could not be allocated.
pub unsafe fn create_event(
    event_ty: EventType,
    notify_tpl: Tpl,
    notify_fn: Option<EventNotifyFn>,
    notify_ctx: Option<NonNull<c_void>>,
) -> Result<Event> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let mut event = ptr::null_mut();

    // Safety: the argument types of the function pointers are defined
    // differently, but are compatible and can be safely transmuted.
    let notify_fn: Option<uefi_raw::table::boot::EventNotifyFn> =
        unsafe { mem::transmute(notify_fn) };

    let notify_ctx = opt_nonnull_to_ptr(notify_ctx);

    // Now we're ready to call UEFI
    unsafe { (bt.create_event)(event_ty, notify_tpl, notify_fn, notify_ctx, &mut event) }
        .to_result_with_val(
            // OK to unwrap: event is non-null for Status::SUCCESS.
            || unsafe { Event::from_ptr(event) }.unwrap(),
        )
}

/// Creates an event in an event group.
///
/// The event's notification function, context, and task priority are specified
/// by `notify_fn`, `notify_ctx`, and `notify_tpl`, respectively. The event will
/// be added to the group of events identified by `event_group`.
///
/// If no group is specified by `event_group`, this function behaves as if the
/// same parameters had been passed to `create_event()`.
///
/// Event groups are collections of events identified by a shared GUID where,
/// when one member event is signaled, all other events are signaled and their
/// individual notification actions are taken. All events are guaranteed to be
/// signaled before the first notification action is taken. All notification
/// functions will be executed in the order specified by their `Tpl`.
///
/// An event can only be part of a single event group. An event may be removed
/// from an event group by calling [`close_event`].
///
/// The [`EventType`] of an event uses the same values as `create_event()`, except that
/// `EventType::SIGNAL_EXIT_BOOT_SERVICES` and `EventType::SIGNAL_VIRTUAL_ADDRESS_CHANGE`
/// are not valid.
///
/// For events of type `NOTIFY_SIGNAL` or `NOTIFY_WAIT`, `notify_fn` must be
/// `Some` and `notify_tpl` must be a valid task priority level. For other event
/// types these parameters are ignored.
///
/// More than one event of type `EventType::TIMER` may be part of a single event
/// group. However, there is no mechanism for determining which of the timers
/// was signaled.
///
/// This operation is only supported starting with UEFI 2.0; earlier versions
/// will fail with [`Status::UNSUPPORTED`].
///
/// # Safety
///
/// The caller must ensure they are passing a valid `Guid` as `event_group`, if applicable.
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: an invalid combination of parameters was provided.
/// * [`Status::OUT_OF_RESOURCES`]: the event could not be allocated.
pub unsafe fn create_event_ex(
    event_type: EventType,
    notify_tpl: Tpl,
    notify_fn: Option<EventNotifyFn>,
    notify_ctx: Option<NonNull<c_void>>,
    event_group: Option<NonNull<Guid>>,
) -> Result<Event> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    if bt.header.revision < Revision::EFI_2_00 {
        return Err(Status::UNSUPPORTED.into());
    }

    let mut event = ptr::null_mut();

    // Safety: the argument types of the function pointers are defined
    // differently, but are compatible and can be safely transmuted.
    let notify_fn: Option<uefi_raw::table::boot::EventNotifyFn> =
        unsafe { mem::transmute(notify_fn) };

    unsafe {
        (bt.create_event_ex)(
            event_type,
            notify_tpl,
            notify_fn,
            opt_nonnull_to_ptr(notify_ctx),
            opt_nonnull_to_ptr(event_group),
            &mut event,
        )
    }
    .to_result_with_val(
        // OK to unwrap: event is non-null for Status::SUCCESS.
        || unsafe { Event::from_ptr(event) }.unwrap(),
    )
}

/// Checks to see if an event is signaled, without blocking execution to wait for it.
///
/// Returns `Ok(true)` if the event is in the signaled state or `Ok(false)`
/// if the event is not in the signaled state.
///
/// # Errors
///
/// Note: Instead of returning [`Status::NOT_READY`] as listed in the UEFI
/// Specification, this function will return `Ok(false)`.
///
/// * [`Status::INVALID_PARAMETER`]: `event` is of type [`NOTIFY_SIGNAL`].
///
/// [`NOTIFY_SIGNAL`]: EventType::NOTIFY_SIGNAL
pub fn check_event(event: Event) -> Result<bool> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let status = unsafe { (bt.check_event)(event.as_ptr()) };
    match status {
        Status::SUCCESS => Ok(true),
        Status::NOT_READY => Ok(false),
        _ => Err(status.into()),
    }
}

/// Places the supplied `event` in the signaled state.
///
/// If `event` is already in
/// the signaled state, the function returns successfully. If `event` is of type
/// [`NOTIFY_SIGNAL`], the event's notification function is scheduled to be
/// invoked at the event's notification task priority level.
///
/// This function may be invoked from any task priority level.
///
/// If `event` is part of an event group, then all of the events in the event
/// group are also signaled and their notification functions are scheduled.
///
/// When signaling an event group, it is possible to create an event in the
/// group, signal it and then close the event to remove it from the group.
///
/// # Errors
///
/// The specification does not list any errors.
///
/// [`NOTIFY_SIGNAL`]: EventType::NOTIFY_SIGNAL
pub fn signal_event(event: &Event) -> Result {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    unsafe { (bt.signal_event)(event.as_ptr()) }.to_result()
}

/// Removes `event` from any event group to which it belongs and closes it.
///
/// If `event` was registered with [`register_protocol_notify`], then the
/// corresponding registration will be removed. Calling this function within the
/// corresponding notify function is allowed.
///
/// # Errors
///
/// The specification does not list any errors, however implementations are
/// allowed to return an error if needed.
pub fn close_event(event: Event) -> Result {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    unsafe { (bt.close_event)(event.as_ptr()) }.to_result()
}

/// Sets the trigger for an event of type [`TIMER`].
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: `event` is not valid.
///
/// [`TIMER`]: EventType::TIMER
pub fn set_timer(event: &Event, trigger_time: TimerTrigger) -> Result {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let (ty, time) = match trigger_time {
        TimerTrigger::Cancel => (TimerDelay::CANCEL, 0),
        TimerTrigger::Periodic(period) => (TimerDelay::PERIODIC, period),
        TimerTrigger::Relative(delay) => (TimerDelay::RELATIVE, delay),
    };
    unsafe { (bt.set_timer)(event.as_ptr(), ty, time) }.to_result()
}

/// Stops execution until an event is signaled.
///
/// This function must be called at priority level [`Tpl::APPLICATION`].
///
/// The input [`Event`] slice is repeatedly iterated from first to last until
/// an event is signaled or an error is detected. The following checks are
/// performed on each event:
///
/// * If an event is of type [`NOTIFY_SIGNAL`], then a
///   [`Status::INVALID_PARAMETER`] error is returned with the index of the
///   event that caused the failure.
/// * If an event is in the signaled state, the signaled state is cleared
///   and the index of the event that was signaled is returned.
/// * If an event is not in the signaled state but does have a notification
///   function, the notification function is queued at the event's
///   notification task priority level. If the execution of the event's
///   notification function causes the event to be signaled, then the
///   signaled state is cleared and the index of the event that was signaled
///   is returned.
///
/// To wait for a specified time, a timer event must be included in `events`.
///
/// To check if an event is signaled without waiting, an already signaled
/// event can be used as the last event in the slice being checked, or the
/// [`check_event`] interface may be used.
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: `events` is empty, or one of the events of
///   of type [`NOTIFY_SIGNAL`].
/// * [`Status::UNSUPPORTED`]: the current TPL is not [`Tpl::APPLICATION`].
///
/// [`NOTIFY_SIGNAL`]: EventType::NOTIFY_SIGNAL
pub fn wait_for_event(events: &mut [Event]) -> Result<usize, Option<usize>> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let number_of_events = events.len();
    let events: *mut uefi_raw::Event = events.as_mut_ptr().cast();

    let mut index = 0;
    unsafe { (bt.wait_for_event)(number_of_events, events, &mut index) }.to_result_with(
        || index,
        |s| {
            if s == Status::INVALID_PARAMETER {
                Some(index)
            } else {
                None
            }
        },
    )
}

/// Connect one or more drivers to a controller.
///
/// Usually one disconnects and then reconnects certain drivers
/// to make them rescan some state that changed, e.g. reconnecting
/// a block handle after your app modified disk partitions.
///
/// # Errors
///
/// * [`Status::NOT_FOUND`]: there are no driver-binding protocol instances
///   present in the system, or no drivers are connected to `controller`.
/// * [`Status::SECURITY_VIOLATION`]: the caller does not have permission to
///   start drivers associated with `controller`.
pub fn connect_controller(
    controller: Handle,
    driver_image: Option<Handle>,
    remaining_device_path: Option<&DevicePath>,
    recursive: bool,
) -> Result {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    unsafe {
        (bt.connect_controller)(
            controller.as_ptr(),
            Handle::opt_to_ptr(driver_image),
            remaining_device_path
                .map(|dp| dp.as_ffi_ptr())
                .unwrap_or(ptr::null())
                .cast(),
            recursive.into(),
        )
    }
    .to_result_with_err(|_| ())
}

/// Disconnect one or more drivers from a controller.
///
/// See also [`connect_controller`].
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: `driver_image` is set but does not manage
///   `controller`, or does not support the driver binding protocol, or one of
///   the handles is invalid.
/// * [`Status::OUT_OF_RESOURCES`]: not enough resources available to disconnect
///   drivers.
/// * [`Status::DEVICE_ERROR`]: the controller could not be disconnected due to
///   a device error.
pub fn disconnect_controller(
    controller: Handle,
    driver_image: Option<Handle>,
    child: Option<Handle>,
) -> Result {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    unsafe {
        (bt.disconnect_controller)(
            controller.as_ptr(),
            Handle::opt_to_ptr(driver_image),
            Handle::opt_to_ptr(child),
        )
    }
    .to_result_with_err(|_| ())
}

/// Installs a protocol interface on a device handle.
///
/// When a protocol interface is installed, firmware will call all functions
/// that have registered to wait for that interface to be installed.
///
/// If `handle` is `None`, a new handle will be created and returned.
///
/// # Safety
///
/// The caller is responsible for ensuring that they pass a valid `Guid` for `protocol`.
///
/// # Errors
///
/// * [`Status::OUT_OF_RESOURCES`]: failed to allocate a new handle.
/// * [`Status::INVALID_PARAMETER`]: this protocol is already installed on the handle.
pub unsafe fn install_protocol_interface(
    handle: Option<Handle>,
    protocol: &Guid,
    interface: *const c_void,
) -> Result<Handle> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let mut handle = Handle::opt_to_ptr(handle);
    unsafe {
        (bt.install_protocol_interface)(
            &mut handle,
            protocol,
            InterfaceType::NATIVE_INTERFACE,
            interface,
        )
    }
    .to_result_with_val(|| unsafe { Handle::from_ptr(handle) }.unwrap())
}

/// Reinstalls a protocol interface on a device handle. `old_interface` is
/// replaced with `new_interface`.
///
/// These interfaces may be the same, in which case the registered protocol
/// notifications occur for the handle without replacing the interface.
///
/// As with `install_protocol_interface`, any process that has registered to
/// wait for the installation of the interface is notified.
///
/// # Safety
///
/// The caller is responsible for ensuring that there are no references to the `old_interface` that is being
/// removed.
///
/// # Errors
///
/// * [`Status::NOT_FOUND`]: the old interface was not found on the handle.
/// * [`Status::ACCESS_DENIED`]: the old interface is still in use and cannot be uninstalled.
pub unsafe fn reinstall_protocol_interface(
    handle: Handle,
    protocol: &Guid,
    old_interface: *const c_void,
    new_interface: *const c_void,
) -> Result<()> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    unsafe {
        (bt.reinstall_protocol_interface)(handle.as_ptr(), protocol, old_interface, new_interface)
    }
    .to_result()
}

/// Removes a protocol interface from a device handle.
///
/// # Safety
///
/// The caller is responsible for ensuring that there are no references to a protocol interface
/// that has been removed. Some protocols may not be able to be removed as there is no information
/// available regarding the references. This includes Console I/O, Block I/O, Disk I/o, and handles
/// to device protocols.
///
/// The caller is responsible for ensuring that they pass a valid `Guid` for `protocol`.
///
/// # Errors
///
/// * [`Status::NOT_FOUND`]: the interface was not found on the handle.
/// * [`Status::ACCESS_DENIED`]: the interface is still in use and cannot be uninstalled.
pub unsafe fn uninstall_protocol_interface(
    handle: Handle,
    protocol: &Guid,
    interface: *const c_void,
) -> Result<()> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    unsafe { (bt.uninstall_protocol_interface)(handle.as_ptr(), protocol, interface).to_result() }
}

/// Registers `event` to be signaled whenever a protocol interface is registered for
/// `protocol` by [`install_protocol_interface`] or [`reinstall_protocol_interface`].
///
/// If successful, a [`SearchType::ByRegisterNotify`] is returned. This can be
/// used with [`locate_handle`] or [`locate_handle_buffer`] to identify the
/// newly (re)installed handles that support `protocol`.
///
/// Events can be unregistered from protocol interface notification by calling [`close_event`].
///
/// # Errors
///
/// * [`Status::OUT_OF_RESOURCES`]: the event could not be allocated.
pub fn register_protocol_notify(
    protocol: &'static Guid,
    event: &Event,
) -> Result<SearchType<'static>> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let mut key = ptr::null();
    unsafe { (bt.register_protocol_notify)(protocol, event.as_ptr(), &mut key) }.to_result_with_val(
        || {
            // OK to unwrap: key is non-null for Status::SUCCESS.
            SearchType::ByRegisterNotify(ProtocolSearchKey(NonNull::new(key.cast_mut()).unwrap()))
        },
    )
}

/// Get the list of protocol interface [`Guids`][Guid] that are installed
/// on a [`Handle`].
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: `handle` is invalid.
/// * [`Status::OUT_OF_RESOURCES`]: out of memory.
pub fn protocols_per_handle(handle: Handle) -> Result<ProtocolsPerHandle> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let mut protocols = ptr::null_mut();
    let mut count = 0;

    unsafe { (bt.protocols_per_handle)(handle.as_ptr(), &mut protocols, &mut count) }
        .to_result_with_val(|| ProtocolsPerHandle {
            count,
            protocols: NonNull::new(protocols)
                .expect("protocols_per_handle must not return a null pointer"),
        })
}

/// Locates the handle of a device on the [`DevicePath`] that supports the
/// specified [`Protocol`].
///
/// The `device_path` is updated to point at the remaining part that matched the
/// protocol. For example, this function can be used with a device path that
/// contains a file path to strip off the file system portion of the device
/// path, leaving the file path and handle to the file system driver needed to
/// access the file.
///
/// If the first node of `device_path` matches the protocol, the `device_path`
/// is advanced to the device path terminator node. If `device_path` is a
/// multi-instance device path, the function will operate on the first instance.
///
/// # Errors
///
/// * [`Status::NOT_FOUND`]: no matching handles.
pub fn locate_device_path<P: ProtocolPointer + ?Sized>(
    device_path: &mut &DevicePath,
) -> Result<Handle> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let mut handle = ptr::null_mut();
    let mut device_path_ptr: *const uefi_raw::protocol::device_path::DevicePathProtocol =
        device_path.as_ffi_ptr().cast();
    unsafe {
        (bt.locate_device_path)(&P::GUID, &mut device_path_ptr, &mut handle).to_result_with_val(
            || {
                *device_path = DevicePath::from_ffi_ptr(device_path_ptr.cast());
                // OK to unwrap: handle is non-null for Status::SUCCESS.
                Handle::from_ptr(handle).unwrap()
            },
        )
    }
}

/// Enumerates all handles installed on the system which match a certain query.
///
/// # Errors
///
/// * [`Status::NOT_FOUND`]: no matching handles found.
/// * [`Status::BUFFER_TOO_SMALL`]: the buffer is not large enough. The required
///   size (in number of handles, not bytes) will be returned in the error data.
pub fn locate_handle<'buf>(
    search_ty: SearchType,
    buffer: &'buf mut [MaybeUninit<Handle>],
) -> Result<&'buf [Handle], Option<usize>> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    // Obtain the needed data from the parameters.
    let (ty, guid, key) = match search_ty {
        SearchType::AllHandles => (0, ptr::null(), ptr::null()),
        SearchType::ByRegisterNotify(registration) => {
            (1, ptr::null(), registration.0.as_ptr().cast_const())
        }
        SearchType::ByProtocol(guid) => (2, ptr::from_ref(guid), ptr::null()),
    };

    let mut buffer_size = buffer.len() * size_of::<Handle>();
    let status =
        unsafe { (bt.locate_handle)(ty, guid, key, &mut buffer_size, buffer.as_mut_ptr().cast()) };

    let num_handles = buffer_size / size_of::<Handle>();

    match status {
        Status::SUCCESS => {
            let buffer = &buffer[..num_handles];
            // SAFETY: the entries up to `num_handles` have been initialized.
            let handles = unsafe { maybe_uninit_slice_assume_init_ref(buffer) };
            Ok(handles)
        }
        Status::BUFFER_TOO_SMALL => Err(Error::new(status, Some(num_handles))),
        _ => Err(Error::new(status, None)),
    }
}

/// Returns an array of handles that support the requested protocol in a
/// pool-allocated buffer.
///
/// See [`SearchType`] for details of the available search operations.
///
/// # Errors
///
/// * [`Status::NOT_FOUND`]: no matching handles.
/// * [`Status::OUT_OF_RESOURCES`]: out of memory.
pub fn locate_handle_buffer(search_ty: SearchType) -> Result<HandleBuffer> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let (ty, guid, key) = match search_ty {
        SearchType::AllHandles => (0, ptr::null(), ptr::null()),
        SearchType::ByRegisterNotify(registration) => {
            (1, ptr::null(), registration.0.as_ptr().cast_const())
        }
        SearchType::ByProtocol(guid) => (2, ptr::from_ref(guid), ptr::null()),
    };

    let mut num_handles: usize = 0;
    let mut buffer: *mut uefi_raw::Handle = ptr::null_mut();
    unsafe { (bt.locate_handle_buffer)(ty, guid, key, &mut num_handles, &mut buffer) }
        .to_result_with_val(|| HandleBuffer {
            count: num_handles,
            buffer: NonNull::new(buffer.cast())
                .expect("locate_handle_buffer must not return a null pointer"),
        })
}

/// Returns all the handles implementing a certain [`Protocol`].
///
/// # Errors
///
/// * [`Status::NOT_FOUND`]: no matching handles.
#[cfg(feature = "alloc")]
pub fn find_handles<P: ProtocolPointer + ?Sized>() -> Result<Vec<Handle>> {
    // Search by protocol.
    let search_type = SearchType::from_proto::<P>();

    // Determine how much we need to allocate.
    let num_handles = match locate_handle(search_type, &mut []) {
        Err(err) => {
            if err.status() == Status::BUFFER_TOO_SMALL {
                err.data().expect("error data is missing")
            } else {
                return Err(err.to_err_without_payload());
            }
        }
        // This should never happen: if no handles match the search then a
        // `NOT_FOUND` error should be returned.
        Ok(_) => panic!("locate_handle should not return success with empty buffer"),
    };

    // Allocate a large enough buffer without pointless initialization.
    let mut handles = Vec::with_capacity(num_handles);

    // Perform the search.
    let num_handles = locate_handle(search_type, handles.spare_capacity_mut())
        .discard_errdata()?
        .len();

    // Mark the returned number of elements as initialized.
    unsafe {
        handles.set_len(num_handles);
    }

    // Emit output, with warnings
    Ok(handles)
}

/// Find an arbitrary handle that supports a particular [`Protocol`]. Returns
/// [`NOT_FOUND`] if no handles support the protocol.
///
/// This method is a convenient wrapper around [`locate_handle_buffer`] for
/// getting just one handle. This is useful when you don't care which handle the
/// protocol is opened on. For example, [`DevicePathToText`] isn't tied to a
/// particular device, so only a single handle is expected to exist.
///
/// [`NOT_FOUND`]: Status::NOT_FOUND
/// [`DevicePathToText`]: uefi::proto::device_path::text::DevicePathToText
///
/// # Example
///
/// ```
/// use uefi::proto::device_path::text::DevicePathToText;
/// use uefi::{boot, Handle};
/// # use uefi::Result;
///
/// # fn get_fake_val<T>() -> T { todo!() }
/// # fn test() -> Result {
/// # let image_handle: Handle = get_fake_val();
/// let handle = boot::get_handle_for_protocol::<DevicePathToText>()?;
/// let device_path_to_text = boot::open_protocol_exclusive::<DevicePathToText>(handle)?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// * [`Status::NOT_FOUND`]: no matching handle.
/// * [`Status::OUT_OF_RESOURCES`]: out of memory.
pub fn get_handle_for_protocol<P: ProtocolPointer + ?Sized>() -> Result<Handle> {
    locate_handle_buffer(SearchType::ByProtocol(&P::GUID))?
        .first()
        .cloned()
        .ok_or_else(|| Status::NOT_FOUND.into())
}

/// Opens a [`Protocol`] interface for a handle.
///
/// See also [`open_protocol_exclusive`], which provides a safe subset of this
/// functionality.
///
/// This function attempts to get the protocol implementation of a handle, based
/// on the [protocol GUID].
///
/// See [`OpenProtocolParams`] and [`OpenProtocolAttributes`] for details of the
/// input parameters.
///
/// If successful, a [`ScopedProtocol`] is returned that will automatically
/// close the protocol interface when dropped.
///
/// [protocol GUID]: uefi::data_types::Identify::GUID
///
/// # Safety
///
/// This function is unsafe because it can be used to open a protocol in ways
/// that don't get tracked by the UEFI implementation. This could allow the
/// protocol to be removed from a handle, or for the handle to be deleted
/// entirely, while a reference to the protocol is still active. The caller is
/// responsible for ensuring that the handle and protocol remain valid until the
/// `ScopedProtocol` is dropped.
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: an invalid combination of `params` and
///   `attributes` was provided.
/// * [`Status::UNSUPPORTED`]: the handle does not support the protocol.
/// * [`Status::ACCESS_DENIED`] or [`Status::ALREADY_STARTED`]: the protocol is
///   already open in a way that is incompatible with the new request.
pub unsafe fn open_protocol<P: ProtocolPointer + ?Sized>(
    params: OpenProtocolParams,
    attributes: OpenProtocolAttributes,
) -> Result<ScopedProtocol<P>> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let mut interface = ptr::null_mut();
    unsafe {
        (bt.open_protocol)(
            params.handle.as_ptr(),
            &P::GUID,
            &mut interface,
            params.agent.as_ptr(),
            Handle::opt_to_ptr(params.controller),
            attributes as u32,
        )
    }
    .to_result_with_val(|| {
        let interface = if interface.is_null() {
            None
        } else {
            NonNull::new(unsafe { P::mut_ptr_from_ffi(interface) })
        };
        ScopedProtocol {
            interface,
            open_params: params,
        }
    })
}

/// Opens a [`Protocol`] interface for a handle in exclusive mode.
///
/// If successful, a [`ScopedProtocol`] is returned that will automatically
/// close the protocol interface when dropped.
///
/// # Errors
///
/// * [`Status::UNSUPPORTED`]: the handle does not support the protocol.
/// * [`Status::ACCESS_DENIED`]: the protocol is already open in a way that is
///   incompatible with the new request.
pub fn open_protocol_exclusive<P: ProtocolPointer + ?Sized>(
    handle: Handle,
) -> Result<ScopedProtocol<P>> {
    // Safety: opening in exclusive mode with the correct agent
    // handle set ensures that the protocol cannot be modified or
    // removed while it is open, so this usage is safe.
    unsafe {
        open_protocol::<P>(
            OpenProtocolParams {
                handle,
                agent: image_handle(),
                controller: None,
            },
            OpenProtocolAttributes::Exclusive,
        )
    }
}

/// Tests whether a handle supports a [`Protocol`].
///
/// Returns `Ok(true)` if the handle supports the protocol, `Ok(false)` if not.
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: one of the handles in `params` is invalid.
pub fn test_protocol<P: ProtocolPointer + ?Sized>(params: OpenProtocolParams) -> Result<bool> {
    const TEST_PROTOCOL: u32 = 0x04;

    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let mut interface = ptr::null_mut();
    let status = unsafe {
        (bt.open_protocol)(
            params.handle.as_ptr(),
            &P::GUID,
            &mut interface,
            params.agent.as_ptr(),
            Handle::opt_to_ptr(params.controller),
            TEST_PROTOCOL,
        )
    };

    match status {
        Status::SUCCESS => Ok(true),
        Status::UNSUPPORTED => Ok(false),
        _ => Err(Error::from(status)),
    }
}

/// Loads a UEFI image into memory and return a [`Handle`] to the image.
///
/// There are two ways to load the image: by copying raw image data
/// from a source buffer, or by loading the image via the
/// [`SimpleFileSystem`] protocol. See [`LoadImageSource`] for more
/// details of the `source` parameter.
///
/// The `parent_image_handle` is used to initialize the
/// `parent_handle` field of the [`LoadedImage`] protocol for the
/// image.
///
/// If the image is successfully loaded, a [`Handle`] supporting the
/// [`LoadedImage`] and [`LoadedImageDevicePath`] protocols is returned. The
/// image can be started with [`start_image`] and unloaded with
/// [`unload_image`].
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: `source` contains an invalid value.
/// * [`Status::UNSUPPORTED`]: the image type is not supported.
/// * [`Status::OUT_OF_RESOURCES`]: insufficient resources to load the image.
/// * [`Status::LOAD_ERROR`]: the image is invalid.
/// * [`Status::DEVICE_ERROR`]: failed to load image due to a read error.
/// * [`Status::ACCESS_DENIED`]: failed to load image due to a security policy.
/// * [`Status::SECURITY_VIOLATION`]: a security policy specifies that the image
///   should not be started.
pub fn load_image(parent_image_handle: Handle, source: LoadImageSource) -> Result<Handle> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let (boot_policy, device_path, source_buffer, source_size) = source.to_ffi_params();

    let mut image_handle = ptr::null_mut();
    unsafe {
        (bt.load_image)(
            boot_policy.into(),
            parent_image_handle.as_ptr(),
            device_path.cast(),
            source_buffer,
            source_size,
            &mut image_handle,
        )
        .to_result_with_val(
            // OK to unwrap: image handle is non-null for Status::SUCCESS.
            || Handle::from_ptr(image_handle).unwrap(),
        )
    }
}

/// Unloads a UEFI image.
///
/// # Errors
///
/// * [`Status::UNSUPPORTED`]: the image has been started, and does not support unload.
/// * [`Status::INVALID_PARAMETER`]: `image_handle` is not valid.
pub fn unload_image(image_handle: Handle) -> Result {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    unsafe { (bt.unload_image)(image_handle.as_ptr()) }.to_result()
}

/// Transfers control to a loaded image's entry point.
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: `image_handle` is not valid, or the image
///   has already been initialized with `start_image`.
/// * [`Status::SECURITY_VIOLATION`]: a security policy specifies that the image
///   should not be started.
pub fn start_image(image_handle: Handle) -> Result {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    // TODO: implement returning exit data to the caller.
    let mut exit_data_size: usize = 0;
    let mut exit_data: *mut u16 = ptr::null_mut();

    unsafe {
        (bt.start_image)(image_handle.as_ptr(), &mut exit_data_size, &mut exit_data).to_result()
    }
}

/// Exits the UEFI application and returns control to the UEFI component
/// that started the UEFI application.
///
/// # Safety
///
/// The caller must ensure that resources owned by the application are properly
/// cleaned up.
///
/// Note that event callbacks installed by the application are not automatically
/// uninstalled. If such a callback is invoked after exiting the application,
/// the function's code may no longer be loaded in memory, leading to a crash or
/// other unexpected behavior.
pub unsafe fn exit(
    image_handle: Handle,
    exit_status: Status,
    exit_data_size: usize,
    exit_data: *mut Char16,
) -> ! {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    unsafe {
        (bt.exit)(
            image_handle.as_ptr(),
            exit_status,
            exit_data_size,
            exit_data.cast(),
        )
    }
}

/// Get the current memory map and exit boot services.
unsafe fn get_memory_map_and_exit_boot_services(buf: &mut [u8]) -> Result<MemoryMapMeta> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    // Get the memory map.
    let memory_map = get_memory_map(buf)?;

    // Try to exit boot services using the memory map key. Note that after
    // the first call to `exit_boot_services`, there are restrictions on
    // what boot services functions can be called. In UEFI 2.8 and earlier,
    // only `get_memory_map` and `exit_boot_services` are allowed. Starting
    // in UEFI 2.9 other memory allocation functions may also be called.
    unsafe { (bt.exit_boot_services)(image_handle().as_ptr(), memory_map.map_key.0) }
        .to_result_with_val(|| memory_map)
}

/// Convenient wrapper to exit UEFI boot services along with corresponding
/// essential steps to get the memory map.
///
/// Exiting UEFI boot services requires a **non-trivial sequence of steps**,
/// including safe retrieval and finalization of the memory map. This wrapper
/// ensures a safe and spec-compliant transition from UEFI boot services to
/// runtime services by retrieving the system memory map and invoking
/// `ExitBootServices()` with the correct memory map key, retrying if necessary.
///
/// After this function completes, UEFI hands over control of the hardware
/// to the executing OS loader, which implies that the UEFI boot services
/// are shut down and cannot be used anymore. Only UEFI configuration tables
/// and runtime services can be used.
///
/// Since the boot services function to free memory is no longer available after
/// this function returns, the allocation will not be freed on drop. It will
/// however be reflected by the memory map itself (self-contained).
///
/// Note that once the boot services are exited, associated loggers and
/// allocators can't use the boot services anymore. For the corresponding
/// abstractions provided by this crate (see the [`helpers`] module),
/// invoking this function will automatically disable them. If the
/// `global_allocator` feature is enabled, attempting to use the allocator
/// after exiting boot services will panic.
///
/// # Arguments
/// - `custom_memory_type`: The [`MemoryType`] for the UEFI allocation that will
///   store the final memory map. If you pass `None`, this defaults to the
///   recommended default value of [`MemoryType::LOADER_DATA`]. If you want a
///   specific memory region for the memory map, you can pass the desired
///   [`MemoryType`].
///
/// # Safety
///
/// The caller is responsible for ensuring that no references to
/// boot-services data remain. A non-exhaustive list of resources to check:
///
/// * All protocols will be invalid after exiting boot services. This
///   includes the [`Output`] protocols attached to stdout/stderr. The
///   caller must ensure that no protocol references remain.
/// * The pool allocator is not usable after exiting boot services. Types
///   such as [`PoolString`] which call [`free_pool`] on drop
///   must be cleaned up before calling `exit_boot_services`, or leaked to
///   avoid drop ever being called.
/// * All data in the memory map marked as
///   [`MemoryType::BOOT_SERVICES_CODE`] and
///   [`MemoryType::BOOT_SERVICES_DATA`] will become free memory.
///
/// # Errors
///
/// This function will fail if it is unable to allocate memory for
/// the memory map, if it fails to retrieve the memory map, or if
/// exiting boot services fails (with up to one retry).
///
/// All errors are treated as unrecoverable because the system is
/// now in an undefined state. Rather than returning control to the
/// caller, the system will be reset.
///
/// [`helpers`]: crate::helpers
/// [`Output`]: crate::proto::console::text::Output
/// [`PoolString`]: crate::data_types::PoolString
#[must_use]
pub unsafe fn exit_boot_services(custom_memory_type: Option<MemoryType>) -> MemoryMapOwned {
    // LOADER_DATA is the default and also used by the Linux kernel:
    // https://elixir.bootlin.com/linux/v6.13.7/source/drivers/firmware/efi/libstub/mem.c#L24
    let memory_type = custom_memory_type.unwrap_or(MemoryType::LOADER_DATA);
    crate::helpers::exit();

    let mut buf = MemoryMapBackingMemory::new(memory_type).expect("Failed to allocate memory");

    // Calling `exit_boot_services` can fail if the memory map key is not
    // current. Retry a second time if that occurs. This matches the
    // behavior of the Linux kernel:
    // https://github.com/torvalds/linux/blob/e544a0743/drivers/firmware/efi/libstub/efi-stub-helper.c#L375
    let mut status = Status::ABORTED;
    for _ in 0..2 {
        match unsafe { get_memory_map_and_exit_boot_services(buf.as_mut_slice()) } {
            Ok(memory_map) => {
                return MemoryMapOwned::from_initialized_mem(buf, memory_map);
            }
            Err(err) => {
                log::error!("Error retrieving the memory map for exiting the boot services");
                status = err.status()
            }
        }
    }

    // Failed to exit boot services.
    log::warn!("Resetting the machine");
    runtime::reset(ResetType::COLD, status, None);
}

/// Adds, updates, or removes a configuration table entry
/// from the EFI System Table.
///
/// # Safety
///
/// When installing or updating a configuration table, the data pointed to by
/// `table_ptr` must be a pool allocation of type
/// [`RUNTIME_SERVICES_DATA`]. Once this table has been installed, the caller
/// should not modify or free the data.
///
/// [`RUNTIME_SERVICES_DATA`]: MemoryType::RUNTIME_SERVICES_DATA
///
/// # Errors
///
/// * [`Status::NOT_FOUND`]: tried to delete a nonexistent entry.
/// * [`Status::OUT_OF_RESOURCES`]: out of memory.
pub unsafe fn install_configuration_table(
    guid_entry: &'static Guid,
    table_ptr: *const c_void,
) -> Result {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    unsafe { (bt.install_configuration_table)(guid_entry, table_ptr) }.to_result()
}

/// Sets the watchdog timer.
///
/// UEFI will start a 5-minute countdown after an UEFI image is loaded.  The
/// image must either successfully load an OS and exit boot services in that
/// time, or disable the watchdog.
///
/// Otherwise, the firmware will log the event using the provided numeric
/// code and data, then reset the system.
///
/// This function allows you to change the watchdog timer's timeout to a
/// certain amount of seconds or to disable the watchdog entirely. It also
/// allows you to change what will be logged when the timer expires.
///
/// The watchdog codes from 0 to 0xffff (65535) are reserved for internal
/// firmware use. Higher values can be used freely by applications.
///
/// If provided, the watchdog data must be a null-terminated string optionally
/// followed by other binary data.
///
/// # Errors
///
/// * [`Status::INVALID_PARAMETER`]: `watchdog_code` is invalid.
/// * [`Status::UNSUPPORTED`]: the system does not have a watchdog timer.
/// * [`Status::DEVICE_ERROR`]: the watchdog timer could not be set due to a
///   hardware error.
pub fn set_watchdog_timer(
    timeout_in_seconds: usize,
    watchdog_code: u64,
    data: Option<&mut [u16]>,
) -> Result {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let (data_len, data) = data
        .map(|d| {
            assert!(
                d.contains(&0),
                "Watchdog data must start with a null-terminated string"
            );
            (d.len(), d.as_mut_ptr())
        })
        .unwrap_or((0, ptr::null_mut()));

    unsafe { (bt.set_watchdog_timer)(timeout_in_seconds, watchdog_code, data_len, data) }
        .to_result()
}

/// Stalls execution for the given duration.
pub fn stall(duration: Duration) {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };

    let microseconds = duration.as_micros() as usize;

    unsafe {
        // No error conditions are defined in the spec for this function, so
        // ignore the status.
        let _ = (bt.stall)(microseconds);
    }
}

/// Retrieves a [`SimpleFileSystem`] protocol associated with the device the given
/// image was loaded from.
///
/// # Errors
///
/// This function can return errors from [`open_protocol_exclusive`] and
/// [`locate_device_path`]. See those functions for more details.
///
/// * [`Status::INVALID_PARAMETER`]
/// * [`Status::UNSUPPORTED`]
/// * [`Status::ACCESS_DENIED`]
/// * [`Status::ALREADY_STARTED`]
/// * [`Status::NOT_FOUND`]
pub fn get_image_file_system(image_handle: Handle) -> Result<ScopedProtocol<SimpleFileSystem>> {
    let loaded_image = open_protocol_exclusive::<LoadedImage>(image_handle)?;

    let device_handle = loaded_image
        .device()
        .ok_or(Error::new(Status::UNSUPPORTED, ()))?;
    let device_path = open_protocol_exclusive::<DevicePath>(device_handle)?;

    let device_handle = locate_device_path::<SimpleFileSystem>(&mut &*device_path)?;

    open_protocol_exclusive(device_handle)
}

/// Calculates the 32-bit CRC32 for the provided slice.
///
/// # Errors
/// * [`Status::INVALID_PARAMETER`]
pub fn calculate_crc32(data: &[u8]) -> Result<u32> {
    let bt = boot_services_raw_panicking();
    let bt = unsafe { bt.as_ref() };
    let mut crc = 0u32;

    unsafe { (bt.calculate_crc32)(data.as_ptr().cast(), data.len(), &mut crc) }
        .to_result_with_val(|| crc)
}

/// Protocol interface [`Guids`][Guid] that are installed on a [`Handle`] as
/// returned by [`protocols_per_handle`].
#[derive(Debug)]
pub struct ProtocolsPerHandle {
    protocols: NonNull<*const Guid>,
    count: usize,
}

impl Drop for ProtocolsPerHandle {
    fn drop(&mut self) {
        let _ = unsafe { free_pool(self.protocols.cast::<u8>()) };
    }
}

impl Deref for ProtocolsPerHandle {
    type Target = [&'static Guid];

    fn deref(&self) -> &Self::Target {
        let ptr: *const &'static Guid = self.protocols.as_ptr().cast();

        // SAFETY:
        //
        // * The firmware is assumed to provide a correctly-aligned pointer and
        //   array length.
        // * The firmware is assumed to provide valid GUID pointers.
        // * Protocol GUIDs should be constants or statics, so a 'static
        //   lifetime (of the individual pointers, not the overall slice) can be
        //   assumed.
        unsafe { slice::from_raw_parts(ptr, self.count) }
    }
}

/// A buffer returned by [`locate_handle_buffer`] that contains an array of
/// [`Handle`]s that support the requested [`Protocol`].
#[derive(Debug, Eq, PartialEq)]
pub struct HandleBuffer {
    count: usize,
    buffer: NonNull<Handle>,
}

impl Drop for HandleBuffer {
    fn drop(&mut self) {
        let _ = unsafe { free_pool(self.buffer.cast::<u8>()) };
    }
}

impl Deref for HandleBuffer {
    type Target = [Handle];

    fn deref(&self) -> &Self::Target {
        unsafe { slice::from_raw_parts(self.buffer.as_ptr(), self.count) }
    }
}

/// An open [`Protocol`] interface. Automatically closes the protocol
/// interface on drop.
///
/// Most protocols have interface data associated with them. `ScopedProtocol`
/// implements [`Deref`] and [`DerefMut`] to access this data. A few protocols
/// (such as [`DevicePath`] and [`LoadedImageDevicePath`]) may be installed with
/// null interface data, in which case [`Deref`] and [`DerefMut`] will
/// panic. The [`get`] and [`get_mut`] methods may be used to access the
/// optional interface data without panicking.
///
/// [`DevicePath`]: crate::proto::device_path::DevicePath
/// [`LoadedImageDevicePath`]: crate::proto::device_path::LoadedImageDevicePath
/// [`get`]: ScopedProtocol::get
/// [`get_mut`]: ScopedProtocol::get_mut
#[derive(Debug)]
pub struct ScopedProtocol<P: Protocol + ?Sized> {
    /// The protocol interface.
    interface: Option<NonNull<P>>,
    open_params: OpenProtocolParams,
}

impl<P: Protocol + ?Sized> ScopedProtocol<P> {
    /// Returns the [`OpenProtocolParams`] used to open the [`ScopedProtocol`].
    #[must_use]
    pub const fn open_params(&self) -> OpenProtocolParams {
        self.open_params
    }
}

impl<P: Protocol + ?Sized> Drop for ScopedProtocol<P> {
    fn drop(&mut self) {
        let bt = boot_services_raw_panicking();
        let bt = unsafe { bt.as_ref() };

        let status = unsafe {
            (bt.close_protocol)(
                self.open_params.handle.as_ptr(),
                &P::GUID,
                self.open_params.agent.as_ptr(),
                Handle::opt_to_ptr(self.open_params.controller),
            )
        };
        // All of the error cases for close_protocol boil down to
        // calling it with a different set of parameters than what was
        // passed to open_protocol. The public API prevents such errors,
        // and the error can't be propagated out of drop anyway, so just
        // assert success.
        assert_eq!(status, Status::SUCCESS);
    }
}

impl<P: Protocol + ?Sized> Deref for ScopedProtocol<P> {
    type Target = P;

    #[track_caller]
    fn deref(&self) -> &Self::Target {
        unsafe { self.interface.unwrap().as_ref() }
    }
}

impl<P: Protocol + ?Sized> DerefMut for ScopedProtocol<P> {
    #[track_caller]
    fn deref_mut(&mut self) -> &mut Self::Target {
        unsafe { self.interface.unwrap().as_mut() }
    }
}

impl<P: Protocol + ?Sized> ScopedProtocol<P> {
    /// Get the protocol interface data, or `None` if the open protocol's
    /// interface is null.
    #[must_use]
    pub fn get(&self) -> Option<&P> {
        self.interface.map(|p| unsafe { p.as_ref() })
    }

    /// Get the protocol interface data, or `None` if the open protocol's
    /// interface is null.
    #[must_use]
    pub fn get_mut(&mut self) -> Option<&mut P> {
        self.interface.map(|mut p| unsafe { p.as_mut() })
    }
}

/// RAII guard for task priority level changes.
///
/// Will automatically restore the former task priority level when dropped.
#[derive(Debug)]
pub struct TplGuard {
    old_tpl: Tpl,
}

impl TplGuard {
    /// Returns the previous [`Tpl`].
    #[must_use]
    pub const fn old_tpl(&self) -> Tpl {
        self.old_tpl
    }
}

impl Drop for TplGuard {
    fn drop(&mut self) {
        let bt = boot_services_raw_panicking();
        let bt = unsafe { bt.as_ref() };

        unsafe {
            (bt.restore_tpl)(self.old_tpl);
        }
    }
}

// OpenProtocolAttributes is safe to model as a regular enum because it
// is only used as an input. The attributes are bitflags, but all valid
// combinations are listed in the spec and only ByDriver and Exclusive
// can actually be combined.
//
// Some values intentionally excluded:
//
// ByHandleProtocol (0x01) excluded because it is only intended to be
// used in an implementation of `HandleProtocol`.
//
// TestProtocol (0x04) excluded because it doesn't actually open the
// protocol, just tests if it's present on the handle. Since that
// changes the interface significantly, that's exposed as a separate
// method: `test_protocol`.

/// Attributes for [`open_protocol`].
#[repr(u32)]
#[derive(Debug)]
pub enum OpenProtocolAttributes {
    /// Used by drivers to get a protocol interface for a handle. The
    /// driver will not be informed if the interface is uninstalled or
    /// reinstalled.
    GetProtocol = 0x02,

    /// Used by bus drivers to show that a protocol is being used by one
    /// of the child controllers of the bus.
    ByChildController = 0x08,

    /// Used by a driver to gain access to a protocol interface. When
    /// this mode is used, the driver's `Stop` function will be called
    /// if the protocol interface is reinstalled or uninstalled. Once a
    /// protocol interface is opened with this attribute, no other
    /// drivers will be allowed to open the same protocol interface with
    /// the `ByDriver` attribute.
    ByDriver = 0x10,

    /// Used by a driver to gain exclusive access to a protocol
    /// interface. If any other drivers have the protocol interface
    /// opened with an attribute of `ByDriver`, then an attempt will be
    /// made to remove them with `DisconnectController`.
    ByDriverExclusive = 0x30,

    /// Used by applications to gain exclusive access to a protocol
    /// interface. If any drivers have the protocol opened with an
    /// attribute of `ByDriver`, then an attempt will be made to remove
    /// them by calling the driver's `Stop` function.
    Exclusive = 0x20,
}

/// Parameters passed to [`open_protocol`].
#[derive(Clone, Copy, Debug)]
pub struct OpenProtocolParams {
    /// The handle for the protocol to open.
    pub handle: Handle,

    /// The handle of the calling agent. For drivers, this is the handle
    /// containing the `EFI_DRIVER_BINDING_PROTOCOL` instance. For
    /// applications, this is the image handle.
    pub agent: Handle,

    /// For drivers, this is the controller handle that requires the
    /// protocol interface. For applications this should be set to
    /// `None`.
    pub controller: Option<Handle>,
}

/// Used as a parameter of [`load_image`] to provide the image source.
#[derive(Debug)]
pub enum LoadImageSource<'a> {
    /// Load an image from a buffer. The data will be copied from the
    /// buffer, so the input reference doesn't need to remain valid
    /// after the image is loaded.
    FromBuffer {
        /// Raw image data.
        buffer: &'a [u8],

        /// If set, this path will be added as the file path of the
        /// loaded image. This is not required to load the image, but
        /// may be used by the image itself to load other resources
        /// relative to the image's path.
        file_path: Option<&'a DevicePath>,
    },

    /// Load an image via the [`SimpleFileSystem`] protocol. If there is
    /// no instance of that protocol associated with the path then the
    /// behavior depends on [`BootPolicy`]. If [`BootPolicy::BootSelection`],
    /// attempt to load via the [`LoadFile`] protocol. If
    /// [`BootPolicy::ExactMatch`], attempt to load via the [`LoadFile2`]
    /// protocol, then fall back to [`LoadFile`].
    ///
    /// [`LoadFile`]: crate::proto::media::load_file::LoadFile
    /// [`LoadFile2`]: crate::proto::media::load_file::LoadFile2
    FromDevicePath {
        /// The full device path from which to load the image.
        ///
        /// The provided path should be a full device path and not just the
        /// file path portion of it. So for example, it must be (the binary
        /// representation)
        /// `PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0,0xFFFF,0x0)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\\EFI\\BOOT\\BOOTX64.EFI`
        /// and not just `\\EFI\\BOOT\\BOOTX64.EFI`.
        device_path: &'a DevicePath,

        /// The [`BootPolicy`] to use.
        boot_policy: BootPolicy,
    },
}

impl LoadImageSource<'_> {
    /// Returns the raw FFI parameters for `load_image`.
    #[must_use]
    pub(crate) fn to_ffi_params(
        &self,
    ) -> (
        BootPolicy,
        *const FfiDevicePath,
        *const u8, /* buffer */
        usize,     /* buffer length */
    ) {
        let boot_policy;
        let device_path;
        let source_buffer;
        let source_size;
        match self {
            LoadImageSource::FromBuffer { buffer, file_path } => {
                // Boot policy is ignored when loading from source buffer.
                boot_policy = BootPolicy::default();

                device_path = file_path.map(|p| p.as_ffi_ptr()).unwrap_or(ptr::null());
                source_buffer = buffer.as_ptr();
                source_size = buffer.len();
            }
            LoadImageSource::FromDevicePath {
                device_path: d_path,
                boot_policy: b_policy,
            } => {
                boot_policy = *b_policy;
                device_path = d_path.as_ffi_ptr();
                source_buffer = ptr::null();
                source_size = 0;
            }
        };
        (boot_policy, device_path, source_buffer, source_size)
    }
}

/// Type of allocation to perform.
#[derive(Debug, Copy, Clone)]
pub enum AllocateType {
    /// Allocate any possible pages.
    AnyPages,
    /// Allocate pages at any address below the given address.
    MaxAddress(PhysicalAddress),
    /// Allocate pages at the specified address.
    Address(PhysicalAddress),
}

/// The type of handle search to perform.
#[derive(Debug, Copy, Clone)]
pub enum SearchType<'guid> {
    /// Return all handles present on the system.
    AllHandles,
    /// Returns all handles supporting a certain protocol, specified by its GUID.
    ///
    /// If the protocol implements the `Protocol` interface,
    /// you can use the `from_proto` function to construct a new `SearchType`.
    ByProtocol(&'guid Guid),
    /// Return all handles that implement a protocol when an interface for that protocol
    /// is (re)installed.
    ByRegisterNotify(ProtocolSearchKey),
}

impl SearchType<'_> {
    /// Constructs a new search type for a specified protocol.
    #[must_use]
    pub const fn from_proto<P: ProtocolPointer + ?Sized>() -> Self {
        SearchType::ByProtocol(&P::GUID)
    }
}

/// Event notification callback type.
pub type EventNotifyFn = unsafe extern "efiapi" fn(event: Event, context: Option<NonNull<c_void>>);

/// Trigger type for events of type [`TIMER`].
///
/// See [`set_timer`].
///
/// [`TIMER`]: EventType::TIMER
#[derive(Debug)]
pub enum TimerTrigger {
    /// Remove the event's timer setting.
    Cancel,

    /// Trigger the event periodically.
    Periodic(
        /// Duration between event signaling in units of 100ns. If set to zero,
        /// the event will be signaled on every timer tick.
        u64,
    ),

    /// Trigger the event one time.
    Relative(
        /// Duration to wait before signaling the event in units of 100ns. If
        /// set to zero, the event will be signaled on the next timer tick.
        u64,
    ),
}

/// Opaque pointer returned by [`register_protocol_notify`] to be used
/// with [`locate_handle`] via [`SearchType::ByRegisterNotify`].
#[derive(Debug, Clone, Copy)]
#[repr(transparent)]
pub struct ProtocolSearchKey(pub(crate) NonNull<c_void>);
