Documentation
//! Class check for builds without Node-API object type tags (#3608).
//!
//! On napi8 native builds every `#[napi]` class instance carries a type tag that
//! is checked before the wrapped pointer is cast to `*mut T` (see `type_tag.rs`).
//! Without `napi8`, and on every wasm target, there is no tag. Instead, every
//! class wrap done by the runtime (`CallbackInfo::_construct` / `_factory`,
//! `new_instance`) records the Rust class in the `REFERENCE_MAP` entry it
//! already creates, and every runtime site that casts a `napi_unwrap` result to
//! `*mut T` checks that record first. Generated code is unchanged, so this also
//! works with code generated by older `napi-derive` versions.
//!
//! Limits, compared with the type tag:
//! - An object that this copy of the runtime did not wrap (a second loaded copy
//!   of the addon, `wrap_and_tag`, hand-written `napi_wrap`) has no record and
//!   is accepted as before.
//! - Zero-sized classes are not checked; no byte is read through them.
//! - `&T` / `&mut T` arguments reach the check through
//!   `register_native_borrow_with_value`, which code generated by
//!   `napi-derive` 3.6.2 and older does not call.

use std::ffi::c_void;
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
use std::{
  any::TypeId,
  collections::HashMap,
  num::NonZeroU32,
  sync::{LazyLock, Mutex},
};

#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
use rustc_hash::FxBuildHasher;

/// The Rust class wrapped at a `REFERENCE_MAP` key. `None` when the entry was
/// added through the public `Reference::add_ref`.
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
pub(crate) type ClassId = Option<NonZeroU32>;

#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
const MAX_CLASS_IDS: usize = 65_536;

#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
#[derive(Default)]
struct ClassRegistry {
  type_ids: Vec<TypeId>,
  indices: HashMap<TypeId, NonZeroU32, FxBuildHasher>,
}

/// Append-only so an ID remains valid for every `REFERENCE_MAP` entry that
/// recorded it, including entries retained after their wrapped value dropped.
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
static CLASS_REGISTRY: LazyLock<Mutex<ClassRegistry>> =
  LazyLock::new(|| Mutex::new(ClassRegistry::default()));

#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
fn class_registry_full() -> crate::Error {
  crate::Error::new(
    crate::Status::GenericFailure,
    "N-API class identity registry is full",
  )
}

#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
fn class_registry_allocation_failure() -> crate::Error {
  // The registry just failed to allocate; keep the error path allocation-free.
  crate::Error::from_status(crate::Status::GenericFailure)
}

#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
fn intern_class_id(
  registry: &Mutex<ClassRegistry>,
  type_id: TypeId,
  capacity: usize,
) -> crate::Result<ClassId> {
  let mut registry = registry.lock().unwrap();
  if let Some(class_id) = registry.indices.get(&type_id) {
    return Ok(Some(*class_id));
  }
  if registry.type_ids.len() >= capacity {
    return Err(class_registry_full());
  }
  registry
    .type_ids
    .try_reserve(1)
    .map_err(|_| class_registry_allocation_failure())?;
  registry
    .indices
    .try_reserve(1)
    .map_err(|_| class_registry_allocation_failure())?;
  let class_id =
    NonZeroU32::new(u32::try_from(registry.type_ids.len() + 1).map_err(|_| class_registry_full())?)
      .ok_or_else(class_registry_full)?;
  registry.type_ids.push(type_id);
  registry.indices.insert(type_id, class_id);
  Ok(Some(class_id))
}

/// Nothing is recorded on napi8 native builds: the type tag does this job.
#[cfg(all(feature = "napi8", not(target_family = "wasm")))]
#[derive(Clone, Copy, Default)]
pub(crate) struct ClassId;

/// The [`ClassId`] recorded when the runtime wraps a `T`.
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
#[inline(always)]
pub(crate) fn class_id_of<T: 'static>() -> crate::Result<ClassId> {
  intern_class_id(&CLASS_REGISTRY, TypeId::of::<T>(), MAX_CLASS_IDS)
}

#[cfg(all(feature = "napi8", not(target_family = "wasm")))]
#[inline(always)]
#[allow(clippy::extra_unused_type_parameters)]
pub(crate) fn class_id_of<T: 'static>() -> crate::Result<ClassId> {
  Ok(ClassId)
}

