#![allow(deprecated)]
#![doc = include_str!("../README.md")]

#[cfg(not(any(
    all(any(target_os = "macos", target_os = "ios"), target_arch = "aarch64"),
    all(target_os = "macos", target_arch = "x86_64"),
    all(
        any(target_os = "linux", target_os = "android"),
        target_arch = "aarch64"
    ),
    all(target_os = "linux", target_arch = "x86_64")
)))]
compile_error!(
    "sighook only supports Apple aarch64/x86_64 (macOS), Apple aarch64 (iOS), Linux/Android aarch64, and Linux x86_64."
);

#[cfg(all(
    feature = "patch_asm",
    any(
        all(target_os = "macos", target_arch = "aarch64"),
        all(target_os = "macos", target_arch = "x86_64"),
        all(target_os = "linux", target_arch = "aarch64"),
        all(target_os = "linux", target_arch = "x86_64")
    )
))]
mod asm;
mod constants;
mod context;
mod error;
mod memory;
mod signal;
mod state;
mod trampoline;

#[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
pub use context::{HookContext, InstrumentCallback};
#[cfg(target_arch = "aarch64")]
pub use context::{HookContext, InstrumentCallback, XRegisters, XRegistersNamed};
pub use error::SigHookError;

/// Replaces one machine instruction at `address` with `new_opcode`.
///
/// The function returns the previously stored 4-byte value.
/// Use this API when you already know the exact opcode encoding for your target architecture.
///
/// - On `aarch64`, `new_opcode` is a 32-bit ARM instruction word.
/// - On `x86_64`, `new_opcode` is interpreted as little-endian bytes.
///   Trailing `NOP` (`0x90`) bytes are treated as explicit padding, so the
///   effective patch length can be from 1 to 4 bytes.
///   If the decoded current instruction is longer than the effective patch,
///   remaining bytes are filled with `NOP`.
///   If the effective patch is longer than the decoded instruction, returns
///   [`SigHookError::PatchTooLong`] and you should use [`patch_bytes`] instead.
///
/// # Example
///
/// ```rust,no_run
/// use sighook::patchcode;
///
/// let address = 0x7FFF_0000_0000u64;
/// let old = patchcode(address, 0x90C3_9090)?;
/// let _ = old;
/// # Ok::<(), sighook::SigHookError>(())
/// ```
#[cfg(any(
    target_arch = "aarch64",
    all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos"))
))]
pub fn patchcode(address: u64, new_opcode: u32) -> Result<u32, SigHookError> {
    #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
    {
        let instruction_len = memory::instruction_width(address)? as usize;
        let opcode_bytes = new_opcode.to_le_bytes();
        let patch_len = effective_x86_patch_len(&opcode_bytes);
        if patch_len > instruction_len {
            return Err(SigHookError::PatchTooLong {
                patch_len,
                instruction_len,
            });
        }

        let original4 = memory::read_bytes(address, 4)?;
        let mut opcode = [0u8; 4];
        opcode.copy_from_slice(&original4);
        let original_opcode = u32::from_le_bytes(opcode);

        let mut patch = vec![0x90u8; instruction_len];
        patch[..patch_len].copy_from_slice(&opcode_bytes[..patch_len]);
        let _ = memory::patch_bytes_public(address, &patch)?;
        unsafe {
            state::cache_original_opcode(address, original_opcode);
        }
        Ok(original_opcode)
    }

    #[cfg(target_arch = "aarch64")]
    let original = memory::patch_u32(address, new_opcode)?;

    #[cfg(target_arch = "aarch64")]
    unsafe {
        state::cache_original_opcode(address, original);
    }

    #[cfg(target_arch = "aarch64")]
    Ok(original)
}

#[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
fn effective_x86_patch_len(opcode_bytes: &[u8; 4]) -> usize {
    let mut patch_len = opcode_bytes.len();
    while patch_len > 0 && opcode_bytes[patch_len - 1] == 0x90 {
        patch_len -= 1;
    }

    patch_len.max(1)
}

