Skip to main content

maplibre_native/
resource.rs

1use std::cell::Cell;
2use std::fmt;
3use std::marker::PhantomData;
4use std::os::raw::{c_char, c_void};
5use std::panic::{AssertUnwindSafe, catch_unwind};
6use std::ptr;
7use std::sync::Arc;
8
9use maplibre_native_core as maplibre_core;
10use maplibre_native_sys as sys;
11
12use crate::Result;
13
14pub use maplibre_core::resource::{
15    ByteRange, ResourceProviderDecision, ResourceRequest, ResourceResponse,
16    ResourceTransformRequest,
17};
18
19use maplibre_core::resource::{
20    ResourceRequestHandleFns, ResourceRequestHandleState, UNKNOWN_PROVIDER_DECISION,
21};
22
23/// Owned handle for a resource provider request selected for handling.
24///
25/// The handle may be sent to another thread for deferred completion. It is
26/// one-shot: call `complete` once to provide a response, or `close`/drop it to
27/// release the provider's reference without completing.
28pub struct ResourceRequestHandle {
29    state: Arc<ResourceRequestHandleState>,
30    _not_sync: PhantomData<Cell<()>>,
31}
32
33impl fmt::Debug for ResourceRequestHandle {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.debug_struct("ResourceRequestHandle")
36            .finish_non_exhaustive()
37    }
38}
39
40impl ResourceRequestHandle {
41    fn from_state(state: Arc<ResourceRequestHandleState>) -> Self {
42        Self {
43            state,
44            _not_sync: PhantomData,
45        }
46    }
47
48    fn from_raw_with_fns(
49        handle: *mut sys::mln_resource_request_handle,
50        fns: ResourceRequestHandleFns,
51    ) -> Result<Self> {
52        // SAFETY: handle is received from the resource-provider C callback and
53        // fns matches that native handle type.
54        unsafe { ResourceRequestHandleState::new(handle, fns) }.map(Self::from_state)
55    }
56
57    /// Completes the request. A completion attempt that reaches native code
58    /// closes this handle, even when native reports a non-OK status. Binding
59    /// validation failures that happen before native code is called leave the
60    /// handle live so completion can be retried with a valid response.
61    ///
62    /// When a provider callback completes inline and then returns
63    /// [`ResourceProviderDecision::PassThrough`], the wrapper still returns the
64    /// native `Handle` decision. Native code must not also pass the completed
65    /// request through to its own networking path.
66    pub fn complete(&self, response: ResourceResponse) -> Result<()> {
67        self.state.complete(&response)
68    }
69
70    /// Reports whether native code has cancelled the request.
71    pub fn is_cancelled(&self) -> Result<bool> {
72        self.state.is_cancelled()
73    }
74
75    /// Releases the provider-owned request handle without completing it.
76    pub fn close(self) {
77        self.state.close();
78    }
79}
80
81impl Drop for ResourceRequestHandle {
82    fn drop(&mut self) {
83        self.state.close();
84    }
85}
86
87type ResourceProviderCallback = dyn Fn(ResourceRequest, ResourceRequestHandle) -> ResourceProviderDecision
88    + Send
89    + Sync
90    + 'static;
91
92pub(crate) struct ResourceProviderState {
93    callback: Box<ResourceProviderCallback>,
94    handle_fns: ResourceRequestHandleFns,
95}
96
97impl fmt::Debug for ResourceProviderState {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.debug_struct("ResourceProviderState")
100            .finish_non_exhaustive()
101    }
102}
103
104impl ResourceProviderState {
105    pub(crate) fn new<F>(callback: F) -> Box<Self>
106    where
107        F: Fn(ResourceRequest, ResourceRequestHandle) -> ResourceProviderDecision
108            + Send
109            + Sync
110            + 'static,
111    {
112        Box::new(Self {
113            callback: Box::new(callback),
114            handle_fns: ResourceRequestHandleFns::NATIVE,
115        })
116    }
117
118    pub(crate) fn descriptor(&self) -> sys::mln_resource_provider {
119        maplibre_core::resource::resource_provider_descriptor(
120            Some(resource_provider_trampoline),
121            ptr::from_ref(self).cast_mut().cast::<c_void>(),
122        )
123    }
124
125    fn invoke(
126        &self,
127        request: *const sys::mln_resource_request,
128        handle: *mut sys::mln_resource_request_handle,
129    ) -> u32 {
130        let Some(raw_request) = ptr::NonNull::new(request.cast_mut()) else {
131            return UNKNOWN_PROVIDER_DECISION;
132        };
133        let handle = match ResourceRequestHandle::from_raw_with_fns(handle, self.handle_fns) {
134            Ok(handle) => handle,
135            Err(_) => return UNKNOWN_PROVIDER_DECISION,
136        };
137        let state = Arc::clone(&handle.state);
138
139        // SAFETY: raw_request is non-null and borrowed for the callback duration.
140        let request =
141            match unsafe { maplibre_core::resource::copy_resource_request(raw_request.as_ref()) } {
142                Ok(request) => request,
143                Err(_) => return state.finish_provider_exception(),
144            };
145
146        match catch_unwind(AssertUnwindSafe(|| (self.callback)(request, handle))) {
147            Ok(decision) => state.finish_provider_decision(decision),
148            Err(_) => state.finish_provider_exception(),
149        }
150    }
151}
152
153unsafe extern "C" fn resource_provider_trampoline(
154    user_data: *mut c_void,
155    request: *const sys::mln_resource_request,
156    handle: *mut sys::mln_resource_request_handle,
157) -> u32 {
158    let Some(state) = ptr::NonNull::new(user_data.cast::<ResourceProviderState>()) else {
159        return UNKNOWN_PROVIDER_DECISION;
160    };
161    // SAFETY: user_data is installed from ResourceProviderState::descriptor and
162    // remains valid until replacement or runtime teardown. The callback state is
163    // Send + Sync because native may invoke it from worker/network threads.
164    unsafe { state.as_ref() }.invoke(request, handle)
165}
166
167type ResourceTransformCallback =
168    dyn Fn(ResourceTransformRequest) -> Option<String> + Send + Sync + 'static;
169
170pub(crate) struct ResourceTransformState {
171    callback: Box<ResourceTransformCallback>,
172}
173
174impl fmt::Debug for ResourceTransformState {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        f.debug_struct("ResourceTransformState")
177            .finish_non_exhaustive()
178    }
179}
180
181impl ResourceTransformState {
182    pub(crate) fn new<F>(callback: F) -> Box<Self>
183    where
184        F: Fn(ResourceTransformRequest) -> Option<String> + Send + Sync + 'static,
185    {
186        Box::new(Self {
187            callback: Box::new(callback),
188        })
189    }
190
191    pub(crate) fn descriptor(&self) -> sys::mln_resource_transform {
192        maplibre_core::resource::resource_transform_descriptor(
193            Some(resource_transform_trampoline),
194            ptr::from_ref(self).cast_mut().cast::<c_void>(),
195        )
196    }
197
198    fn invoke(
199        &self,
200        raw_kind: u32,
201        url: *const c_char,
202        out_response: *mut sys::mln_resource_transform_response,
203    ) -> sys::mln_status {
204        // SAFETY: out_response is callback-duration output storage provided by
205        // native; core validates null before initializing it.
206        let status = unsafe {
207            maplibre_core::resource::initialize_resource_transform_response(out_response)
208        };
209        if status != sys::MLN_STATUS_OK {
210            return status;
211        }
212
213        // SAFETY: url is borrowed for the callback duration by the C API.
214        let request = match unsafe {
215            maplibre_core::resource::copy_resource_transform_request(raw_kind, url)
216        } {
217            Ok(request) => request,
218            Err(error) => return maplibre_core::resource::status_for_error(&error),
219        };
220
221        let replacement = match catch_unwind(AssertUnwindSafe(|| (self.callback)(request))) {
222            Ok(replacement) => replacement,
223            Err(_) => return sys::MLN_STATUS_NATIVE_ERROR,
224        };
225
226        match replacement {
227            Some(replacement) if !replacement.is_empty() => {
228                // SAFETY: out_response was checked by
229                // initialize_resource_transform_response. The helper copies the
230                // temporary Rust string into C API-managed callback scratch
231                // storage that remains live while native copies it after this
232                // trampoline returns.
233                unsafe {
234                    sys::mln_resource_transform_response_set_url(
235                        out_response,
236                        replacement.as_ptr().cast(),
237                        replacement.len(),
238                    )
239                }
240            }
241            _ => sys::MLN_STATUS_OK,
242        }
243    }
244}
245
246unsafe extern "C" fn resource_transform_trampoline(
247    user_data: *mut c_void,
248    kind: u32,
249    url: *const c_char,
250    out_response: *mut sys::mln_resource_transform_response,
251) -> sys::mln_status {
252    let Some(state) = ptr::NonNull::new(user_data.cast::<ResourceTransformState>()) else {
253        return sys::MLN_STATUS_INVALID_ARGUMENT;
254    };
255    // SAFETY: user_data is installed from ResourceTransformState::descriptor
256    // and remains valid until the runtime replaces/clears the transform or is
257    // destroyed. The callback state itself is Send + Sync.
258    unsafe { state.as_ref() }.invoke(kind, url, out_response)
259}
260
261#[cfg(test)]
262mod tests {
263    use std::ffi::CString;
264    use std::sync::Mutex as StdMutex;
265    use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
266
267    use static_assertions::{assert_impl_all, assert_not_impl_any};
268
269    use super::*;
270    use crate::{
271        ErrorKind, ResourceKind, ResourceLoadingMethod, ResourcePriority, ResourceStoragePolicy,
272        ResourceUsage,
273    };
274
275    static FAKE_HANDLE_TEST_LOCK: StdMutex<()> = StdMutex::new(());
276    static COMPLETE_COUNT: AtomicUsize = AtomicUsize::new(0);
277    static CANCELLED_COUNT: AtomicUsize = AtomicUsize::new(0);
278    static RELEASE_COUNT: AtomicUsize = AtomicUsize::new(0);
279    static CANCELLED_VALUE: AtomicBool = AtomicBool::new(false);
280    static COMPLETE_STATUS: AtomicI32 = AtomicI32::new(sys::MLN_STATUS_OK);
281
282    unsafe extern "C" fn fake_complete(
283        _handle: *mut sys::mln_resource_request_handle,
284        _response: *const sys::mln_resource_response,
285    ) -> sys::mln_status {
286        COMPLETE_COUNT.fetch_add(1, Ordering::SeqCst);
287        COMPLETE_STATUS.load(Ordering::SeqCst)
288    }
289
290    unsafe extern "C" fn fake_cancelled(
291        _handle: *const sys::mln_resource_request_handle,
292        out_cancelled: *mut bool,
293    ) -> sys::mln_status {
294        CANCELLED_COUNT.fetch_add(1, Ordering::SeqCst);
295        if out_cancelled.is_null() {
296            return sys::MLN_STATUS_INVALID_ARGUMENT;
297        }
298        // SAFETY: out_cancelled is non-null and points to caller-owned output storage.
299        unsafe { *out_cancelled = CANCELLED_VALUE.load(Ordering::SeqCst) };
300        sys::MLN_STATUS_OK
301    }
302
303    unsafe extern "C" fn fake_release(_handle: *mut sys::mln_resource_request_handle) {
304        RELEASE_COUNT.fetch_add(1, Ordering::SeqCst);
305    }
306
307    fn fake_fns() -> ResourceRequestHandleFns {
308        // SAFETY: These fake functions implement the same ownership contract as
309        // the native handle functions for tests.
310        unsafe { ResourceRequestHandleFns::new(fake_complete, fake_cancelled, fake_release) }
311    }
312
313    fn reset_fake_handle_state() {
314        COMPLETE_COUNT.store(0, Ordering::SeqCst);
315        CANCELLED_COUNT.store(0, Ordering::SeqCst);
316        RELEASE_COUNT.store(0, Ordering::SeqCst);
317        CANCELLED_VALUE.store(false, Ordering::SeqCst);
318        COMPLETE_STATUS.store(sys::MLN_STATUS_OK, Ordering::SeqCst);
319    }
320
321    fn fake_handle() -> ResourceRequestHandle {
322        reset_fake_handle_state();
323        ResourceRequestHandle::from_raw_with_fns(
324            0x1234usize as *mut sys::mln_resource_request_handle,
325            fake_fns(),
326        )
327        .unwrap()
328    }
329
330    fn request() -> sys::mln_resource_request {
331        sys::mln_resource_request {
332            size: std::mem::size_of::<sys::mln_resource_request>() as u32,
333            url: c"https://example.test/tile.pbf".as_ptr(),
334            kind: sys::MLN_RESOURCE_KIND_TILE,
335            loading_method: sys::MLN_RESOURCE_LOADING_METHOD_NETWORK_ONLY,
336            priority: sys::MLN_RESOURCE_PRIORITY_LOW,
337            usage: sys::MLN_RESOURCE_USAGE_OFFLINE,
338            storage_policy: sys::MLN_RESOURCE_STORAGE_POLICY_VOLATILE,
339            has_range: true,
340            range_start: 7,
341            range_end: 11,
342            has_prior_modified: true,
343            prior_modified_unix_ms: 123,
344            has_prior_expires: true,
345            prior_expires_unix_ms: 456,
346            prior_etag: c"etag".as_ptr(),
347            prior_data: [1u8, 2, 3].as_ptr(),
348            prior_data_size: 3,
349        }
350    }
351
352    fn response() -> sys::mln_resource_transform_response {
353        sys::mln_resource_transform_response {
354            size: std::mem::size_of::<sys::mln_resource_transform_response>() as u32,
355            url: ptr::null(),
356            context: ptr::null_mut(),
357        }
358    }
359
360    #[test]
361    // Rust regression: Rust request handles are transferable for deferred
362    // completion, but shared references are not safe to use concurrently.
363    fn resource_request_handle_is_send_but_not_sync() {
364        assert_impl_all!(ResourceRequestHandle: Send);
365        assert_not_impl_any!(ResourceRequestHandle: Sync);
366    }
367
368    #[test]
369    // Spec coverage: BND-142.
370    fn provider_callback_copies_request_and_pass_through_does_not_release() {
371        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
372        reset_fake_handle_state();
373        let state = ResourceProviderState {
374            callback: Box::new(|request, handle| {
375                assert_eq!(request.url, "https://example.test/tile.pbf");
376                assert_eq!(request.kind, ResourceKind::Tile);
377                assert_eq!(request.raw_kind, sys::MLN_RESOURCE_KIND_TILE);
378                assert_eq!(request.loading_method, ResourceLoadingMethod::NetworkOnly);
379                assert_eq!(request.priority, ResourcePriority::Low);
380                assert_eq!(request.usage, ResourceUsage::Offline);
381                assert_eq!(request.storage_policy, ResourceStoragePolicy::Volatile);
382                let range = request.range.unwrap();
383                assert_eq!((range.start, range.end), (7, 11));
384                assert_eq!(request.prior_modified_unix_ms, Some(123));
385                assert_eq!(request.prior_expires_unix_ms, Some(456));
386                assert_eq!(request.prior_etag.as_deref(), Some("etag"));
387                assert_eq!(request.prior_data, vec![1, 2, 3]);
388                drop(handle);
389                ResourceProviderDecision::PassThrough
390            }),
391            handle_fns: fake_fns(),
392        };
393        let raw_request = request();
394
395        let decision = state.invoke(
396            &raw_request,
397            0x1234usize as *mut sys::mln_resource_request_handle,
398        );
399
400        assert_eq!(decision, sys::MLN_RESOURCE_PROVIDER_DECISION_PASS_THROUGH);
401        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
402    }
403
404    #[test]
405    // Spec coverage: BND-151.
406    fn retained_pass_through_request_handle_is_stale_after_callback() {
407        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
408        reset_fake_handle_state();
409        let (sender, receiver) = std::sync::mpsc::channel();
410        let state = ResourceProviderState {
411            callback: Box::new(move |_, handle| {
412                sender.send(handle).unwrap();
413                ResourceProviderDecision::PassThrough
414            }),
415            handle_fns: fake_fns(),
416        };
417        let raw_request = request();
418
419        let decision = state.invoke(
420            &raw_request,
421            0x1234usize as *mut sys::mln_resource_request_handle,
422        );
423        let handle = receiver.recv().unwrap();
424
425        assert_eq!(decision, sys::MLN_RESOURCE_PROVIDER_DECISION_PASS_THROUGH);
426        let error = handle.is_cancelled().unwrap_err();
427        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
428        let error = handle.complete(ResourceResponse::no_content()).unwrap_err();
429        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
430        handle.close();
431        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 0);
432        assert_eq!(CANCELLED_COUNT.load(Ordering::SeqCst), 0);
433        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
434    }
435
436    #[test]
437    // Spec coverage: BND-143 and BND-150.
438    fn inline_completion_returns_handle_and_releases_once() {
439        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
440        reset_fake_handle_state();
441        let state = ResourceProviderState {
442            callback: Box::new(|_, handle| {
443                handle.complete(ResourceResponse::ok([1, 2, 3])).unwrap();
444                ResourceProviderDecision::PassThrough
445            }),
446            handle_fns: fake_fns(),
447        };
448        let raw_request = request();
449
450        let decision = state.invoke(
451            &raw_request,
452            0x1234usize as *mut sys::mln_resource_request_handle,
453        );
454
455        assert_eq!(decision, sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE);
456        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
457        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
458    }
459
460    #[test]
461    // Spec coverage: BND-144 and BND-145.
462    fn deferred_completion_from_another_thread_releases_once() {
463        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
464        let handle = fake_handle();
465        assert_eq!(
466            handle
467                .state
468                .finish_provider_decision(ResourceProviderDecision::Handle),
469            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
470        );
471
472        let thread = std::thread::spawn(move || {
473            handle
474                .complete(ResourceResponse::ok(vec![4, 5, 6]))
475                .unwrap();
476        });
477        thread.join().unwrap();
478
479        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
480        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
481    }
482
483    #[test]
484    // Spec coverage: BND-152.
485    fn failed_completion_is_terminal_after_reaching_c() {
486        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
487        let handle = fake_handle();
488        assert_eq!(
489            handle
490                .state
491                .finish_provider_decision(ResourceProviderDecision::Handle),
492            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
493        );
494        COMPLETE_STATUS.store(sys::MLN_STATUS_INVALID_STATE, Ordering::SeqCst);
495
496        let error = handle
497            .complete(ResourceResponse::ok(Vec::new()))
498            .unwrap_err();
499
500        assert_eq!(error.kind(), ErrorKind::InvalidState);
501        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
502        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
503        let error = handle.complete(ResourceResponse::no_content()).unwrap_err();
504        assert_eq!(error.kind(), ErrorKind::InvalidState);
505        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
506    }
507
508    #[test]
509    // Spec coverage: BND-146.
510    fn validation_failure_before_completion_keeps_handle_retryable() {
511        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
512        let handle = fake_handle();
513        assert_eq!(
514            handle
515                .state
516                .finish_provider_decision(ResourceProviderDecision::Handle),
517            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
518        );
519
520        let error = handle
521            .complete(ResourceResponse::error(
522                crate::ResourceErrorReason::Other,
523                "bad\0message",
524            ))
525            .unwrap_err();
526
527        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
528        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 0);
529        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
530        handle.complete(ResourceResponse::no_content()).unwrap();
531        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
532        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
533    }
534
535    #[test]
536    // Spec coverage: BND-147 and BND-153.
537    fn drop_releases_uncompleted_handled_request_once() {
538        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
539        let handle = fake_handle();
540        assert_eq!(
541            handle
542                .state
543                .finish_provider_decision(ResourceProviderDecision::Handle),
544            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
545        );
546        drop(handle);
547
548        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
549    }
550
551    #[test]
552    // Spec coverage: BND-153.
553    fn close_before_handle_decision_releases_after_decision() {
554        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
555        let handle = fake_handle();
556        let state = Arc::clone(&handle.state);
557        handle.close();
558        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
559        assert_eq!(
560            state.finish_provider_decision(ResourceProviderDecision::Handle),
561            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
562        );
563
564        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
565    }
566
567    #[test]
568    // Spec coverage: BND-147.
569    fn cancelled_queries_use_native_function_until_closed() {
570        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
571        let handle = fake_handle();
572        CANCELLED_VALUE.store(true, Ordering::SeqCst);
573
574        assert!(handle.is_cancelled().unwrap());
575        handle.close();
576    }
577
578    #[test]
579    // Spec coverage: BND-023 and BND-148.
580    fn late_completion_after_observed_cancellation_maps_native_status_and_closes() {
581        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
582        let handle = fake_handle();
583        assert_eq!(
584            handle
585                .state
586                .finish_provider_decision(ResourceProviderDecision::Handle),
587            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
588        );
589        CANCELLED_VALUE.store(true, Ordering::SeqCst);
590
591        assert!(handle.is_cancelled().unwrap());
592        COMPLETE_STATUS.store(sys::MLN_STATUS_INVALID_STATE, Ordering::SeqCst);
593        let error = handle.complete(ResourceResponse::no_content()).unwrap_err();
594
595        assert_eq!(error.kind(), ErrorKind::InvalidState);
596        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
597        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
598        let error = handle.complete(ResourceResponse::no_content()).unwrap_err();
599        assert_eq!(error.kind(), ErrorKind::InvalidState);
600        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
601    }
602
603    #[test]
604    // Spec coverage: BND-121.
605    fn provider_panics_produce_unknown_decision_without_unwinding() {
606        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
607        reset_fake_handle_state();
608        let state = ResourceProviderState {
609            callback: Box::new(|_, _| panic!("boom")),
610            handle_fns: fake_fns(),
611        };
612        let raw_request = request();
613
614        let decision = state.invoke(
615            &raw_request,
616            0x1234usize as *mut sys::mln_resource_request_handle,
617        );
618
619        assert_eq!(decision, UNKNOWN_PROVIDER_DECISION);
620        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
621    }
622
623    #[test]
624    // Spec coverage: BND-141.
625    fn transform_callback_copies_request_when_keeping_original_url() {
626        let state = ResourceTransformState::new(|request| {
627            assert_eq!(request.kind, ResourceKind::Style);
628            assert_eq!(request.raw_kind, sys::MLN_RESOURCE_KIND_STYLE);
629            assert_eq!(request.url, "https://example.test/style.json");
630            None
631        });
632        let descriptor = state.descriptor();
633        let callback = descriptor.callback.unwrap();
634        let url = CString::new("https://example.test/style.json").unwrap();
635        let mut response = response();
636
637        let status = unsafe {
638            callback(
639                descriptor.user_data,
640                sys::MLN_RESOURCE_KIND_STYLE,
641                url.as_ptr(),
642                &mut response,
643            )
644        };
645
646        assert_eq!(status, sys::MLN_STATUS_OK);
647        assert!(response.url.is_null());
648    }
649
650    #[test]
651    // Spec coverage: BND-140.
652    fn transform_callback_clears_stale_response_when_keeping_original_url() {
653        let state = ResourceTransformState::new(|_| None);
654        let descriptor = state.descriptor();
655        let callback = descriptor.callback.unwrap();
656        let url = CString::new("https://example.test/style.json").unwrap();
657        let stale = CString::new("https://stale.test/style.json").unwrap();
658        let mut response = response();
659        response.url = stale.as_ptr();
660
661        let status = unsafe {
662            callback(
663                descriptor.user_data,
664                sys::MLN_RESOURCE_KIND_STYLE,
665                url.as_ptr(),
666                &mut response,
667            )
668        };
669
670        assert_eq!(status, sys::MLN_STATUS_OK);
671        assert!(response.url.is_null());
672    }
673
674    #[test]
675    // Spec coverage: BND-121.
676    fn transform_callback_contains_panics() {
677        let state = ResourceTransformState::new(|_| panic!("boom"));
678        let descriptor = state.descriptor();
679        let callback = descriptor.callback.unwrap();
680        let url = CString::new("https://example.test/style.json").unwrap();
681        let mut response = response();
682
683        let status = unsafe {
684            callback(
685                descriptor.user_data,
686                sys::MLN_RESOURCE_KIND_STYLE,
687                url.as_ptr(),
688                &mut response,
689            )
690        };
691
692        assert_eq!(status, sys::MLN_STATUS_NATIVE_ERROR);
693        assert!(response.url.is_null());
694    }
695
696    #[test]
697    // Spec coverage: BND-024.
698    fn transform_callback_rejects_embedded_nul_replacements() {
699        let state = ResourceTransformState::new(|_| Some("https://example.test/\0bad".to_owned()));
700        let descriptor = state.descriptor();
701        let callback = descriptor.callback.unwrap();
702        let url = CString::new("https://example.test/style.json").unwrap();
703        let mut response = response();
704
705        let status = unsafe {
706            callback(
707                descriptor.user_data,
708                sys::MLN_RESOURCE_KIND_STYLE,
709                url.as_ptr(),
710                &mut response,
711            )
712        };
713
714        assert_eq!(status, sys::MLN_STATUS_INVALID_ARGUMENT);
715        assert!(response.url.is_null());
716    }
717
718    #[test]
719    // Rust regression: proves callback captures are released by the Rust
720    // transform-state implementation after native unregistration.
721    fn transform_state_drops_callback_capture() {
722        let token = Arc::new(());
723        let callback_token = Arc::clone(&token);
724        let state = ResourceTransformState::new(move |_| {
725            let _ = &callback_token;
726            None
727        });
728        assert_eq!(Arc::strong_count(&token), 2);
729        drop(state);
730        assert_eq!(Arc::strong_count(&token), 1);
731    }
732
733    #[test]
734    // Spec coverage: BND-123.
735    fn callback_can_run_from_multiple_threads() {
736        let calls = Arc::new(AtomicUsize::new(0));
737        let callback_calls = Arc::clone(&calls);
738        let state = Arc::new(ResourceTransformState {
739            callback: Box::new(move |request| {
740                callback_calls.fetch_add(1, Ordering::SeqCst);
741                let _ = request;
742                None
743            }),
744        });
745
746        let handles = (0..2)
747            .map(|_| {
748                let state = Arc::clone(&state);
749                std::thread::spawn(move || {
750                    let descriptor = state.descriptor();
751                    let callback = descriptor.callback.unwrap();
752                    let url = CString::new("https://example.test/tile").unwrap();
753                    let mut response = response();
754                    let status = unsafe {
755                        callback(
756                            descriptor.user_data,
757                            sys::MLN_RESOURCE_KIND_TILE,
758                            url.as_ptr(),
759                            &mut response,
760                        )
761                    };
762                    assert_eq!(status, sys::MLN_STATUS_OK);
763                    assert!(response.url.is_null());
764                })
765            })
766            .collect::<Vec<_>>();
767
768        for handle in handles {
769            handle.join().unwrap();
770        }
771        assert_eq!(calls.load(Ordering::SeqCst), 2);
772    }
773}