/// The error for a value of the wrong class. Shared with `validate_type_tag`,
/// so builds with and without type tags throw the same error.
pub(crate) fn wrong_class_error(class_name: &str) -> crate::Error {
  crate::Error::new(
    crate::Status::InvalidArg,
    format!("Value is not an instance of class `{class_name}`"),
  )
}

/// `TypeId` of `T` without a `T: 'static` bound (same trick as the `typeid`
/// crate). `TypeId` ignores lifetimes, so this equals `TypeId::of` of the
/// `'static` form of `T`. Needed because the unwrap sites (`unwrap_raw<T>`,
/// `ClassInstance<'env, T>`, ...) have no `'static` bound, and classes with a
/// lifetime use them.
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
#[inline(always)]
fn type_id_of<T: ?Sized>() -> TypeId {
  trait NonStaticAny {
    fn get_type_id(&self) -> TypeId
    where
      Self: 'static;
  }

  impl<T: ?Sized> NonStaticAny for std::marker::PhantomData<T> {
    #[inline(always)]
    fn get_type_id(&self) -> TypeId
    where
      Self: 'static,
    {
      TypeId::of::<T>()
    }
  }

  let phantom_data = std::marker::PhantomData::<T>;
  NonStaticAny::get_type_id(unsafe {
    std::mem::transmute::<&dyn NonStaticAny, &(dyn NonStaticAny + 'static)>(&phantom_data)
  })
}

/// Check that `wrapped`, a `napi_unwrap` result, may be cast to `*mut T`.
/// `wrapped` is only used as a map key and for address checks; it is never read.
#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
#[inline]
fn check_class_identity_with_registry<T>(
  registry: &Mutex<ClassRegistry>,
  wrapped: *mut c_void,
  class_name: fn() -> &'static str,
) -> crate::Result<()> {
  // No byte is read through a zero-sized `T`. Zero-sized instances can also
  // share one dangling key (older `napi-derive` output, or a zero-sized class
  // aligned above 1), so the recorded class may belong to another instance.
  if std::mem::size_of::<T>() == 0 {
    return Ok(());
  }
  let recorded = super::REFERENCE_MAP
    .with(|cell| cell.borrow_mut(|map| map.get(&wrapped).map(|entry| entry.class_id)));
  let is_t = match recorded {
    Some(Some(class_id)) => registry
      .lock()
      .unwrap()
      .type_ids
      .get(class_id.get() as usize - 1)
      .is_some_and(|registered| *registered == type_id_of::<T>()),
    // Not wrapped by this copy's class glue (a second loaded copy of the addon,
    // `wrap_and_tag`, hand-written `napi_wrap`): accepted as before. A live `T`
    // is never in the null page or misaligned; such a pointer is the dangling
    // pointer of a zero-sized class whose shared entry is gone.
    _ => {
      let addr = wrapped as usize;
      addr >= 4096 && addr.is_multiple_of(std::mem::align_of::<T>())
    }
  };
  if is_t {
    Ok(())
  } else {
    Err(wrong_class_error(class_name()))
  }
}

#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
#[inline]
pub(crate) fn check_class_identity<T>(
  wrapped: *mut c_void,
  class_name: fn() -> &'static str,
) -> crate::Result<()> {
  check_class_identity_with_registry::<T>(&CLASS_REGISTRY, wrapped, class_name)
}

/// No-op on napi8 native builds: the type tag check covers these sites.
#[cfg(all(feature = "napi8", not(target_family = "wasm")))]
#[inline(always)]
#[allow(clippy::extra_unused_type_parameters)]
pub(crate) fn check_class_identity<T>(
  _wrapped: *mut c_void,
  _class_name: fn() -> &'static str,
) -> crate::Result<()> {
  Ok(())
}

/// `T`'s name without its module path, which is the name generated
/// `FromNapiRef` code passes to `validate_type_tag`.
pub(crate) fn short_type_name<T>() -> &'static str {
  let name = std::any::type_name::<T>();
  let name = name.split('<').next().unwrap_or(name);
  name.rsplit("::").next().unwrap_or(name)
}

#[cfg(all(test, not(all(feature = "napi8", not(target_family = "wasm")))))]
mod tests {
  use std::{any::TypeId, ffi::c_void, sync::Mutex};