/// Replaces one machine instruction at `address` from assembly text.
///
/// This API assembles `asm` for the active target, then patches one decoded
/// instruction at `address`.
///
/// Notes:
/// - Requires crate feature `patch_asm`.
/// - On `aarch64`, use ARM64 syntax (e.g. `"mul w0, w8, w9"`).
/// - On Linux `x86_64`, use GNU/AT&T syntax (e.g. `"imul %edx"`).
/// - On `x86_64`, if assembled bytes are shorter than the decoded current instruction,
///   the trailing bytes are padded with `NOP`; if longer, returns
///   [`SigHookError::PatchTooLong`] and you should use [`patch_bytes`].
///
/// Returns the original 4-byte value previously stored at `address`.
///
/// # Example
///
/// ```rust,no_run
/// # #[cfg(all(feature = "patch_asm", any(all(target_os = "macos", target_arch = "aarch64"), all(target_os = "macos", target_arch = "x86_64"), all(target_os = "linux", target_arch = "aarch64"), all(target_os = "linux", target_arch = "x86_64"))))]
/// # {
/// use sighook::patch_asm;
///
/// let address = 0x7FFF_0000_0000u64;
/// let old = patch_asm(address, "nop")?;
/// let _ = old;
/// # }
/// # Ok::<(), sighook::SigHookError>(())
/// ```
#[cfg(all(
    feature = "patch_asm",
    any(
        all(target_os = "macos", target_arch = "aarch64"),
        all(target_os = "macos", target_arch = "x86_64"),
        all(target_os = "linux", target_arch = "aarch64"),
        all(target_os = "linux", target_arch = "x86_64")
    )
))]
pub fn patch_asm(address: u64, asm: &str) -> Result<u32, SigHookError> {
    #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
    {
        use crate::asm::assemble_bytes;

        let mut patch = assemble_bytes(address, asm)?;
        let instruction_len = memory::instruction_width(address)? as usize;
        if patch.len() > instruction_len {
            return Err(SigHookError::PatchTooLong {
                patch_len: patch.len(),
                instruction_len,
            });
        }

        let original4 = memory::read_bytes(address, 4)?;
        let mut opcode = [0u8; 4];
        opcode.copy_from_slice(&original4);
        let original_opcode = u32::from_le_bytes(opcode);

        patch.resize(instruction_len, 0x90);
        let _ = memory::patch_bytes_public(address, &patch)?;
        unsafe {
            state::cache_original_opcode(address, original_opcode);
        }
        return Ok(original_opcode);
    }

    #[cfg(any(
        all(target_os = "macos", target_arch = "aarch64"),
        all(target_os = "linux", target_arch = "aarch64")
    ))]
    {
        let opcode = asm::assemble_patch_opcode(address, asm)?;
        patchcode(address, opcode)
    }
}

/// Installs an instruction-level hook and executes the original instruction afterward.
///
/// This API patches the target instruction with a trap opcode and registers `callback`.
/// On trap, your callback receives a mutable [`HookContext`].
/// If the callback does not redirect control flow (`pc`/`rip` unchanged),
/// the original instruction runs through an internal trampoline, then execution continues.
///
/// On `x86_64`, trap patching writes `int3` at instruction start and pads the
/// remaining bytes of that decoded instruction with `NOP`.
///
/// # PC-relative note
///
/// Across architectures, this API does **not** support patch points whose original
/// instruction is PC-relative.
///
/// Examples include `aarch64` `adr`/`adrp`, and `x86_64` RIP-relative forms such as
/// `lea` or `mov` using `[rip + disp]`. These instructions can observe a different
/// `pc`/`rip` when replayed by the trampoline, which may produce incorrect behavior.
///
/// For such patch points, prefer [`instrument_no_original`], and emulate the original
/// instruction semantics manually in your callback (typically using `ctx.pc`/`ctx.rip`).
///
/// Returns the original 4-byte value previously stored at `address`.
///
/// # Example
///
/// ```rust,no_run
/// use sighook::{instrument, HookContext};
///
/// extern "C" fn on_hit(_address: u64, _ctx: *mut HookContext) {}
///
/// let target = 0x1000_0000u64;
/// let original = instrument(target, on_hit)?;
/// let _ = original;
/// # Ok::<(), sighook::SigHookError>(())
/// ```
pub fn instrument(address: u64, callback: InstrumentCallback) -> Result<u32, SigHookError> {
    instrument_internal(
        address,
        callback,
        true,
        false,
        InstrumentInstallMode::RuntimePatch,
    )
}

