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.

#[cfg(not(all(feature = "napi8", not(target_family = "wasm"))))]
use std::any::TypeId;
use std::ffi::c_void;

/// 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<TypeId>;

/// 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>() -> ClassId {
  Some(TypeId::of::<T>())
}

#[cfg(all(feature = "napi8", not(target_family = "wasm")))]
#[inline(always)]
#[allow(clippy::extra_unused_type_parameters)]
pub(crate) fn class_id_of<T: 'static>() -> ClassId {
  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]
pub(crate) fn check_class_identity<T>(
  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)) => class_id == 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()))
  }
}

/// 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::ffi::c_void;

  use super::{check_class_identity, class_id_of, short_type_name, ClassId};
  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>());
    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>>());
    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 short_type_name_drops_the_module_path() {
    assert_eq!(short_type_name::<ClassA>(), "ClassA");
    assert_eq!(short_type_name::<WithLifetime<'static>>(), "WithLifetime");
  }
}