// `rustdoc` is buggy, claiming that we have some links to private items
// when they are actually public.
#![allow(rustdoc::private_intra_doc_links)]

use std::ffi::c_void;
use std::ffi::CStr;
use std::ffi::OsStr;
use std::marker::PhantomData;
use std::mem;
use std::mem::size_of;
use std::mem::size_of_val;
use std::mem::transmute;
use std::ops::Deref;
use std::os::unix::ffi::OsStrExt as _;
use std::os::unix::io::AsFd;
use std::os::unix::io::AsRawFd;
use std::os::unix::io::BorrowedFd;
use std::os::unix::io::FromRawFd;
use std::os::unix::io::OwnedFd;
use std::path::Path;
use std::ptr;
use std::ptr::NonNull;
use std::slice;

use libbpf_sys::bpf_func_id;

use crate::util;
use crate::util::validate_bpf_ret;
use crate::AsRawLibbpf;
use crate::Error;
use crate::ErrorExt as _;
use crate::Link;
use crate::Mut;
use crate::Result;

/// Options to optionally be provided when attaching to a uprobe.
#[derive(Clone, Debug, Default)]
pub struct UprobeOpts {
    /// Offset of kernel reference counted USDT semaphore.
    pub ref_ctr_offset: usize,
    /// Custom user-provided value accessible through `bpf_get_attach_cookie`.
    pub cookie: u64,
    /// uprobe is return probe, invoked at function return time.
    pub retprobe: bool,
    /// Function name to attach to.
    ///
    /// Could be an unqualified ("abc") or library-qualified "abc@LIBXYZ" name.
    /// To specify function entry, `func_name` should be set while `func_offset`
    /// argument to should be 0. To trace an offset within a function, specify
    /// `func_name` and use `func_offset` argument to specify offset within the
    /// function. Shared library functions must specify the shared library
    /// binary_path.
    pub func_name: String,
    #[doc(hidden)]
    pub _non_exhaustive: (),
}

/// Options to optionally be provided when attaching to a USDT.
#[derive(Clone, Debug, Default)]
pub struct UsdtOpts {
    /// Custom user-provided value accessible through `bpf_usdt_cookie`.
    pub cookie: u64,
    #[doc(hidden)]
    pub _non_exhaustive: (),
}

impl From<UsdtOpts> for libbpf_sys::bpf_usdt_opts {
    fn from(opts: UsdtOpts) -> Self {
        let UsdtOpts {
            cookie,
            _non_exhaustive,
        } = opts;
        #[allow(clippy::needless_update)]
        libbpf_sys::bpf_usdt_opts {
            sz: size_of::<Self>() as _,
            usdt_cookie: cookie,
            // bpf_usdt_opts might have padding fields on some platform
            ..Default::default()
        }
    }
}

/// Options to optionally be provided when attaching to a tracepoint.
#[derive(Clone, Debug, Default)]
pub struct TracepointOpts {
    /// Custom user-provided value accessible through `bpf_get_attach_cookie`.
    pub cookie: u64,
    #[doc(hidden)]
    pub _non_exhaustive: (),
}

impl From<TracepointOpts> for libbpf_sys::bpf_tracepoint_opts {
    fn from(opts: TracepointOpts) -> Self {
        let TracepointOpts {
            cookie,
            _non_exhaustive,
        } = opts;

        #[allow(clippy::needless_update)]
        libbpf_sys::bpf_tracepoint_opts {
            sz: size_of::<Self>() as _,
            bpf_cookie: cookie,
            // bpf_tracepoint_opts might have padding fields on some platform
            ..Default::default()
        }
    }
}


/// An immutable parsed but not yet loaded BPF program.
pub type OpenProgram<'obj> = OpenProgramImpl<'obj>;
/// A mutable parsed but not yet loaded BPF program.
pub type OpenProgramMut<'obj> = OpenProgramImpl<'obj, Mut>;

/// Represents a parsed but not yet loaded BPF program.
///
/// This object exposes operations that need to happen before the program is loaded.
#[derive(Debug)]
#[repr(transparent)]
pub struct OpenProgramImpl<'obj, T = ()> {
    ptr: NonNull<libbpf_sys::bpf_program>,
    _phantom: PhantomData<&'obj T>,
}

// TODO: Document variants.
#[allow(missing_docs)]
impl<'obj> OpenProgram<'obj> {
    /// Create a new [`OpenProgram`] from a ptr to a `libbpf_sys::bpf_program`.
    pub fn new(prog: &'obj libbpf_sys::bpf_program) -> Self {
        // SAFETY: We inferred the address from a reference, which is always
        //         valid.
        Self {
            ptr: unsafe { NonNull::new_unchecked(prog as *const _ as *mut _) },
            _phantom: PhantomData,
        }
    }

    // The `ProgramType` of this `OpenProgram`.
    pub fn prog_type(&self) -> ProgramType {
        ProgramType::from(unsafe { libbpf_sys::bpf_program__type(self.ptr.as_ptr()) })
    }

    /// Retrieve the name of this `OpenProgram`.
    pub fn name(&self) -> &OsStr {
        let name_ptr = unsafe { libbpf_sys::bpf_program__name(self.ptr.as_ptr()) };
        let name_c_str = unsafe { CStr::from_ptr(name_ptr) };
        // SAFETY: `bpf_program__name` always returns a non-NULL pointer.
        OsStr::from_bytes(name_c_str.to_bytes())
    }

    /// Retrieve the name of the section this `OpenProgram` belongs to.
    pub fn section(&self) -> &OsStr {
        // SAFETY: The program is always valid.
        let p = unsafe { libbpf_sys::bpf_program__section_name(self.ptr.as_ptr()) };
        // SAFETY: `bpf_program__section_name` will always return a non-NULL
        //         pointer.
        let section_c_str = unsafe { CStr::from_ptr(p) };
        let section = OsStr::from_bytes(section_c_str.to_bytes());
        section
    }