/// Installs an instruction-level hook and skips the original instruction by default.
///
/// This behaves like [`instrument`] except `execute_original = false`.
/// After your callback returns, execution advances past the patched instruction
/// unless the callback explicitly changes control flow (`pc`/`rip`).
///
/// Returns the original 4-byte value previously stored at `address`.
///
/// # Example
///
/// ```rust,no_run
/// use sighook::{instrument_no_original, HookContext};
///
/// extern "C" fn replace_logic(_address: u64, _ctx: *mut HookContext) {}
///
/// let target = 0x1000_0010u64;
/// let original = instrument_no_original(target, replace_logic)?;
/// let _ = original;
/// # Ok::<(), sighook::SigHookError>(())
/// ```
pub fn instrument_no_original(
    address: u64,
    callback: InstrumentCallback,
) -> Result<u32, SigHookError> {
    instrument_internal(
        address,
        callback,
        false,
        false,
        InstrumentInstallMode::RuntimePatch,
    )
}

#[derive(Copy, Clone)]
enum InstrumentInstallMode {
    RuntimePatch,
    Prepatched,
}

fn ensure_prepatched_trap(address: u64) -> Result<(), SigHookError> {
    if address == 0 {
        return Err(SigHookError::InvalidAddress);
    }

    #[cfg(target_arch = "aarch64")]
    {
        let opcode = memory::read_u32(address);
        if !memory::is_brk(opcode) {
            return Err(SigHookError::InvalidAddress);
        }
    }

    #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
    {
        let opcode = memory::read_u8(address);
        if !memory::is_int3(opcode) {
            return Err(SigHookError::InvalidAddress);
        }
    }

    Ok(())
}

/// APIs for trap points that are already patched offline (for example static `brk/int3` patching).
///
/// These APIs reuse the same signal handling and callback contract as runtime patch APIs,
/// but they do not modify executable code pages at install time.
///
/// Notes:
/// - `prepatched::*` requires that the target address already contains a trap instruction.
/// - `prepatched::instrument_no_original` and `prepatched::inline_hook` are the primary paths.
/// - `prepatched::instrument` needs original instruction bytes to execute through trampoline.
///   For `aarch64`, you can preload original opcode via [`cache_original_opcode`].
pub mod prepatched {
    #[cfg(target_arch = "aarch64")]
    use super::state;
    use super::{InstrumentCallback, InstrumentInstallMode, SigHookError, instrument_internal};

    /// Registers a prepatched trap point and executes original instruction afterward.
    ///
    /// This API does not write trap opcodes. The target address must already be patched
    /// to trap offline.
    ///
    /// On `aarch64`, execution of original instruction requires a preloaded original opcode.
    /// Use [`cache_original_opcode`] before calling this function.
    pub fn instrument(address: u64, callback: InstrumentCallback) -> Result<u32, SigHookError> {
        instrument_internal(
            address,
            callback,
            true,
            false,
            InstrumentInstallMode::Prepatched,
        )
    }

    /// Registers a prepatched trap point and skips original instruction by default.
    ///
    /// This API does not write trap opcodes. The target address must already be patched
    /// to trap offline.
    pub fn instrument_no_original(
        address: u64,
        callback: InstrumentCallback,
    ) -> Result<u32, SigHookError> {
        instrument_internal(
            address,
            callback,
            false,
            false,
            InstrumentInstallMode::Prepatched,
        )
    }

    /// Registers a prepatched function-entry trap and returns to caller by default.
    ///
    /// This behaves like [`super::inline_hook`] but never patches executable code pages.
    pub fn inline_hook(addr: u64, callback: InstrumentCallback) -> Result<u32, SigHookError> {
        instrument_internal(
            addr,
            callback,
            false,
            true,
            InstrumentInstallMode::Prepatched,
        )
    }

    /// Preloads original opcode for prepatched trap points on `aarch64`.
    ///
    /// This is optional and only needed when using [`instrument`] (execute-original mode).
    #[cfg(target_arch = "aarch64")]
    pub fn cache_original_opcode(address: u64, original_opcode: u32) -> Result<(), SigHookError> {
        if address == 0 || (address & 0b11) != 0 {
            return Err(SigHookError::InvalidAddress);
        }

        unsafe {
            state::cache_original_opcode(address, original_opcode);
        }
        Ok(())
    }
}