  use super::{
    check_class_identity, check_class_identity_with_registry, class_id_of, intern_class_id,
    short_type_name, ClassId, ClassRegistry,
  };
  use crate::bindgen_runtime::{RefEntry, REFERENCE_MAP};
  use crate::Status;

  struct ClassA(#[allow(dead_code)] u64);
  struct ClassB(#[allow(dead_code)] u64);
  struct WithLifetime<'a>(#[allow(dead_code)] &'a u64);
  struct Empty;

  fn record(wrapped: *mut c_void, class_id: ClassId) {
    let info = (wrapped, std::ptr::null_mut(), std::ptr::null());
    REFERENCE_MAP
      .with(|cell| cell.borrow_mut(|map| map.insert(wrapped, RefEntry { info, class_id })));
  }

  fn forget(wrapped: *mut c_void) {
    REFERENCE_MAP.with(|cell| cell.borrow_mut(|map| map.remove(&wrapped)));
  }

  fn name() -> &'static str {
    "ClassB"
  }

  #[test]
  fn recorded_class_must_match() {
    let mut value = ClassA(1);
    let wrapped: *mut c_void = (&mut value as *mut ClassA).cast();
    record(wrapped, class_id_of::<ClassA>().unwrap());
    assert!(check_class_identity::<ClassA>(wrapped, name).is_ok());
    let err = check_class_identity::<ClassB>(wrapped, name).unwrap_err();
    assert_eq!(err.status, Status::InvalidArg);
    assert_eq!(err.reason, "Value is not an instance of class `ClassB`");
    // No byte is read through a zero-sized class.
    assert!(check_class_identity::<Empty>(wrapped, name).is_ok());

    // Lifetimes do not change the class.
    record(wrapped, class_id_of::<WithLifetime<'static>>().unwrap());
    assert!(check_class_identity::<WithLifetime<'_>>(wrapped, name).is_ok());
    assert!(check_class_identity::<ClassA>(wrapped, name).is_err());
    forget(wrapped);
  }

  #[test]
  fn unrecorded_class_is_accepted_unless_it_cannot_be_a_value() {
    let mut value = ClassB(1);
    let wrapped: *mut c_void = (&mut value as *mut ClassB).cast();
    assert!(check_class_identity::<ClassB>(wrapped, name).is_ok());
    // Entry added through the public `Reference::add_ref`.
    record(wrapped, None);
    assert!(check_class_identity::<ClassB>(wrapped, name).is_ok());
    forget(wrapped);

    let dangling = std::ptr::NonNull::<Empty>::dangling().as_ptr().cast();
    assert!(check_class_identity::<ClassB>(dangling, name).is_err());
    assert!(check_class_identity::<ClassB>(wrapped.wrapping_byte_add(1), name).is_err());
  }

  #[test]
  fn class_ids_are_compact_and_stable() {
    assert_eq!(std::mem::size_of::<ClassId>(), 4);
    assert_eq!(
      class_id_of::<ClassA>().unwrap(),
      class_id_of::<ClassA>().unwrap()
    );
    assert_ne!(
      class_id_of::<ClassA>().unwrap(),
      class_id_of::<ClassB>().unwrap()
    );
  }

  #[test]
  fn capacity_failure_leaves_registry_usable() {
    let registry = Mutex::new(ClassRegistry::default());
    let mut value = ClassA(1);
    let wrapped: *mut c_void = (&mut value as *mut ClassA).cast();
    let class_a = intern_class_id(&registry, TypeId::of::<ClassA>(), 1).unwrap();
    record(wrapped, class_a);

    let err = intern_class_id(&registry, TypeId::of::<ClassB>(), 1).unwrap_err();
    assert_eq!(err.status, Status::GenericFailure);
    assert_eq!(err.reason, "N-API class identity registry is full");
    assert!(registry.lock().is_ok());

    assert_eq!(
      intern_class_id(&registry, TypeId::of::<ClassA>(), 1).unwrap(),
      class_a
    );
    assert!(check_class_identity_with_registry::<ClassA>(&registry, wrapped, name).is_ok());
    assert!(check_class_identity_with_registry::<ClassB>(&registry, wrapped, name).is_err());
    forget(wrapped);
  }

  #[test]
  fn short_type_name_drops_the_module_path() {
    assert_eq!(short_type_name::<ClassA>(), "ClassA");
    assert_eq!(short_type_name::<WithLifetime<'static>>(), "WithLifetime");
  }
}