    /// Returns the number of instructions that form the program.
    ///
    /// Note: Keep in mind, libbpf can modify the program's instructions
    /// and consequently its instruction count, as it processes the BPF object file.
    /// So [`OpenProgram::insn_cnt`] and [`Program::insn_cnt`] may return different values.
    pub fn insn_cnt(&self) -> usize {
        unsafe { libbpf_sys::bpf_program__insn_cnt(self.ptr.as_ptr()) as usize }
    }

    /// Gives read-only access to BPF program's underlying BPF instructions.
    ///
    /// Keep in mind, libbpf can modify and append/delete BPF program's
    /// instructions as it processes BPF object file and prepares everything for
    /// uploading into the kernel. So [`OpenProgram::insns`] and [`Program::insns`] may return
    /// different sets of instructions. As an example, during BPF object load phase BPF program
    /// instructions will be CO-RE-relocated, BPF subprograms instructions will be appended, ldimm64
    /// instructions will have FDs embedded, etc. So instructions returned before load and after it
    /// might be quite different.
    pub fn insns(&self) -> &[libbpf_sys::bpf_insn] {
        let count = self.insn_cnt();
        let ptr = unsafe { libbpf_sys::bpf_program__insns(self.ptr.as_ptr()) };
        unsafe { slice::from_raw_parts(ptr, count) }
    }
}

impl<'obj> OpenProgramMut<'obj> {
    /// Create a new [`OpenProgram`] from a ptr to a `libbpf_sys::bpf_program`.
    pub fn new_mut(prog: &'obj mut libbpf_sys::bpf_program) -> Self {
        Self {
            ptr: unsafe { NonNull::new_unchecked(prog as *mut _) },
            _phantom: PhantomData,
        }
    }

    pub fn set_prog_type(&mut self, prog_type: ProgramType) {
        let rc = unsafe { libbpf_sys::bpf_program__set_type(self.ptr.as_ptr(), prog_type as u32) };
        debug_assert!(util::parse_ret(rc).is_ok(), "{rc}");
    }

    pub fn set_attach_type(&mut self, attach_type: ProgramAttachType) {
        let rc = unsafe {
            libbpf_sys::bpf_program__set_expected_attach_type(self.ptr.as_ptr(), attach_type as u32)
        };
        debug_assert!(util::parse_ret(rc).is_ok(), "{rc}");
    }

    pub fn set_ifindex(&mut self, idx: u32) {
        unsafe { libbpf_sys::bpf_program__set_ifindex(self.ptr.as_ptr(), idx) }
    }

    /// Set the log level for the bpf program.
    ///
    /// The log level is interpreted by bpf kernel code and interpretation may
    /// change with newer kernel versions. Refer to the kernel source code for
    /// details.
    ///
    /// In general, a value of `0` disables logging while values `> 0` enables
    /// it.
    pub fn set_log_level(&mut self, log_level: u32) {
        let rc = unsafe { libbpf_sys::bpf_program__set_log_level(self.ptr.as_ptr(), log_level) };
        debug_assert!(util::parse_ret(rc).is_ok(), "{rc}");
    }

    /// Set whether a bpf program should be automatically loaded by default
    /// when the bpf object is loaded.
    pub fn set_autoload(&mut self, autoload: bool) {
        let rc = unsafe { libbpf_sys::bpf_program__set_autoload(self.ptr.as_ptr(), autoload) };
        debug_assert!(util::parse_ret(rc).is_ok(), "{rc}");
    }

    pub fn set_attach_target(
        &mut self,
        attach_prog_fd: i32,
        attach_func_name: Option<String>,
    ) -> Result<()> {
        let ret = if let Some(name) = attach_func_name {
            // NB: we must hold onto a CString otherwise our pointer dangles
            let name_c = util::str_to_cstring(&name)?;
            unsafe {
                libbpf_sys::bpf_program__set_attach_target(
                    self.ptr.as_ptr(),
                    attach_prog_fd,
                    name_c.as_ptr(),
                )
            }
        } else {
            unsafe {
                libbpf_sys::bpf_program__set_attach_target(
                    self.ptr.as_ptr(),
                    attach_prog_fd,
                    ptr::null(),
                )
            }
        };
        util::parse_ret(ret)
    }

    pub fn set_flags(&mut self, flags: u32) {
        let rc = unsafe { libbpf_sys::bpf_program__set_flags(self.ptr.as_ptr(), flags) };
        debug_assert!(util::parse_ret(rc).is_ok(), "{rc}");
    }
}

impl<'obj> Deref for OpenProgramMut<'obj> {
    type Target = OpenProgram<'obj>;

    fn deref(&self) -> &Self::Target {
        // SAFETY: `OpenProgramImpl` is `repr(transparent)` and so
        //         in-memory representation of both types is the same.
        unsafe { transmute::<&OpenProgramMut<'obj>, &OpenProgram<'obj>>(self) }
    }
}

impl<T> AsRawLibbpf for OpenProgramImpl<'_, T> {
    type LibbpfType = libbpf_sys::bpf_program;

    /// Retrieve the underlying [`libbpf_sys::bpf_program`].
    fn as_libbpf_object(&self) -> NonNull<Self::LibbpfType> {
        self.ptr
    }
}

/// Type of a [`Program`]. Maps to `enum bpf_prog_type` in kernel uapi.
#[non_exhaustive]
#[repr(u32)]
#[derive(Copy, Clone, Debug)]
// TODO: Document variants.
#[allow(missing_docs)]
pub enum ProgramType {
    Unspec = 0,
    SocketFilter,
    Kprobe,
    SchedCls,
    SchedAct,
    Tracepoint,
    Xdp,
    PerfEvent,
    CgroupSkb,
    CgroupSock,
    LwtIn,
    LwtOut,
    LwtXmit,
    SockOps,
    SkSkb,
    CgroupDevice,
    SkMsg,
    RawTracepoint,
    CgroupSockAddr,
    LwtSeg6local,
    LircMode2,
    SkReuseport,
    FlowDissector,
    CgroupSysctl,
    RawTracepointWritable,
    CgroupSockopt,
    Tracing,
    StructOps,
    Ext,
    Lsm,
    SkLookup,
    Syscall,
    /// See [`MapType::Unknown`][crate::MapType::Unknown]
    Unknown = u32::MAX,
}