fn instrument_internal(
    address: u64,
    callback: InstrumentCallback,
    execute_original: bool,
    return_to_caller: bool,
    install_mode: InstrumentInstallMode,
) -> Result<u32, SigHookError> {
    unsafe {
        if let Some((bytes, len)) = state::original_bytes_by_address(address) {
            state::register_slot(
                address,
                &bytes[..len as usize],
                len,
                callback,
                execute_original,
                return_to_caller,
                matches!(install_mode, InstrumentInstallMode::RuntimePatch),
            )?;
            let original_opcode = state::cached_original_opcode_by_address(address)
                .or_else(|| state::original_opcode_by_address(address))
                .ok_or(SigHookError::InvalidAddress)?;
            return Ok(original_opcode);
        }

        signal::ensure_handlers_installed()?;

        let step_len: u8 = memory::instruction_width(address)?;

        let (original_bytes, original_opcode, runtime_patch_installed) = match install_mode {
            InstrumentInstallMode::RuntimePatch => {
                #[cfg(target_arch = "aarch64")]
                {
                    let original = memory::patch_u32(address, constants::BRK_OPCODE)?;
                    (original.to_le_bytes().to_vec(), original, true)
                }

                #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
                {
                    let original_bytes = memory::read_bytes(address, step_len as usize)?;
                    let original4 = memory::read_bytes(address, 4)?;

                    let mut trap_patch = vec![0x90u8; step_len as usize];
                    trap_patch[0] = memory::int3_opcode();
                    let _ = memory::patch_bytes_public(address, &trap_patch)?;

                    let mut opcode = [0u8; 4];
                    opcode.copy_from_slice(&original4);
                    (original_bytes, u32::from_le_bytes(opcode), true)
                }
            }
            InstrumentInstallMode::Prepatched => {
                ensure_prepatched_trap(address)?;

                #[cfg(target_arch = "aarch64")]
                {
                    if execute_original {
                        let original_opcode = state::cached_original_opcode_by_address(address)
                            .ok_or(SigHookError::UnsupportedOperation)?;
                        (
                            original_opcode.to_le_bytes().to_vec(),
                            original_opcode,
                            false,
                        )
                    } else {
                        let original_bytes = memory::read_bytes(address, step_len as usize)?;
                        let mut opcode = [0u8; 4];
                        opcode.copy_from_slice(&original_bytes[..4]);
                        (original_bytes, u32::from_le_bytes(opcode), false)
                    }
                }

                #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
                {
                    if execute_original {
                        return Err(SigHookError::UnsupportedOperation);
                    }

                    let original_bytes = memory::read_bytes(address, step_len as usize)?;
                    let original4 = memory::read_bytes(address, 4)?;
                    let mut opcode = [0u8; 4];
                    opcode.copy_from_slice(&original4);
                    (original_bytes, u32::from_le_bytes(opcode), false)
                }
            }
        };

        let register_result = state::register_slot(
            address,
            &original_bytes,
            step_len,
            callback,
            execute_original,
            return_to_caller,
            runtime_patch_installed,
        );

        if let Err(err) = register_result {
            if runtime_patch_installed {
                #[cfg(target_arch = "aarch64")]
                {
                    let mut bytes = [0u8; 4];
                    bytes.copy_from_slice(&original_bytes[..4]);
                    let original_opcode = u32::from_le_bytes(bytes);
                    let _ = memory::patch_u32(address, original_opcode);
                }

                #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
                {
                    let _ = memory::patch_bytes_public(address, &original_bytes);
                }
            }

            return Err(err);
        }

        if runtime_patch_installed {
            state::cache_original_opcode(address, original_opcode);
        }

        Ok(original_opcode)
    }
}

