use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::sync::Arc;
use super::{CONTENT_DESCRIPTION, FIT, Plan, Setter, TINT, color, owned_text, slot_of};
use crate::NativeWidgetError;
use crate::registry::SlotId;
use crate::runtime::Params;
pub(crate) const KIND: &str = "image";
pub(crate) const REV: &str = "imageRev";
pub(crate) struct Image;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum Fit {
#[default]
Contain,
Cover,
Fill,
Center,
}
impl Fit {
fn from_params(raw: &str) -> Option<Self> {
match raw {
"contain" => Some(Self::Contain),
"cover" => Some(Self::Cover),
"fill" => Some(Self::Fill),
"center" => Some(Self::Center),
_ => None,
}
}
#[cfg(target_os = "android")]
pub(crate) fn scale_type_constant(self) -> &'static jni::strings::JNIStr {
use jni::jni_str;
match self {
Self::Contain => jni_str!("FIT_CENTER"),
Self::Cover => jni_str!("CENTER_CROP"),
Self::Fill => jni_str!("FIT_XY"),
Self::Center => jni_str!("CENTER"),
}
}
#[cfg(target_os = "ios")]
pub(crate) fn content_mode(self) -> objc2_ui_kit::UIViewContentMode {
use objc2_ui_kit::UIViewContentMode;
match self {
Self::Contain => UIViewContentMode::ScaleAspectFit,
Self::Cover => UIViewContentMode::ScaleAspectFill,
Self::Fill => UIViewContentMode::ScaleToFill,
Self::Center => UIViewContentMode::Center,
}
}
#[cfg(target_os = "macos")]
pub(crate) fn image_scaling(self) -> objc2_app_kit::NSImageScaling {
use objc2_app_kit::NSImageScaling;
match self {
Self::Contain | Self::Cover => NSImageScaling::ScaleProportionallyUpOrDown,
Self::Fill => NSImageScaling::ScaleAxesIndependently,
Self::Center => NSImageScaling::ScaleNone,
}
}
}
#[derive(Clone, Default)]
pub(crate) struct ImageBytes {
rev: u64,
bytes: Option<Arc<[u8]>>,
}
impl ImageBytes {
pub(crate) fn empty() -> Self {
Self::default()
}
pub(crate) fn as_slice(&self) -> Option<&[u8]> {
self.bytes.as_deref()
}
pub(crate) fn len(&self) -> usize {
self.bytes.as_ref().map_or(0, |bytes| bytes.len())
}
pub(crate) fn rev(&self) -> u64 {
self.rev
}
}
impl PartialEq for ImageBytes {
fn eq(&self, other: &Self) -> bool {
self.rev == other.rev
&& match (&self.bytes, &other.bytes) {
(Some(ours), Some(theirs)) => Arc::ptr_eq(ours, theirs),
(None, None) => true,
_ => false,
}
}
}
impl std::fmt::Debug for ImageBytes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImageBytes")
.field("rev", &self.rev)
.field("len", &self.len())
.finish()
}
}
struct Published {
rev: u64,
bytes: Arc<[u8]>,
holders: u32,
}
#[allow(clippy::missing_const_for_thread_local)]
mod tables {
use super::{Cell, HashMap, Published, RefCell, SlotId};
thread_local! {
pub(super) static PUBLISHED: RefCell<HashMap<SlotId, Published>> =
RefCell::new(HashMap::new());
pub(super) static NEXT_REV: Cell<u64> = const { Cell::new(1) };
}
}
use tables::{NEXT_REV, PUBLISHED};
pub(crate) fn publish_bytes(slot: SlotId, bytes: Arc<[u8]>) -> u64 {
PUBLISHED.with(|table| {
let mut table = table.borrow_mut();
let holders = match table.get(&slot) {
Some(existing) if Arc::ptr_eq(&existing.bytes, &bytes) => return existing.rev,
Some(existing) => existing.holders,
None => 0,
};
let rev = NEXT_REV.with(|next| {
let rev = next.get();
next.set(rev.wrapping_add(1));
rev
});
table.insert(
slot,
Published {
rev,
bytes,
holders,
},
);
rev
})
}
pub(crate) fn claim_bytes(slot: SlotId) {
PUBLISHED.with(|table| {
if let Some(entry) = table.borrow_mut().get_mut(&slot) {
entry.holders = entry.holders.saturating_add(1);
}
});
}
pub(crate) fn release_bytes(slot: SlotId) {
PUBLISHED.with(|table| {
let mut table = table.borrow_mut();
let Some(entry) = table.get_mut(&slot) else {
return;
};
entry.holders = entry.holders.saturating_sub(1);
if entry.holders == 0 {
table.remove(&slot);
}
});
}
pub(crate) fn retire(slot: SlotId) {
PUBLISHED.with(|table| {
table.borrow_mut().remove(&slot);
});
}
fn published_bytes(slot: SlotId) -> ImageBytes {
PUBLISHED.with(|table| {
table
.borrow()
.get(&slot)
.map(|entry| ImageBytes {
rev: entry.rev,
bytes: Some(Arc::clone(&entry.bytes)),
})
.unwrap_or_default()
})
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ImageProps {
pub(crate) slot: SlotId,
pub(crate) bytes: ImageBytes,
pub(crate) fit: Fit,
pub(crate) tint: Option<i32>,
pub(crate) content_description: Option<String>,
}
impl ImageProps {
pub(crate) fn platform_default(slot: SlotId) -> Self {
Self {
slot,
bytes: ImageBytes::empty(),
fit: Fit::Contain,
tint: None,
content_description: None,
}
}
pub(crate) fn decode(params: &Params<'_>) -> Result<Self, NativeWidgetError> {
let slot = slot_of(params)?;
let bytes = published_bytes(slot);
if let Some(claimed) = params.int(REV)
&& claimed as u64 != bytes.rev()
{
log::debug!(
"frust-native-widgets: image slot {slot} params claim revision {claimed} but the \
publish table holds {} — showing the published bytes",
bytes.rev()
);
}
Ok(Self {
slot,
bytes,
fit: params
.string(FIT)
.and_then(|raw| Fit::from_params(&raw))
.unwrap_or_default(),
tint: color(params, TINT),
content_description: owned_text(params, CONTENT_DESCRIPTION),
})
}
pub(crate) fn plan<'a>(old: &Self, new: &'a Self) -> Plan<'a> {
let mut plan = Plan::new();
if old.bytes != new.bytes {
plan.push(Setter::ImageBytes(&new.bytes));
}
if old.fit != new.fit {
plan.push(Setter::ScaleType(new.fit));
}
if old.tint != new.tint {
plan.push(Setter::ImageTint(new.tint));
}
if old.content_description != new.content_description {
plan.push(Setter::ContentDescription(
new.content_description.as_deref(),
));
}
plan
}
}
#[cfg(target_os = "android")]
pub(crate) mod platform {
use jni::objects::JObject;
use jni::refs::Global;
use super::{Image, ImageProps, claim_bytes, release_bytes};
use crate::NativeWidgetError;
use crate::android::{NativeCtx, NativeView};
use crate::controls::platform::{FRAME_CAPACITY, apply_all};
use crate::registry::SlotId;
use crate::runtime::{NativeWidget, Params};
const CLASS: &str = "android.widget.ImageView";
pub(crate) struct ImageState {
view: Global<JObject<'static>>,
slot: SlotId,
}
impl NativeWidget for Image {
type Props = ImageProps;
type State = ImageState;
fn decode_props(params: &Params<'_>) -> Result<Self::Props, NativeWidgetError> {
ImageProps::decode(params)
}
fn create(
ctx: &mut NativeCtx<'_, '_>,
props: &Self::Props,
) -> Result<(NativeView, Self::State), NativeWidgetError> {
let view = ctx.new_view(CLASS)?;
let plan = ImageProps::plan(&ImageProps::platform_default(props.slot), props);
ctx.with_frame(FRAME_CAPACITY, |ctx| apply_all(ctx, &view, &plan))?;
let handle = ctx.retain(&view)?;
let retained = ctx.retain(&view)?;
claim_bytes(props.slot);
Ok((
NativeView::new(handle),
ImageState {
view: retained,
slot: props.slot,
},
))
}
fn update(
ctx: &mut NativeCtx<'_, '_>,
state: &mut Self::State,
old: &Self::Props,
new: &Self::Props,
) -> Result<(), NativeWidgetError> {
let plan = ImageProps::plan(old, new);
ctx.with_frame(FRAME_CAPACITY, |ctx| apply_all(ctx, &state.view, &plan))
}
fn dispose(
_ctx: &mut NativeCtx<'_, '_>,
state: Self::State,
) -> Result<(), NativeWidgetError> {
release_bytes(state.slot);
Ok(())
}
}
}
#[cfg(target_os = "ios")]
pub(crate) mod platform {
use objc2::MainThreadMarker;
use objc2::rc::Retained;
use objc2_foundation::NSData;
use objc2_ui_kit::{UIImage, UIImageRenderingMode, UIImageView};
use super::{Image, ImageProps, KIND, claim_bytes, release_bytes};
use crate::NativeWidgetError;
use crate::apple::{NativeCtx, NativeView};
use crate::controls::platform;
use crate::controls::{Plan, Setter};
use crate::registry::SlotId;
use crate::runtime::{NativeWidget, Params};
pub(crate) struct ImageState {
view: Retained<UIImageView>,
slot: SlotId,
image: Option<Retained<UIImage>>,
tinted: bool,
}
impl ImageState {
fn install(&self) {
let rendered = self.image.as_ref().map(|image| {
if self.tinted {
image.imageWithRenderingMode(UIImageRenderingMode::AlwaysTemplate)
} else {
Retained::clone(image)
}
});
self.view.setImage(rendered.as_deref());
}
}
impl NativeWidget for Image {
type Props = ImageProps;
type State = ImageState;
fn decode_props(params: &Params<'_>) -> Result<Self::Props, NativeWidgetError> {
ImageProps::decode(params)
}
fn create(
ctx: &mut NativeCtx<'_, '_>,
props: &Self::Props,
) -> Result<(NativeView, Self::State), NativeWidgetError> {
let mtm = ctx.mtm();
let view = UIImageView::new(mtm);
let default = ImageProps::platform_default(props.slot);
view.setContentMode(default.fit.content_mode());
view.setClipsToBounds(true);
let mut state = ImageState {
view,
slot: props.slot,
image: None,
tinted: false,
};
let plan = ImageProps::plan(&default, props);
apply_all(mtm, &mut state, &plan);
let handle = NativeView::new(Retained::clone(&state.view).into_super(), mtm);
claim_bytes(props.slot);
Ok((handle, state))
}
fn update(
ctx: &mut NativeCtx<'_, '_>,
state: &mut Self::State,
old: &Self::Props,
new: &Self::Props,
) -> Result<(), NativeWidgetError> {
let plan = ImageProps::plan(old, new);
apply_all(ctx.mtm(), state, &plan);
Ok(())
}
fn dispose(
_ctx: &mut NativeCtx<'_, '_>,
state: Self::State,
) -> Result<(), NativeWidgetError> {
release_bytes(state.slot);
Ok(())
}
}
fn apply_all(mtm: MainThreadMarker, state: &mut ImageState, plan: &Plan<'_>) {
for setter in plan {
apply(mtm, state, setter);
}
}
fn apply(mtm: MainThreadMarker, state: &mut ImageState, setter: &Setter<'_>) {
match *setter {
Setter::ImageBytes(bytes) => {
state.image = bytes
.as_slice()
.and_then(|raw| UIImage::imageWithData(&NSData::with_bytes(raw)));
if state.image.is_none() && bytes.as_slice().is_some() {
log::warn!(
"frust-native-widgets: UIImage could not decode {} image byte(s) — \
clearing the view",
bytes.len()
);
}
state.install();
}
Setter::ScaleType(fit) => state.view.setContentMode(fit.content_mode()),
Setter::ImageTint(argb) => {
platform::set_image_tint(&state.view, platform::optional_ui_color(argb).as_deref());
state.tinted = argb.is_some();
state.install();
}
Setter::ContentDescription(label) => {
platform::set_accessibility_label(&state.view, label, mtm);
}
ref other => platform::warn_unexpected_setter(KIND, other),
}
}
}
#[cfg(target_os = "macos")]
pub(crate) mod platform {
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use objc2::AnyThread;
use objc2::rc::Retained;
use objc2_app_kit::{NSImage, NSImageFrameStyle, NSImageView};
use objc2_foundation::NSData;
use super::{Image, ImageProps, KIND, claim_bytes, release_bytes};
use crate::NativeWidgetError;
use crate::appkit::{NativeCtx, NativeView};
use crate::controls::platform;
use crate::controls::{Plan, Setter};
use crate::registry::SlotId;
use crate::runtime::{NativeWidget, Params};
pub(crate) struct ImageState {
view: Retained<NSImageView>,
slot: SlotId,
image: Option<Retained<NSImage>>,
tinted: bool,
}
impl NativeWidget for Image {
type Props = ImageProps;
type State = ImageState;
fn decode_props(params: &Params<'_>) -> Result<Self::Props, NativeWidgetError> {
ImageProps::decode(params)
}
fn create(
ctx: &mut NativeCtx<'_, '_>,
props: &Self::Props,
) -> Result<(NativeView, Self::State), NativeWidgetError> {
let mtm = ctx.mtm();
let view = NSImageView::new(mtm);
let default = ImageProps::platform_default(props.slot);
view.setImageScaling(default.fit.image_scaling());
view.setImageFrameStyle(NSImageFrameStyle::None);
let mut state = ImageState {
view,
slot: props.slot,
image: None,
tinted: false,
};
let plan = ImageProps::plan(&default, props);
apply_all(&mut state, &plan);
let handle =
NativeView::new(Retained::clone(&state.view).into_super().into_super(), mtm);
claim_bytes(props.slot);
Ok((handle, state))
}
fn update(
_ctx: &mut NativeCtx<'_, '_>,
state: &mut Self::State,
old: &Self::Props,
new: &Self::Props,
) -> Result<(), NativeWidgetError> {
apply_all(state, &ImageProps::plan(old, new));
Ok(())
}
fn dispose(
_ctx: &mut NativeCtx<'_, '_>,
state: Self::State,
) -> Result<(), NativeWidgetError> {
release_bytes(state.slot);
Ok(())
}
}
fn apply_all(state: &mut ImageState, plan: &Plan<'_>) {
for setter in plan {
apply(state, setter);
}
}
fn apply(state: &mut ImageState, setter: &Setter<'_>) {
state.tinted = next_tinted(state.tinted, setter);
match *setter {
Setter::ImageBytes(bytes) => {
let image = bytes.as_slice().and_then(decode);
if let Some(image) = &image {
image.setTemplate(state.tinted);
}
state.view.setImage(image.as_deref());
state.image = image;
}
Setter::ScaleType(fit) => state.view.setImageScaling(fit.image_scaling()),
Setter::ImageTint(argb) => {
if let Some(image) = &state.image {
image.setTemplate(state.tinted);
}
platform::set_tint(&state.view, "ImageTint", argb);
}
Setter::ContentDescription(label) => {
platform::set_accessibility_label(&state.view, label);
}
ref other => platform::warn_unexpected_setter(KIND, other),
}
}
fn next_tinted(current: bool, setter: &Setter<'_>) -> bool {
match *setter {
Setter::ImageTint(argb) => argb.is_some(),
_ => current,
}
}
fn decode(raw: &[u8]) -> Option<Retained<NSImage>> {
if shadowed_codec().is_some() {
return None;
}
let image = NSImage::initWithData(NSImage::alloc(), &NSData::with_bytes(raw));
if image.is_none() {
log::warn!(
"frust-native-widgets: macOS NSImage could not decode {} image byte(s) — \
clearing the view",
raw.len()
);
}
image
}
const IMAGEIO_CODEC_LEAVES: [&str; 6] = [
"libPng.dylib",
"libJPEG.dylib",
"libTIFF.dylib",
"libGIF.dylib",
"libJP2.dylib",
"libRadiance.dylib",
];
fn shadowed_codec() -> Option<&'static Path> {
static SHADOW: OnceLock<Option<PathBuf>> = OnceLock::new();
SHADOW
.get_or_init(|| {
let search = std::env::var_os("DYLD_LIBRARY_PATH")?;
let shadow = shadowing_file(&search, |candidate| candidate.is_file())?;
log::warn!(
"frust-native-widgets: macOS Image slots stay empty — DYLD_LIBRARY_PATH \
replaced ImageIO's private codec with {} (dyld matches library leaf names, \
case-insensitively on APFS), leaving ImageIO's `__cg_*` imports unbound, so \
any NSImage decode would crash the process (SIGBUS at 0xbad4007). Relaunch \
without DYLD_LIBRARY_PATH to show images",
shadow.display()
);
Some(shadow)
})
.as_deref()
}
fn shadowing_file(search: &OsStr, exists: impl Fn(&Path) -> bool) -> Option<PathBuf> {
std::env::split_paths(search)
.filter(|dir| !dir.as_os_str().is_empty())
.flat_map(|dir| IMAGEIO_CODEC_LEAVES.iter().map(move |leaf| dir.join(leaf)))
.find(|candidate| exists(candidate))
}
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use std::path::Path;
use super::super::ImageBytes;
use super::{IMAGEIO_CODEC_LEAVES, next_tinted, shadowing_file};
use crate::controls::Setter;
#[test]
fn an_image_tint_setter_recomputes_tinted_from_the_new_colour() {
assert!(next_tinted(false, &Setter::ImageTint(Some(128))));
assert!(!next_tinted(true, &Setter::ImageTint(None)));
}
#[test]
fn an_image_bytes_setter_never_changes_the_tinted_flag() {
let empty = ImageBytes::empty();
assert!(
!next_tinted(false, &Setter::ImageBytes(&empty)),
"untinted stays untinted across a bytes change"
);
assert!(
next_tinted(true, &Setter::ImageBytes(&empty)),
"a bytes change alone must not clear an active tint"
);
}
#[test]
fn a_tint_survives_a_bytes_change_that_follows_it() {
let empty = ImageBytes::empty();
let tinted = next_tinted(false, &Setter::ImageTint(Some(128)));
assert!(tinted);
let after_bytes = next_tinted(tinted, &Setter::ImageBytes(&empty));
assert!(after_bytes, "the tint must persist across the bytes swap");
}
#[test]
fn clearing_the_tint_after_a_bytes_change_still_untints() {
let empty = ImageBytes::empty();
let tinted = next_tinted(false, &Setter::ImageTint(Some(128)));
let after_bytes = next_tinted(tinted, &Setter::ImageBytes(&empty));
let cleared = next_tinted(after_bytes, &Setter::ImageTint(None));
assert!(!cleared);
}
#[test]
fn a_search_path_without_a_codec_leaf_shadows_nothing() {
let search = OsStr::new("/opt/homebrew/lib:/usr/local/lib");
assert_eq!(shadowing_file(search, |_| false), None);
}
#[test]
fn the_first_entry_holding_a_codec_leaf_is_the_shadow() {
let search = OsStr::new("/nothing/here::/opt/homebrew/lib:/usr/local/lib");
let found = shadowing_file(search, |candidate| {
candidate.parent() != Some(Path::new("/nothing/here"))
&& candidate
.file_name()
.and_then(OsStr::to_str)
.is_some_and(|leaf| leaf.eq_ignore_ascii_case("libpng.dylib"))
});
assert_eq!(
found.as_deref(),
Some(Path::new("/opt/homebrew/lib/libPng.dylib"))
);
}
#[test]
fn an_empty_search_path_entry_is_skipped_not_read_as_the_working_directory() {
let found = shadowing_file(OsStr::new("::"), |candidate| {
candidate
.parent()
.is_none_or(|dir| dir.as_os_str().is_empty())
});
assert_eq!(found, None);
}
#[test]
fn every_codec_leaf_is_looked_up() {
for leaf in IMAGEIO_CODEC_LEAVES {
let found = shadowing_file(OsStr::new("/x"), |candidate| {
candidate.file_name() == Some(OsStr::new(leaf))
});
assert_eq!(found, Some(Path::new("/x").join(leaf)));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::with_identity;
fn decode(slot: SlotId, body: &str) -> ImageProps {
let raw = with_identity(KIND, slot, body);
ImageProps::decode(&Params::new(&raw)).expect("decodes")
}
fn payload(byte: u8) -> Arc<[u8]> {
Arc::from(vec![byte; 8].into_boxed_slice())
}
#[test]
fn a_slot_with_nothing_published_decodes_to_an_empty_image() {
let props = decode(900, "");
assert_eq!(props, ImageProps::platform_default(900));
assert!(props.bytes.as_slice().is_none());
assert!(ImageProps::plan(&ImageProps::platform_default(900), &props).is_empty());
}
#[test]
fn publishing_the_same_arc_twice_keeps_one_revision_and_plans_no_decode() {
let slot = 901;
let bytes = payload(1);
let first = publish_bytes(slot, Arc::clone(&bytes));
let second = publish_bytes(slot, Arc::clone(&bytes));
assert_eq!(first, second, "identity, not content, drives the revision");
let old = decode(slot, &format!("\"imageRev\":{first}"));
let new = decode(slot, &format!("\"imageRev\":{second}"));
assert_eq!(old, new);
assert!(
ImageProps::plan(&old, &new).is_empty(),
"an unchanged Arc never re-decodes"
);
release_bytes(slot);
}
#[test]
fn a_different_arc_bumps_the_revision_and_plans_exactly_one_decode() {
let slot = 902;
let first_rev = publish_bytes(slot, payload(1));
let old = decode(slot, &format!("\"imageRev\":{first_rev}"));
let second_rev = publish_bytes(slot, payload(2));
assert_ne!(first_rev, second_rev);
let new = decode(slot, &format!("\"imageRev\":{second_rev}"));
let plan = ImageProps::plan(&old, &new);
assert_eq!(plan, vec![Setter::ImageBytes(&new.bytes)]);
assert_eq!(plan[0].tier(), super::super::Tier::Decode);
release_bytes(slot);
}
#[test]
fn identical_content_in_a_fresh_arc_is_a_new_identity_by_design() {
let slot = 903;
let first = publish_bytes(slot, payload(7));
let old = decode(slot, "");
let second = publish_bytes(slot, payload(7));
let new = decode(slot, "");
assert_ne!(first, second);
assert_eq!(ImageProps::plan(&old, &new).len(), 1);
release_bytes(slot);
}
#[test]
fn the_create_plan_sets_exactly_the_non_default_fields() {
let slot = 904;
publish_bytes(slot, payload(3));
let props = decode(
slot,
"\"fit\":\"cover\",\"tint\":128,\"contentDescription\":\"avatar\"",
);
assert_eq!(
ImageProps::plan(&ImageProps::platform_default(slot), &props),
vec![
Setter::ImageBytes(&props.bytes),
Setter::ScaleType(Fit::Cover),
Setter::ImageTint(Some(128)),
Setter::ContentDescription(Some("avatar")),
]
);
release_bytes(slot);
}
#[test]
fn an_unknown_fit_spelling_degrades_to_the_platform_default() {
let props = decode(905, "\"fit\":\"stretch-ish\"");
assert_eq!(props.fit, Fit::Contain);
assert_eq!(decode(905, "\"fit\":\"center\"").fit, Fit::Center);
}
#[test]
fn a_replaced_instances_dispose_never_deletes_the_replacements_bytes() {
let slot = 906;
let rev = publish_bytes(slot, payload(1));
claim_bytes(slot); claim_bytes(slot);
release_bytes(slot); assert_eq!(
decode(slot, "").bytes.rev(),
rev,
"the replacement's bytes survive"
);
release_bytes(slot); assert!(decode(slot, "").bytes.as_slice().is_none());
}
#[test]
fn replacing_a_payload_keeps_the_slots_existing_hold() {
let slot = 907;
publish_bytes(slot, payload(1));
claim_bytes(slot);
let replaced = publish_bytes(slot, payload(2));
assert_eq!(decode(slot, "").bytes.rev(), replaced);
release_bytes(slot);
assert!(decode(slot, "").bytes.as_slice().is_none());
}
#[test]
fn releasing_a_slot_nobody_published_is_a_silent_no_op() {
release_bytes(908);
assert!(decode(908, "").bytes.as_slice().is_none());
}
#[test]
fn the_culled_dispose_then_republish_leak_is_reaped_on_component_teardown() {
let slot = 950;
let r1 = publish_bytes(slot, payload(1));
claim_bytes(slot);
assert_eq!(decode(slot, "").bytes.rev(), r1, "R1 is live after claim");
release_bytes(slot);
assert!(
decode(slot, "").bytes.as_slice().is_none(),
"the culled dispose emptied the table via the ordinary counted path"
);
let r2 = publish_bytes(slot, payload(2));
assert_ne!(r1, r2);
assert_eq!(
decode(slot, "").bytes.rev(),
r2,
"R2 is published but has zero holders — nothing native ever \
claimed it after the culled dispose"
);
retire(slot);
assert!(
decode(slot, "").bytes.as_slice().is_none(),
"retire reaps the R2 entry on Component teardown even though \
nothing native ever claimed it — without `retire`, R2 would \
leak for the process lifetime (a defect that once shipped)"
);
}
#[test]
fn the_ordinary_counted_path_still_works_with_no_premature_removal_while_a_claim_is_live() {
let slot = 951;
let rev = publish_bytes(slot, payload(3));
claim_bytes(slot);
assert_eq!(
decode(slot, "").bytes.rev(),
rev,
"a live native claim must not be prematurely removed"
);
release_bytes(slot);
assert!(
decode(slot, "").bytes.as_slice().is_none(),
"the ordinary counted release path still empties the table on its own"
);
}
#[test]
fn retire_is_unconditional_and_reaps_a_live_claim_too() {
let slot = 952;
publish_bytes(slot, payload(4));
claim_bytes(slot);
retire(slot);
assert!(decode(slot, "").bytes.as_slice().is_none());
release_bytes(slot);
assert!(decode(slot, "").bytes.as_slice().is_none());
}
#[test]
fn retire_on_an_already_empty_slot_is_a_silent_no_op() {
retire(953);
assert!(decode(953, "").bytes.as_slice().is_none());
}
#[test]
fn a_late_claim_after_retire_no_ops_rather_than_resurrecting_the_entry() {
let slot = 954;
publish_bytes(slot, payload(5));
retire(slot);
claim_bytes(slot);
assert!(decode(slot, "").bytes.as_slice().is_none());
}
}