impl ProgramType {
    /// Detects if host kernel supports this BPF program type
    ///
    /// Make sure the process has required set of CAP_* permissions (or runs as
    /// root) when performing feature checking.
    pub fn is_supported(&self) -> Result<bool> {
        let ret = unsafe { libbpf_sys::libbpf_probe_bpf_prog_type(*self as u32, ptr::null()) };
        match ret {
            0 => Ok(false),
            1 => Ok(true),
            _ => Err(Error::from_raw_os_error(-ret)),
        }
    }

    /// Detects if host kernel supports the use of a given BPF helper from this BPF program type.
    /// * `helper_id` - BPF helper ID (enum bpf_func_id) to check support for
    ///
    /// Make sure the process has required set of CAP_* permissions (or runs as
    /// root) when performing feature checking.
    pub fn is_helper_supported(&self, helper_id: bpf_func_id) -> Result<bool> {
        let ret =
            unsafe { libbpf_sys::libbpf_probe_bpf_helper(*self as u32, helper_id, ptr::null()) };
        match ret {
            0 => Ok(false),
            1 => Ok(true),
            _ => Err(Error::from_raw_os_error(-ret)),
        }
    }
}

impl From<u32> for ProgramType {
    fn from(value: u32) -> Self {
        use ProgramType::*;

        match value {
            x if x == Unspec as u32 => Unspec,
            x if x == SocketFilter as u32 => SocketFilter,
            x if x == Kprobe as u32 => Kprobe,
            x if x == SchedCls as u32 => SchedCls,
            x if x == SchedAct as u32 => SchedAct,
            x if x == Tracepoint as u32 => Tracepoint,
            x if x == Xdp as u32 => Xdp,
            x if x == PerfEvent as u32 => PerfEvent,
            x if x == CgroupSkb as u32 => CgroupSkb,
            x if x == CgroupSock as u32 => CgroupSock,
            x if x == LwtIn as u32 => LwtIn,
            x if x == LwtOut as u32 => LwtOut,
            x if x == LwtXmit as u32 => LwtXmit,
            x if x == SockOps as u32 => SockOps,
            x if x == SkSkb as u32 => SkSkb,
            x if x == CgroupDevice as u32 => CgroupDevice,
            x if x == SkMsg as u32 => SkMsg,
            x if x == RawTracepoint as u32 => RawTracepoint,
            x if x == CgroupSockAddr as u32 => CgroupSockAddr,
            x if x == LwtSeg6local as u32 => LwtSeg6local,
            x if x == LircMode2 as u32 => LircMode2,
            x if x == SkReuseport as u32 => SkReuseport,
            x if x == FlowDissector as u32 => FlowDissector,
            x if x == CgroupSysctl as u32 => CgroupSysctl,
            x if x == RawTracepointWritable as u32 => RawTracepointWritable,
            x if x == CgroupSockopt as u32 => CgroupSockopt,
            x if x == Tracing as u32 => Tracing,
            x if x == StructOps as u32 => StructOps,
            x if x == Ext as u32 => Ext,
            x if x == Lsm as u32 => Lsm,
            x if x == SkLookup as u32 => SkLookup,
            x if x == Syscall as u32 => Syscall,
            _ => Unknown,
        }
    }
}

/// Attach type of a [`Program`]. Maps to `enum bpf_attach_type` in kernel uapi.
#[non_exhaustive]
#[repr(u32)]
#[derive(Clone, Debug)]
// TODO: Document variants.
#[allow(missing_docs)]
pub enum ProgramAttachType {
    CgroupInetIngress,
    CgroupInetEgress,
    CgroupInetSockCreate,
    CgroupSockOps,
    SkSkbStreamParser,
    SkSkbStreamVerdict,
    CgroupDevice,
    SkMsgVerdict,
    CgroupInet4Bind,
    CgroupInet6Bind,
    CgroupInet4Connect,
    CgroupInet6Connect,
    CgroupInet4PostBind,
    CgroupInet6PostBind,
    CgroupUdp4Sendmsg,
    CgroupUdp6Sendmsg,
    LircMode2,
    FlowDissector,
    CgroupSysctl,
    CgroupUdp4Recvmsg,
    CgroupUdp6Recvmsg,
    CgroupGetsockopt,
    CgroupSetsockopt,
    TraceRawTp,
    TraceFentry,
    TraceFexit,
    ModifyReturn,
    LsmMac,
    TraceIter,
    CgroupInet4Getpeername,
    CgroupInet6Getpeername,
    CgroupInet4Getsockname,
    CgroupInet6Getsockname,
    XdpDevmap,
    CgroupInetSockRelease,
    XdpCpumap,
    SkLookup,
    Xdp,
    SkSkbVerdict,
    SkReuseportSelect,
    SkReuseportSelectOrMigrate,
    PerfEvent,
    /// See [`MapType::Unknown`][crate::MapType::Unknown]
    Unknown = u32::MAX,
}