/// Hooks a function entry by trap instruction and returns to caller by default.
///
/// This API installs a signal-based entry hook using trap instrumentation.
/// In your callback, set return-value registers (for example `x0` or `rax`).
///
/// If your callback does not redirect control flow (`pc`/`rip` unchanged),
/// this hook returns to the caller automatically:
/// - `aarch64`: `pc <- x30`
/// - `x86_64`: `rip <- [rsp]`, `rsp += 8` (equivalent to `ret`)
///
/// Returns the first 4 bytes of original instruction bytes at `addr`.
///
/// # Example
///
/// ```rust,no_run
/// use sighook::{inline_hook, HookContext};
///
/// extern "C" fn replacement(_address: u64, ctx: *mut HookContext) {
///     unsafe {
///         #[cfg(target_arch = "aarch64")]
///         {
///             (*ctx).regs.named.x0 = 42;
///         }
///         #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
///         {
///             (*ctx).rax = 42;
///         }
///     }
/// }
///
/// let function_entry = 0x1000_1000u64;
/// let original = inline_hook(function_entry, replacement)?;
/// let _ = original;
/// # Ok::<(), sighook::SigHookError>(())
/// ```
pub fn inline_hook(addr: u64, callback: InstrumentCallback) -> Result<u32, SigHookError> {
    instrument_internal(
        addr,
        callback,
        false,
        true,
        InstrumentInstallMode::RuntimePatch,
    )
}

/// Detours a function entry to `replace_fn` with inline jump patching.
///
/// Strategy:
/// - Try near jump first (short encoding).
/// - Fall back to architecture-specific far jump sequence when out of range.
///
/// Returns the first 4 bytes of original instruction bytes at `addr`.
///
/// # Example
///
/// ```rust,no_run
/// use sighook::inline_hook_jump;
///
/// extern "C" fn replacement() {}
///
/// let function_entry = 0x1000_1000u64;
/// let replacement_addr = replacement as usize as u64;
/// let original = inline_hook_jump(function_entry, replacement_addr)?;
/// let _ = original;
/// # Ok::<(), sighook::SigHookError>(())
/// ```
pub fn inline_hook_jump(addr: u64, replace_fn: u64) -> Result<u32, SigHookError> {
    #[cfg(target_arch = "aarch64")]
    {
        let patch = match memory::encode_b(addr, replace_fn) {
            Ok(b_opcode) => b_opcode.to_le_bytes().to_vec(),
            Err(SigHookError::BranchOutOfRange) => {
                let mut bytes = [0u8; 16];
                bytes[0..4].copy_from_slice(&constants::LDR_X16_LITERAL_8.to_le_bytes());
                bytes[4..8].copy_from_slice(&constants::BR_X16.to_le_bytes());
                bytes[8..16].copy_from_slice(&replace_fn.to_le_bytes());
                bytes.to_vec()
            }
            Err(err) => return Err(err),
        };

        let original = memory::read_bytes(addr, 16)?;
        let inserted = unsafe { state::cache_inline_patch(addr, &original)? };
        if let Err(err) = memory::patch_bytes_public(addr, &patch) {
            if inserted {
                unsafe {
                    state::remove_inline_patch(addr);
                }
            }
            return Err(err);
        }

        if original.len() < 4 {
            return Err(SigHookError::InvalidAddress);
        }

        let mut opcode = [0u8; 4];
        opcode.copy_from_slice(&original[..4]);
        let original_opcode = u32::from_le_bytes(opcode);
        unsafe {
            state::cache_original_opcode(addr, original_opcode);
        }
        Ok(original_opcode)
    }

    #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
    {
        let patch = if let Ok(jmp) = memory::encode_jmp_rel32(addr, replace_fn) {
            jmp.to_vec()
        } else {
            memory::encode_absolute_jump(replace_fn).to_vec()
        };

        let original = memory::read_bytes(addr, memory::encode_absolute_jump(0).len())?;
        let inserted = unsafe { state::cache_inline_patch(addr, &original)? };
        if let Err(err) = memory::patch_bytes_public(addr, &patch) {
            if inserted {
                unsafe {
                    state::remove_inline_patch(addr);
                }
            }
            return Err(err);
        }

        if original.len() < 4 {
            return Err(SigHookError::InvalidAddress);
        }

        let mut opcode = [0u8; 4];
        opcode.copy_from_slice(&original[..4]);
        let original_opcode = u32::from_le_bytes(opcode);
        unsafe {
            state::cache_original_opcode(addr, original_opcode);
        }
        Ok(original_opcode)
    }
}