impl From<u32> for ProgramAttachType {
    fn from(value: u32) -> Self {
        use ProgramAttachType::*;

        match value {
            x if x == CgroupInetIngress as u32 => CgroupInetIngress,
            x if x == CgroupInetEgress as u32 => CgroupInetEgress,
            x if x == CgroupInetSockCreate as u32 => CgroupInetSockCreate,
            x if x == CgroupSockOps as u32 => CgroupSockOps,
            x if x == SkSkbStreamParser as u32 => SkSkbStreamParser,
            x if x == SkSkbStreamVerdict as u32 => SkSkbStreamVerdict,
            x if x == CgroupDevice as u32 => CgroupDevice,
            x if x == SkMsgVerdict as u32 => SkMsgVerdict,
            x if x == CgroupInet4Bind as u32 => CgroupInet4Bind,
            x if x == CgroupInet6Bind as u32 => CgroupInet6Bind,
            x if x == CgroupInet4Connect as u32 => CgroupInet4Connect,
            x if x == CgroupInet6Connect as u32 => CgroupInet6Connect,
            x if x == CgroupInet4PostBind as u32 => CgroupInet4PostBind,
            x if x == CgroupInet6PostBind as u32 => CgroupInet6PostBind,
            x if x == CgroupUdp4Sendmsg as u32 => CgroupUdp4Sendmsg,
            x if x == CgroupUdp6Sendmsg as u32 => CgroupUdp6Sendmsg,
            x if x == LircMode2 as u32 => LircMode2,
            x if x == FlowDissector as u32 => FlowDissector,
            x if x == CgroupSysctl as u32 => CgroupSysctl,
            x if x == CgroupUdp4Recvmsg as u32 => CgroupUdp4Recvmsg,
            x if x == CgroupUdp6Recvmsg as u32 => CgroupUdp6Recvmsg,
            x if x == CgroupGetsockopt as u32 => CgroupGetsockopt,
            x if x == CgroupSetsockopt as u32 => CgroupSetsockopt,
            x if x == TraceRawTp as u32 => TraceRawTp,
            x if x == TraceFentry as u32 => TraceFentry,
            x if x == TraceFexit as u32 => TraceFexit,
            x if x == ModifyReturn as u32 => ModifyReturn,
            x if x == LsmMac as u32 => LsmMac,
            x if x == TraceIter as u32 => TraceIter,
            x if x == CgroupInet4Getpeername as u32 => CgroupInet4Getpeername,
            x if x == CgroupInet6Getpeername as u32 => CgroupInet6Getpeername,
            x if x == CgroupInet4Getsockname as u32 => CgroupInet4Getsockname,
            x if x == CgroupInet6Getsockname as u32 => CgroupInet6Getsockname,
            x if x == XdpDevmap as u32 => XdpDevmap,
            x if x == CgroupInetSockRelease as u32 => CgroupInetSockRelease,
            x if x == XdpCpumap as u32 => XdpCpumap,
            x if x == SkLookup as u32 => SkLookup,
            x if x == Xdp as u32 => Xdp,
            x if x == SkSkbVerdict as u32 => SkSkbVerdict,
            x if x == SkReuseportSelect as u32 => SkReuseportSelect,
            x if x == SkReuseportSelectOrMigrate as u32 => SkReuseportSelectOrMigrate,
            x if x == PerfEvent as u32 => PerfEvent,
            _ => Unknown,
        }
    }
}

/// The input a program accepts.
///
/// This type is mostly used in conjunction with the [`Program::test_run`]
/// facility.
#[derive(Debug, Default)]
pub struct Input<'dat> {
    /// The input context to provide.
    ///
    /// The input is mutable because the kernel may modify it.
    pub context_in: Option<&'dat mut [u8]>,
    /// The output context buffer provided to the program.
    pub context_out: Option<&'dat mut [u8]>,
    /// Additional data to provide to the program.
    pub data_in: Option<&'dat [u8]>,
    /// The output data buffer provided to the program.
    pub data_out: Option<&'dat mut [u8]>,
    /// The 'cpu' value passed to the kernel.
    pub cpu: u32,
    /// The 'flags' value passed to the kernel.
    pub flags: u32,
    /// The struct is non-exhaustive and open to extension.
    #[doc(hidden)]
    pub _non_exhaustive: (),
}

/// The output a program produces.
///
/// This type is mostly used in conjunction with the [`Program::test_run`]
/// facility.
#[derive(Debug)]
pub struct Output<'dat> {
    /// The value returned by the program.
    pub return_value: u32,
    /// The output context filled by the program/kernel.
    pub context: Option<&'dat mut [u8]>,
    /// Output data filled by the program.
    pub data: Option<&'dat mut [u8]>,
    /// The struct is non-exhaustive and open to extension.
    #[doc(hidden)]
    pub _non_exhaustive: (),
}

/// An immutable loaded BPF program.
pub type Program<'obj> = ProgramImpl<'obj>;
/// A mutable loaded BPF program.
pub type ProgramMut<'obj> = ProgramImpl<'obj, Mut>;


/// Represents a loaded [`Program`].
///
/// This struct is not safe to clone because the underlying libbpf resource cannot currently
/// be protected from data races.
///
/// If you attempt to attach a `Program` with the wrong attach method, the `attach_*`
/// method will fail with the appropriate error.
#[derive(Debug)]
#[repr(transparent)]
pub struct ProgramImpl<'obj, T = ()> {
    pub(crate) ptr: NonNull<libbpf_sys::bpf_program>,
    _phantom: PhantomData<&'obj T>,
}

impl<'obj> Program<'obj> {
    /// Create a [`Program`] from a [`libbpf_sys::bpf_program`]
    pub fn new(prog: &'obj libbpf_sys::bpf_program) -> Self {
        // SAFETY: We inferred the address from a reference, which is always
        //         valid.
        Self {
            ptr: unsafe { NonNull::new_unchecked(prog as *const _ as *mut _) },
            _phantom: PhantomData,
        }
    }

    /// Retrieve the name of this `Program`.
    pub fn name(&self) -> &OsStr {
        let name_ptr = unsafe { libbpf_sys::bpf_program__name(self.ptr.as_ptr()) };
        let name_c_str = unsafe { CStr::from_ptr(name_ptr) };
        // SAFETY: `bpf_program__name` always returns a non-NULL pointer.
        OsStr::from_bytes(name_c_str.to_bytes())
    }