/// Restores a previously installed hook at `address`.
///
/// This API supports hook points created by [`instrument`], [`instrument_no_original`],
/// [`inline_hook`], [`prepatched::instrument`], [`prepatched::instrument_no_original`],
/// [`prepatched::inline_hook`], and [`inline_hook_jump`]. On success, runtime state for that
/// address is removed. For runtime-patched hooks, patched instruction bytes are also restored.
///
/// Signal handlers stay installed once initialized, even after unhooking all addresses.
///
/// # Example
///
/// ```rust,ignore
/// use sighook::{instrument, unhook, HookContext};
///
/// extern "C" fn on_hit(_address: u64, _ctx: *mut HookContext) {}
///
/// let addr = 0x1000_0000u64;
/// let _ = instrument(addr, on_hit)?;
/// unhook(addr)?;
/// # Ok::<(), sighook::SigHookError>(())
/// ```
pub fn unhook(address: u64) -> Result<(), SigHookError> {
    if address == 0 {
        return Err(SigHookError::InvalidAddress);
    }

    unsafe {
        if let Some(slot) = state::slot_by_address(address) {
            if slot.original_len == 0 {
                return Err(SigHookError::InvalidAddress);
            }

            if slot.runtime_patch_installed {
                memory::patch_bytes_public(
                    address,
                    &slot.original_bytes[..slot.original_len as usize],
                )?;
            }

            if let Some(removed_slot) = state::remove_slot(address) {
                if removed_slot.trampoline_pc != 0 {
                    trampoline::free_original_trampoline(removed_slot.trampoline_pc);
                }
            }

            state::remove_cached_original_opcode(address);
            return Ok(());
        }

        if let Some((bytes, len)) = state::inline_patch_by_address(address) {
            if len == 0 {
                return Err(SigHookError::InvalidAddress);
            }

            memory::patch_bytes_public(address, &bytes[..len as usize])?;
            state::remove_inline_patch(address);
            state::remove_cached_original_opcode(address);
            return Ok(());
        }
    }

    Err(SigHookError::HookNotFound)
}

/// Returns the saved original 4-byte value for a previously patched address.
///
/// The value is available after a successful call to [`patchcode`], [`instrument`],
/// [`instrument_no_original`], [`inline_hook`], or [`inline_hook_jump`] on the same address.
///
/// # Example
///
/// ```rust,no_run
/// use sighook::{instrument, original_opcode, HookContext};
///
/// extern "C" fn on_hit(_address: u64, _ctx: *mut HookContext) {}
///
/// let addr = 0x1000_2000u64;
/// let _ = instrument(addr, on_hit)?;
/// let maybe_old = original_opcode(addr);
/// let _ = maybe_old;
/// # Ok::<(), sighook::SigHookError>(())
/// ```
pub fn original_opcode(address: u64) -> Option<u32> {
    unsafe {
        state::cached_original_opcode_by_address(address)
            .or_else(|| state::original_opcode_by_address(address))
    }
}

/// Writes raw bytes to `address` and returns the overwritten bytes with the same length.
///
/// Use this API when you need to patch more than one instruction or when your patch
/// length exceeds the current instruction length.
///
/// # Example
///
/// ```rust,no_run
/// use sighook::patch_bytes;
///
/// let address = 0x7FFF_0000_0000u64;
/// let original = patch_bytes(address, &[0x90, 0x90, 0x90, 0x90])?;
/// let _ = original;
/// # Ok::<(), sighook::SigHookError>(())
/// ```
pub fn patch_bytes(address: u64, bytes: &[u8]) -> Result<Vec<u8>, SigHookError> {
    let original = memory::patch_bytes_public(address, bytes)?;
    if original.len() >= 4 {
        let mut opcode = [0u8; 4];
        opcode.copy_from_slice(&original[..4]);
        let original_opcode = u32::from_le_bytes(opcode);
        unsafe {
            state::cache_original_opcode(address, original_opcode);
        }
    }
    Ok(original)
}

#[cfg(test)]
mod tests {
    #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
    use super::effective_x86_patch_len;

    #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
    #[test]
    fn effective_x86_patch_len_trims_trailing_nops() {
        assert_eq!(effective_x86_patch_len(&[0x0f, 0xaf, 0xc2, 0x90]), 3);
        assert_eq!(effective_x86_patch_len(&[0x90, 0x90, 0x90, 0x90]), 1);
        assert_eq!(effective_x86_patch_len(&[0x0f, 0xaf, 0xc2, 0x00]), 4);
    }
}