    /// Retrieve the name of the section this `Program` belongs to.
    pub fn section(&self) -> &OsStr {
        // SAFETY: The program is always valid.
        let p = unsafe { libbpf_sys::bpf_program__section_name(self.ptr.as_ptr()) };
        // SAFETY: `bpf_program__section_name` will always return a non-NULL
        //         pointer.
        let section_c_str = unsafe { CStr::from_ptr(p) };
        let section = OsStr::from_bytes(section_c_str.to_bytes());
        section
    }

    /// Retrieve the type of the program.
    pub fn prog_type(&self) -> ProgramType {
        ProgramType::from(unsafe { libbpf_sys::bpf_program__type(self.ptr.as_ptr()) })
    }

    /// Returns program fd by id
    pub fn get_fd_by_id(id: u32) -> Result<OwnedFd> {
        let ret = unsafe { libbpf_sys::bpf_prog_get_fd_by_id(id) };
        let fd = util::parse_ret_i32(ret)?;
        // SAFETY
        // A file descriptor coming from the bpf_prog_get_fd_by_id function is always suitable for
        // ownership and can be cleaned up with close.
        Ok(unsafe { OwnedFd::from_raw_fd(fd) })
    }

    /// Returns program id by fd
    pub fn get_id_by_fd(fd: BorrowedFd<'_>) -> Result<u32> {
        let mut prog_info = libbpf_sys::bpf_prog_info::default();
        let prog_info_ptr: *mut libbpf_sys::bpf_prog_info = &mut prog_info;
        let mut len = size_of::<libbpf_sys::bpf_prog_info>() as u32;
        let ret = unsafe {
            libbpf_sys::bpf_obj_get_info_by_fd(
                fd.as_raw_fd(),
                prog_info_ptr as *mut c_void,
                &mut len,
            )
        };
        util::parse_ret(ret)?;
        Ok(prog_info.id)
    }

    /// Returns flags that have been set for the program.
    pub fn flags(&self) -> u32 {
        unsafe { libbpf_sys::bpf_program__flags(self.ptr.as_ptr()) }
    }

    /// Retrieve the attach type of the program.
    pub fn attach_type(&self) -> ProgramAttachType {
        ProgramAttachType::from(unsafe {
            libbpf_sys::bpf_program__expected_attach_type(self.ptr.as_ptr())
        })
    }

    /// Return `true` if the bpf program is set to autoload, `false` otherwise.
    pub fn autoload(&self) -> bool {
        unsafe { libbpf_sys::bpf_program__autoload(self.ptr.as_ptr()) }
    }

    /// Return the bpf program's log level.
    pub fn log_level(&self) -> u32 {
        unsafe { libbpf_sys::bpf_program__log_level(self.ptr.as_ptr()) }
    }

    /// Returns the number of instructions that form the program.
    ///
    /// Please see note in [`OpenProgram::insn_cnt`].
    pub fn insn_cnt(&self) -> usize {
        unsafe { libbpf_sys::bpf_program__insn_cnt(self.ptr.as_ptr()) as usize }
    }

    /// Gives read-only access to BPF program's underlying BPF instructions.
    ///
    /// Please see note in [`OpenProgram::insns`].
    pub fn insns(&self) -> &[libbpf_sys::bpf_insn] {
        let count = self.insn_cnt();
        let ptr = unsafe { libbpf_sys::bpf_program__insns(self.ptr.as_ptr()) };
        unsafe { slice::from_raw_parts(ptr, count) }
    }
}

impl<'obj> ProgramMut<'obj> {
    /// Create a [`Program`] from a [`libbpf_sys::bpf_program`]
    pub fn new_mut(prog: &'obj mut libbpf_sys::bpf_program) -> Self {
        Self {
            ptr: unsafe { NonNull::new_unchecked(prog as *mut _) },
            _phantom: PhantomData,
        }
    }

    /// [Pin](https://facebookmicrosites.github.io/bpf/blog/2018/08/31/object-lifetime.html#bpffs)
    /// this program to bpffs.
    pub fn pin<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
        let path_c = util::path_to_cstring(path)?;
        let path_ptr = path_c.as_ptr();

        let ret = unsafe { libbpf_sys::bpf_program__pin(self.ptr.as_ptr(), path_ptr) };
        util::parse_ret(ret)
    }

    /// [Unpin](https://facebookmicrosites.github.io/bpf/blog/2018/08/31/object-lifetime.html#bpffs)
    /// this program from bpffs
    pub fn unpin<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
        let path_c = util::path_to_cstring(path)?;
        let path_ptr = path_c.as_ptr();

        let ret = unsafe { libbpf_sys::bpf_program__unpin(self.ptr.as_ptr(), path_ptr) };
        util::parse_ret(ret)
    }

    /// Auto-attach based on prog section
    pub fn attach(&mut self) -> Result<Link> {
        let ptr = unsafe { libbpf_sys::bpf_program__attach(self.ptr.as_ptr()) };
        let ptr = validate_bpf_ret(ptr).context("failed to attach BPF program")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach this program to a
    /// [cgroup](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html).
    pub fn attach_cgroup(&mut self, cgroup_fd: i32) -> Result<Link> {
        let ptr = unsafe { libbpf_sys::bpf_program__attach_cgroup(self.ptr.as_ptr(), cgroup_fd) };
        let ptr = validate_bpf_ret(ptr).context("failed to attach cgroup")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach this program to a [perf event](https://linux.die.net/man/2/perf_event_open).
    pub fn attach_perf_event(&mut self, pfd: i32) -> Result<Link> {
        let ptr = unsafe { libbpf_sys::bpf_program__attach_perf_event(self.ptr.as_ptr(), pfd) };
        let ptr = validate_bpf_ret(ptr).context("failed to attach perf event")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach this program to a [userspace
    /// probe](https://www.kernel.org/doc/html/latest/trace/uprobetracer.html).
    pub fn attach_uprobe<T: AsRef<Path>>(
        &mut self,
        retprobe: bool,
        pid: i32,
        binary_path: T,
        func_offset: usize,
    ) -> Result<Link> {
        let path = util::path_to_cstring(binary_path)?;
        let path_ptr = path.as_ptr();
        let ptr = unsafe {
            libbpf_sys::bpf_program__attach_uprobe(
                self.ptr.as_ptr(),
                retprobe,
                pid,
                path_ptr,
                func_offset as libbpf_sys::size_t,
            )
        };
        let ptr = validate_bpf_ret(ptr).context("failed to attach uprobe")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach this program to a [userspace
    /// probe](https://www.kernel.org/doc/html/latest/trace/uprobetracer.html),
    /// providing additional options.
    pub fn attach_uprobe_with_opts(
        &mut self,
        pid: i32,
        binary_path: impl AsRef<Path>,
        func_offset: usize,
        opts: UprobeOpts,
    ) -> Result<Link> {
        let path = util::path_to_cstring(binary_path)?;
        let path_ptr = path.as_ptr();
        let UprobeOpts {
            ref_ctr_offset,
            cookie,
            retprobe,
            func_name,
            _non_exhaustive,
        } = opts;

        let func_name = util::str_to_cstring(&func_name)?;
        let opts = libbpf_sys::bpf_uprobe_opts {
            sz: size_of::<libbpf_sys::bpf_uprobe_opts>() as _,
            ref_ctr_offset: ref_ctr_offset as libbpf_sys::size_t,
            bpf_cookie: cookie,
            retprobe,
            func_name: func_name.as_ptr(),
            ..Default::default()
        };

        let ptr = unsafe {
            libbpf_sys::bpf_program__attach_uprobe_opts(
                self.ptr.as_ptr(),
                pid,
                path_ptr,
                func_offset as libbpf_sys::size_t,
                &opts as *const _,
            )
        };
        let ptr = validate_bpf_ret(ptr).context("failed to attach uprobe")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach this program to a [kernel
    /// probe](https://www.kernel.org/doc/html/latest/trace/kprobetrace.html).
    pub fn attach_kprobe<T: AsRef<str>>(&mut self, retprobe: bool, func_name: T) -> Result<Link> {
        let func_name = util::str_to_cstring(func_name.as_ref())?;
        let func_name_ptr = func_name.as_ptr();
        let ptr = unsafe {
            libbpf_sys::bpf_program__attach_kprobe(self.ptr.as_ptr(), retprobe, func_name_ptr)
        };
        let ptr = validate_bpf_ret(ptr).context("failed to attach kprobe")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach this program to the specified syscall
    pub fn attach_ksyscall<T: AsRef<str>>(
        &mut self,
        retprobe: bool,
        syscall_name: T,
    ) -> Result<Link> {
        let opts = libbpf_sys::bpf_ksyscall_opts {
            sz: size_of::<libbpf_sys::bpf_ksyscall_opts>() as _,
            retprobe,
            ..Default::default()
        };

        let syscall_name = util::str_to_cstring(syscall_name.as_ref())?;
        let syscall_name_ptr = syscall_name.as_ptr();
        let ptr = unsafe {
            libbpf_sys::bpf_program__attach_ksyscall(self.ptr.as_ptr(), syscall_name_ptr, &opts)
        };
        let ptr = validate_bpf_ret(ptr).context("failed to attach ksyscall")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    fn attach_tracepoint_impl(
        &mut self,
        tp_category: &str,
        tp_name: &str,
        tp_opts: Option<TracepointOpts>,
    ) -> Result<Link> {
        let tp_category = util::str_to_cstring(tp_category)?;
        let tp_category_ptr = tp_category.as_ptr();
        let tp_name = util::str_to_cstring(tp_name)?;
        let tp_name_ptr = tp_name.as_ptr();

        let ptr = if let Some(tp_opts) = tp_opts {
            let tp_opts = libbpf_sys::bpf_tracepoint_opts::from(tp_opts);
            unsafe {
                libbpf_sys::bpf_program__attach_tracepoint_opts(
                    self.ptr.as_ptr(),
                    tp_category_ptr,
                    tp_name_ptr,
                    &tp_opts as *const _,
                )
            }
        } else {
            unsafe {
                libbpf_sys::bpf_program__attach_tracepoint(
                    self.ptr.as_ptr(),
                    tp_category_ptr,
                    tp_name_ptr,
                )
            }
        };

        let ptr = validate_bpf_ret(ptr).context("failed to attach tracepoint")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach this program to a [kernel
    /// tracepoint](https://www.kernel.org/doc/html/latest/trace/tracepoints.html).
    pub fn attach_tracepoint(
        &mut self,
        tp_category: impl AsRef<str>,
        tp_name: impl AsRef<str>,
    ) -> Result<Link> {
        self.attach_tracepoint_impl(tp_category.as_ref(), tp_name.as_ref(), None)
    }

    /// Attach this program to a [kernel
    /// tracepoint](https://www.kernel.org/doc/html/latest/trace/tracepoints.html),
    /// providing additional options.
    pub fn attach_tracepoint_with_opts(
        &mut self,
        tp_category: impl AsRef<str>,
        tp_name: impl AsRef<str>,
        tp_opts: TracepointOpts,
    ) -> Result<Link> {
        self.attach_tracepoint_impl(tp_category.as_ref(), tp_name.as_ref(), Some(tp_opts))
    }

    /// Attach this program to a [raw kernel
    /// tracepoint](https://lwn.net/Articles/748352/).
    pub fn attach_raw_tracepoint<T: AsRef<str>>(&mut self, tp_name: T) -> Result<Link> {
        let tp_name = util::str_to_cstring(tp_name.as_ref())?;
        let tp_name_ptr = tp_name.as_ptr();
        let ptr = unsafe {
            libbpf_sys::bpf_program__attach_raw_tracepoint(self.ptr.as_ptr(), tp_name_ptr)
        };
        let ptr = validate_bpf_ret(ptr).context("failed to attach raw tracepoint")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach to an [LSM](https://en.wikipedia.org/wiki/Linux_Security_Modules) hook
    pub fn attach_lsm(&mut self) -> Result<Link> {
        let ptr = unsafe { libbpf_sys::bpf_program__attach_lsm(self.ptr.as_ptr()) };
        let ptr = validate_bpf_ret(ptr).context("failed to attach LSM")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach to a [fentry/fexit kernel probe](https://lwn.net/Articles/801479/)
    pub fn attach_trace(&mut self) -> Result<Link> {
        let ptr = unsafe { libbpf_sys::bpf_program__attach_trace(self.ptr.as_ptr()) };
        let ptr = validate_bpf_ret(ptr).context("failed to attach fentry/fexit kernel probe")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach a verdict/parser to a [sockmap/sockhash](https://lwn.net/Articles/731133/)
    pub fn attach_sockmap(&self, map_fd: i32) -> Result<()> {
        let err = unsafe {
            libbpf_sys::bpf_prog_attach(
                self.as_fd().as_raw_fd(),
                map_fd,
                self.attach_type() as u32,
                0,
            )
        };
        util::parse_ret(err)
    }

    /// Attach this program to [XDP](https://lwn.net/Articles/825998/)
    pub fn attach_xdp(&mut self, ifindex: i32) -> Result<Link> {
        let ptr = unsafe { libbpf_sys::bpf_program__attach_xdp(self.ptr.as_ptr(), ifindex) };
        let ptr = validate_bpf_ret(ptr).context("failed to attach XDP program")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach this program to [netns-based programs](https://lwn.net/Articles/819618/)
    pub fn attach_netns(&mut self, netns_fd: i32) -> Result<Link> {
        let ptr = unsafe { libbpf_sys::bpf_program__attach_netns(self.ptr.as_ptr(), netns_fd) };
        let ptr = validate_bpf_ret(ptr).context("failed to attach network namespace program")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    fn attach_usdt_impl(
        &mut self,
        pid: i32,
        binary_path: &Path,
        usdt_provider: &str,
        usdt_name: &str,
        usdt_opts: Option<UsdtOpts>,
    ) -> Result<Link> {
        let path = util::path_to_cstring(binary_path)?;
        let path_ptr = path.as_ptr();
        let usdt_provider = util::str_to_cstring(usdt_provider)?;
        let usdt_provider_ptr = usdt_provider.as_ptr();
        let usdt_name = util::str_to_cstring(usdt_name)?;
        let usdt_name_ptr = usdt_name.as_ptr();
        let usdt_opts = usdt_opts.map(libbpf_sys::bpf_usdt_opts::from);
        let usdt_opts_ptr = usdt_opts
            .as_ref()
            .map(|opts| opts as *const _)
            .unwrap_or_else(ptr::null);

        let ptr = unsafe {
            libbpf_sys::bpf_program__attach_usdt(
                self.ptr.as_ptr(),
                pid,
                path_ptr,
                usdt_provider_ptr,
                usdt_name_ptr,
                usdt_opts_ptr,
            )
        };
        let ptr = validate_bpf_ret(ptr).context("failed to attach USDT")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Attach this program to a [USDT](https://lwn.net/Articles/753601/) probe
    /// point. The entry point of the program must be defined with
    /// `SEC("usdt")`.
    pub fn attach_usdt(
        &mut self,
        pid: i32,
        binary_path: impl AsRef<Path>,
        usdt_provider: impl AsRef<str>,
        usdt_name: impl AsRef<str>,
    ) -> Result<Link> {
        self.attach_usdt_impl(
            pid,
            binary_path.as_ref(),
            usdt_provider.as_ref(),
            usdt_name.as_ref(),
            None,
        )
    }

    /// Attach this program to a [USDT](https://lwn.net/Articles/753601/) probe
    /// point, providing additional options. The entry point of the program must
    /// be defined with `SEC("usdt")`.
    pub fn attach_usdt_with_opts(
        &mut self,
        pid: i32,
        binary_path: impl AsRef<Path>,
        usdt_provider: impl AsRef<str>,
        usdt_name: impl AsRef<str>,
        usdt_opts: UsdtOpts,
    ) -> Result<Link> {
        self.attach_usdt_impl(
            pid,
            binary_path.as_ref(),
            usdt_provider.as_ref(),
            usdt_name.as_ref(),
            Some(usdt_opts),
        )
    }

    /// Attach this program to a
    /// [BPF Iterator](https://www.kernel.org/doc/html/latest/bpf/bpf_iterators.html).
    /// The entry point of the program must be defined with `SEC("iter")` or `SEC("iter.s")`.
    pub fn attach_iter(&mut self, map_fd: BorrowedFd<'_>) -> Result<Link> {
        let mut linkinfo = libbpf_sys::bpf_iter_link_info::default();
        linkinfo.map.map_fd = map_fd.as_raw_fd() as _;
        let attach_opt = libbpf_sys::bpf_iter_attach_opts {
            link_info: &mut linkinfo as *mut libbpf_sys::bpf_iter_link_info,
            link_info_len: size_of::<libbpf_sys::bpf_iter_link_info>() as _,
            sz: size_of::<libbpf_sys::bpf_iter_attach_opts>() as _,
            ..Default::default()
        };
        let ptr = unsafe {
            libbpf_sys::bpf_program__attach_iter(
                self.ptr.as_ptr(),
                &attach_opt as *const libbpf_sys::bpf_iter_attach_opts,
            )
        };

        let ptr = validate_bpf_ret(ptr).context("failed to attach iterator")?;
        // SAFETY: the pointer came from libbpf and has been checked for errors.
        let link = unsafe { Link::new(ptr) };
        Ok(link)
    }

    /// Test run the program with the given input data.
    ///
    /// This function uses the
    /// [BPF_PROG_RUN](https://www.kernel.org/doc/html/latest/bpf/bpf_prog_run.html)
    /// facility.
    pub fn test_run<'dat>(&mut self, input: Input<'dat>) -> Result<Output<'dat>> {
        unsafe fn slice_from_array<'t, T>(items: *mut T, num_items: usize) -> Option<&'t mut [T]> {
            if items.is_null() {
                None
            } else {
                Some(unsafe { slice::from_raw_parts_mut(items, num_items) })
            }
        }

        let Input {
            context_in,
            mut context_out,
            data_in,
            mut data_out,
            cpu,
            flags,
            _non_exhaustive: (),
        } = input;

        let mut opts = unsafe { mem::zeroed::<libbpf_sys::bpf_test_run_opts>() };
        opts.sz = size_of_val(&opts) as _;
        opts.ctx_in = context_in
            .as_ref()
            .map(|data| data.as_ptr().cast())
            .unwrap_or_else(ptr::null);
        opts.ctx_size_in = context_in.map(|data| data.len() as _).unwrap_or(0);
        opts.ctx_out = context_out
            .as_mut()
            .map(|data| data.as_mut_ptr().cast())
            .unwrap_or_else(ptr::null_mut);
        opts.ctx_size_out = context_out.map(|data| data.len() as _).unwrap_or(0);
        opts.data_in = data_in
            .map(|data| data.as_ptr().cast())
            .unwrap_or_else(ptr::null);
        opts.data_size_in = data_in.map(|data| data.len() as _).unwrap_or(0);
        opts.data_out = data_out
            .as_mut()
            .map(|data| data.as_mut_ptr().cast())
            .unwrap_or_else(ptr::null_mut);
        opts.data_size_out = data_out.map(|data| data.len() as _).unwrap_or(0);
        opts.cpu = cpu;
        opts.flags = flags;

        let rc = unsafe { libbpf_sys::bpf_prog_test_run_opts(self.as_fd().as_raw_fd(), &mut opts) };
        let () = util::parse_ret(rc)?;
        let output = Output {
            return_value: opts.retval,
            context: unsafe { slice_from_array(opts.ctx_out.cast(), opts.ctx_size_out as _) },
            data: unsafe { slice_from_array(opts.data_out.cast(), opts.data_size_out as _) },
            _non_exhaustive: (),
        };
        Ok(output)
    }
}

impl<'obj> Deref for ProgramMut<'obj> {
    type Target = Program<'obj>;

    fn deref(&self) -> &Self::Target {
        // SAFETY: `ProgramImpl` is `repr(transparent)` and so in-memory
        //         representation of both types is the same.
        unsafe { transmute::<&ProgramMut<'obj>, &Program<'obj>>(self) }
    }
}

impl<T> AsFd for ProgramImpl<'_, T> {
    fn as_fd(&self) -> BorrowedFd<'_> {
        let fd = unsafe { libbpf_sys::bpf_program__fd(self.ptr.as_ptr()) };
        unsafe { BorrowedFd::borrow_raw(fd) }
    }
}

impl<T> AsRawLibbpf for ProgramImpl<'_, T> {
    type LibbpfType = libbpf_sys::bpf_program;

    /// Retrieve the underlying [`libbpf_sys::bpf_program`].
    fn as_libbpf_object(&self) -> NonNull<Self::LibbpfType> {
        self.ptr
    }
}

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

    use std::mem::discriminant;

    #[test]
    fn program_type() {
        use ProgramType::*;

        for t in [
            Unspec,
            SocketFilter,
            Kprobe,
            SchedCls,
            SchedAct,
            Tracepoint,
            Xdp,
            PerfEvent,
            CgroupSkb,
            CgroupSock,
            LwtIn,
            LwtOut,
            LwtXmit,
            SockOps,
            SkSkb,
            CgroupDevice,
            SkMsg,
            RawTracepoint,
            CgroupSockAddr,
            LwtSeg6local,
            LircMode2,
            SkReuseport,
            FlowDissector,
            CgroupSysctl,
            RawTracepointWritable,
            CgroupSockopt,
            Tracing,
            StructOps,
            Ext,
            Lsm,
            SkLookup,
            Syscall,
            Unknown,
        ] {
            // check if discriminants match after a roundtrip conversion
            assert_eq!(discriminant(&t), discriminant(&ProgramType::from(t as u32)));
        }
    }

    #[test]
    fn program_attach_type() {
        use ProgramAttachType::*;

        for t in [
            CgroupInetIngress,
            CgroupInetEgress,
            CgroupInetSockCreate,
            CgroupSockOps,
            SkSkbStreamParser,
            SkSkbStreamVerdict,
            CgroupDevice,
            SkMsgVerdict,
            CgroupInet4Bind,
            CgroupInet6Bind,
            CgroupInet4Connect,
            CgroupInet6Connect,
            CgroupInet4PostBind,
            CgroupInet6PostBind,
            CgroupUdp4Sendmsg,
            CgroupUdp6Sendmsg,
            LircMode2,
            FlowDissector,
            CgroupSysctl,
            CgroupUdp4Recvmsg,
            CgroupUdp6Recvmsg,
            CgroupGetsockopt,
            CgroupSetsockopt,
            TraceRawTp,
            TraceFentry,
            TraceFexit,
            ModifyReturn,
            LsmMac,
            TraceIter,
            CgroupInet4Getpeername,
            CgroupInet6Getpeername,
            CgroupInet4Getsockname,
            CgroupInet6Getsockname,
            XdpDevmap,
            CgroupInetSockRelease,
            XdpCpumap,
            SkLookup,
            Xdp,
            SkSkbVerdict,
            SkReuseportSelect,
            SkReuseportSelectOrMigrate,
            PerfEvent,
            Unknown,
        ] {
            // check if discriminants match after a roundtrip conversion
            assert_eq!(
                discriminant(&t),
                discriminant(&ProgramAttachType::from(t as u32))
            );
        }
    }
}