Skip to main content

maplibre_native_ffi/
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_ffi_core as maplibre_core;
10use maplibre_native_ffi_sys as sys;
11
12use crate::Result;
13
14pub use maplibre_core::resource::{
15    ByteRange, HttpHeader, HttpHeaderTransformRequest, ResourceProviderDecision, ResourceRequest,
16    ResourceResponse, 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: 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; a binding
59    /// validation failure leaves the handle live so completion can be retried.
60    ///
61    /// A callback that completes inline and then returns
62    /// [`ResourceProviderDecision::PassThrough`] still reports the native
63    /// `Handle` decision.
64    pub fn complete(&self, response: ResourceResponse) -> Result<()> {
65        self.state.complete(&response)
66    }
67
68    /// Reports whether native code has cancelled the request.
69    pub fn is_cancelled(&self) -> Result<bool> {
70        self.state.is_cancelled()
71    }
72
73    /// Registers the callback that runs when MapLibre cancels this request.
74    ///
75    /// A request accepts one registration; a second one reports
76    /// [`ErrorKind::InvalidState`](crate::ErrorKind::InvalidState). MapLibre
77    /// runs the callback at most once, on the thread that discards the request,
78    /// and never for a request the provider completed. Registering on a request
79    /// MapLibre already cancelled runs the callback before this call returns, as
80    /// part of this call: a concurrent `close` on another thread does not wait
81    /// for it. A
82    /// panic inside the callback is contained and discarded, because unwinding
83    /// into native code is undefined behavior and the cancel path reports no
84    /// status.
85    ///
86    /// The callback may complete or close this same request, which is how a
87    /// host retires it early: share the handle through `Arc<Mutex<Option<_>>>`
88    /// and take it from inside the callback. It must not use the map or runtime
89    /// the request came from. The request owns the callback until it runs or
90    /// the request is completed or closed, so a callback that captures its own
91    /// handle keeps that handle alive until then: a host that drops such a
92    /// handle without completing or closing it leaks the native request.
93    pub fn set_cancel_callback<F>(&self, callback: F) -> Result<()>
94    where
95        F: FnOnce() + Send + 'static,
96    {
97        self.state.set_cancel_callback(Box::new(callback))
98    }
99
100    /// Releases the provider-owned request handle without completing it.
101    /// Releasing waits for a cancel callback running on another thread, and
102    /// returns without waiting from inside that callback.
103    pub fn close(self) {
104        self.state.close();
105    }
106}
107
108impl Drop for ResourceRequestHandle {
109    fn drop(&mut self) {
110        self.state.close();
111    }
112}
113
114type ResourceProviderCallback = dyn Fn(ResourceRequest, ResourceRequestHandle) -> ResourceProviderDecision
115    + Send
116    + Sync
117    + 'static;
118
119pub(crate) struct ResourceProviderState {
120    callback: Box<ResourceProviderCallback>,
121    handle_fns: ResourceRequestHandleFns,
122}
123
124impl fmt::Debug for ResourceProviderState {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        f.debug_struct("ResourceProviderState")
127            .finish_non_exhaustive()
128    }
129}
130
131impl ResourceProviderState {
132    pub(crate) fn new<F>(callback: F) -> Box<Self>
133    where
134        F: Fn(ResourceRequest, ResourceRequestHandle) -> ResourceProviderDecision
135            + Send
136            + Sync
137            + 'static,
138    {
139        Box::new(Self {
140            callback: Box::new(callback),
141            handle_fns: ResourceRequestHandleFns::NATIVE,
142        })
143    }
144
145    pub(crate) fn descriptor(&self) -> sys::mln_resource_provider {
146        maplibre_core::resource::resource_provider_descriptor(
147            Some(resource_provider_trampoline),
148            ptr::from_ref(self).cast_mut().cast::<c_void>(),
149        )
150    }
151
152    fn invoke(
153        &self,
154        request: *const sys::mln_resource_request,
155        handle: sys::mln_resource_request_handle,
156    ) -> u32 {
157        let Some(raw_request) = ptr::NonNull::new(request.cast_mut()) else {
158            return UNKNOWN_PROVIDER_DECISION;
159        };
160        let handle = match ResourceRequestHandle::from_raw_with_fns(handle, self.handle_fns) {
161            Ok(handle) => handle,
162            Err(_) => return UNKNOWN_PROVIDER_DECISION,
163        };
164        let state = Arc::clone(&handle.state);
165
166        // SAFETY: raw_request is non-null and borrowed for the callback duration.
167        let request =
168            match unsafe { maplibre_core::resource::copy_resource_request(raw_request.as_ref()) } {
169                Ok(request) => request,
170                Err(_) => return state.finish_provider_exception(),
171            };
172
173        match catch_unwind(AssertUnwindSafe(|| (self.callback)(request, handle))) {
174            Ok(decision) => state.finish_provider_decision(decision),
175            Err(_) => state.finish_provider_exception(),
176        }
177    }
178}
179
180unsafe extern "C" fn resource_provider_trampoline(
181    user_data: *mut c_void,
182    request: *const sys::mln_resource_request,
183    handle: sys::mln_resource_request_handle,
184) -> u32 {
185    let Some(state) = ptr::NonNull::new(user_data.cast::<ResourceProviderState>()) else {
186        return UNKNOWN_PROVIDER_DECISION;
187    };
188    // SAFETY: user_data is installed from ResourceProviderState::descriptor and
189    // remains valid until replacement or runtime teardown. Native may invoke
190    // this from worker or network threads.
191    unsafe { state.as_ref() }.invoke(request, handle)
192}
193
194type ResourceTransformCallback =
195    dyn Fn(ResourceTransformRequest) -> Option<String> + Send + Sync + 'static;
196
197pub(crate) struct ResourceTransformState {
198    callback: Box<ResourceTransformCallback>,
199}
200
201impl fmt::Debug for ResourceTransformState {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        f.debug_struct("ResourceTransformState")
204            .finish_non_exhaustive()
205    }
206}
207
208impl ResourceTransformState {
209    pub(crate) fn new<F>(callback: F) -> Box<Self>
210    where
211        F: Fn(ResourceTransformRequest) -> Option<String> + Send + Sync + 'static,
212    {
213        Box::new(Self {
214            callback: Box::new(callback),
215        })
216    }
217
218    pub(crate) fn descriptor(&self) -> sys::mln_resource_transform {
219        maplibre_core::resource::resource_transform_descriptor(
220            Some(resource_transform_trampoline),
221            ptr::from_ref(self).cast_mut().cast::<c_void>(),
222        )
223    }
224
225    fn invoke(
226        &self,
227        raw_kind: u32,
228        url: *const c_char,
229        out_response: *mut sys::mln_resource_transform_response,
230    ) -> sys::mln_status {
231        // SAFETY: out_response is callback-duration output storage provided by
232        // native; core validates null before initializing it.
233        let status = unsafe {
234            maplibre_core::resource::initialize_resource_transform_response(out_response)
235        };
236        if status != sys::MLN_STATUS_OK {
237            return status;
238        }
239
240        // SAFETY: url is borrowed for the callback duration by the C API.
241        let request = match unsafe {
242            maplibre_core::resource::copy_resource_transform_request(raw_kind, url)
243        } {
244            Ok(request) => request,
245            Err(error) => return maplibre_core::resource::status_for_error(&error),
246        };
247
248        let replacement = match catch_unwind(AssertUnwindSafe(|| (self.callback)(request))) {
249            Ok(replacement) => replacement,
250            Err(_) => return sys::MLN_STATUS_NATIVE_ERROR,
251        };
252
253        match replacement {
254            Some(replacement) if !replacement.is_empty() => {
255                // SAFETY: out_response was checked by
256                // initialize_resource_transform_response, and the helper copies
257                // the string into C API-managed scratch storage that outlives
258                // this trampoline.
259                unsafe {
260                    sys::mln_resource_transform_response_set_url(
261                        out_response,
262                        replacement.as_ptr().cast(),
263                        replacement.len(),
264                    )
265                }
266            }
267            _ => sys::MLN_STATUS_OK,
268        }
269    }
270}
271
272unsafe extern "C" fn resource_transform_trampoline(
273    user_data: *mut c_void,
274    kind: u32,
275    url: *const c_char,
276    out_response: *mut sys::mln_resource_transform_response,
277) -> sys::mln_status {
278    let Some(state) = ptr::NonNull::new(user_data.cast::<ResourceTransformState>()) else {
279        return sys::MLN_STATUS_INVALID_ARGUMENT;
280    };
281    // SAFETY: user_data is installed from ResourceTransformState::descriptor
282    // and remains valid until the runtime replaces or clears the transform or
283    // is destroyed.
284    unsafe { state.as_ref() }.invoke(kind, url, out_response)
285}
286
287type HttpHeaderTransformCallback =
288    dyn Fn(HttpHeaderTransformRequest) -> Vec<HttpHeader> + Send + Sync + 'static;
289
290pub(crate) struct HttpHeaderTransformState {
291    callback: Box<HttpHeaderTransformCallback>,
292}
293
294impl fmt::Debug for HttpHeaderTransformState {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        f.debug_struct("HttpHeaderTransformState")
297            .finish_non_exhaustive()
298    }
299}
300
301impl HttpHeaderTransformState {
302    pub(crate) fn new<F>(callback: F) -> Box<Self>
303    where
304        F: Fn(HttpHeaderTransformRequest) -> Vec<HttpHeader> + Send + Sync + 'static,
305    {
306        Box::new(Self {
307            callback: Box::new(callback),
308        })
309    }
310
311    pub(crate) fn descriptor(&self) -> sys::mln_http_header_transform {
312        maplibre_core::resource::http_header_transform_descriptor(
313            Some(http_header_transform_trampoline),
314            ptr::from_ref(self).cast_mut().cast::<c_void>(),
315        )
316    }
317
318    fn invoke(
319        &self,
320        raw_kind: u32,
321        url: *const c_char,
322        out_response: *mut sys::mln_http_header_transform_response,
323    ) -> sys::mln_status {
324        // SAFETY: native supplies writable callback-duration response storage.
325        let status = unsafe {
326            maplibre_core::resource::initialize_http_header_transform_response(out_response)
327        };
328        if status != sys::MLN_STATUS_OK {
329            return status;
330        }
331        // SAFETY: url is borrowed for the callback duration.
332        let request = match unsafe {
333            maplibre_core::resource::copy_http_header_transform_request(raw_kind, url)
334        } {
335            Ok(request) => request,
336            Err(error) => return maplibre_core::resource::status_for_error(&error),
337        };
338        let headers = match catch_unwind(AssertUnwindSafe(|| (self.callback)(request))) {
339            Ok(headers) => headers,
340            Err(_) => return sys::MLN_STATUS_NATIVE_ERROR,
341        };
342
343        let mut names = Vec::<String>::with_capacity(headers.len());
344        for header in headers {
345            if names
346                .iter()
347                .any(|name| name.eq_ignore_ascii_case(&header.name))
348            {
349                return sys::MLN_STATUS_INVALID_ARGUMENT;
350            }
351            names.push(header.name.clone());
352            // SAFETY: native copies both strings during this call.
353            let status = unsafe {
354                sys::mln_http_header_transform_response_set(
355                    out_response,
356                    header.name.as_ptr().cast(),
357                    header.name.len(),
358                    header.value.as_ptr().cast(),
359                    header.value.len(),
360                )
361            };
362            if status != sys::MLN_STATUS_OK {
363                return status;
364            }
365        }
366        sys::MLN_STATUS_OK
367    }
368}
369
370unsafe extern "C" fn http_header_transform_trampoline(
371    user_data: *mut c_void,
372    kind: u32,
373    url: *const c_char,
374    out_response: *mut sys::mln_http_header_transform_response,
375) -> sys::mln_status {
376    let Some(state) = ptr::NonNull::new(user_data.cast::<HttpHeaderTransformState>()) else {
377        return sys::MLN_STATUS_INVALID_ARGUMENT;
378    };
379    // SAFETY: native retains user_data through callback retirement.
380    unsafe { state.as_ref() }.invoke(kind, url, out_response)
381}
382
383#[cfg(test)]
384mod tests {
385    use std::ffi::CString;
386    use std::sync::Mutex as StdMutex;
387    use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
388
389    use static_assertions::{assert_impl_all, assert_not_impl_any};
390
391    use super::*;
392    use crate::{
393        ErrorKind, ResourceKind, ResourceLoadingMethod, ResourcePriority, ResourceStoragePolicy,
394        ResourceUsage,
395    };
396
397    static FAKE_HANDLE_TEST_LOCK: StdMutex<()> = StdMutex::new(());
398    static COMPLETE_COUNT: AtomicUsize = AtomicUsize::new(0);
399    static CANCELLED_COUNT: AtomicUsize = AtomicUsize::new(0);
400    static RELEASE_COUNT: AtomicUsize = AtomicUsize::new(0);
401    static SET_CANCEL_COUNT: AtomicUsize = AtomicUsize::new(0);
402    static CANCELLED_VALUE: AtomicBool = AtomicBool::new(false);
403    static COMPLETE_STATUS: AtomicI32 = AtomicI32::new(sys::MLN_STATUS_OK);
404
405    unsafe extern "C" fn fake_complete(
406        _handle: sys::mln_resource_request_handle,
407        _response: *const sys::mln_resource_response,
408    ) -> sys::mln_status {
409        COMPLETE_COUNT.fetch_add(1, Ordering::SeqCst);
410        COMPLETE_STATUS.load(Ordering::SeqCst)
411    }
412
413    unsafe extern "C" fn fake_cancelled(
414        _handle: sys::mln_resource_request_handle,
415        out_cancelled: *mut bool,
416    ) -> sys::mln_status {
417        CANCELLED_COUNT.fetch_add(1, Ordering::SeqCst);
418        if out_cancelled.is_null() {
419            return sys::MLN_STATUS_INVALID_ARGUMENT;
420        }
421        // SAFETY: out_cancelled is non-null and points to caller-owned output storage.
422        unsafe { *out_cancelled = CANCELLED_VALUE.load(Ordering::SeqCst) };
423        sys::MLN_STATUS_OK
424    }
425
426    unsafe extern "C" fn fake_release(_handle: sys::mln_resource_request_handle) {
427        RELEASE_COUNT.fetch_add(1, Ordering::SeqCst);
428    }
429
430    /// Stands in for the C API's registration, which reports a request
431    /// cancelled before registration instead of storing the callback.
432    unsafe extern "C" fn fake_set_cancel_callback(
433        _handle: sys::mln_resource_request_handle,
434        callback: sys::mln_resource_request_cancel_callback,
435        _user_data: *mut c_void,
436        out_cancelled: *mut bool,
437    ) -> sys::mln_status {
438        SET_CANCEL_COUNT.fetch_add(1, Ordering::SeqCst);
439        if callback.is_none() || out_cancelled.is_null() {
440            return sys::MLN_STATUS_INVALID_ARGUMENT;
441        }
442        // SAFETY: out_cancelled is non-null and points to caller-owned output storage.
443        unsafe { *out_cancelled = CANCELLED_VALUE.load(Ordering::SeqCst) };
444        sys::MLN_STATUS_OK
445    }
446
447    fn fake_fns() -> ResourceRequestHandleFns {
448        // SAFETY: These fake functions implement the same ownership contract as
449        // the native handle functions for tests.
450        unsafe {
451            ResourceRequestHandleFns::new(
452                fake_complete,
453                fake_cancelled,
454                fake_set_cancel_callback,
455                fake_release,
456            )
457        }
458    }
459
460    fn reset_fake_handle_state() {
461        COMPLETE_COUNT.store(0, Ordering::SeqCst);
462        CANCELLED_COUNT.store(0, Ordering::SeqCst);
463        RELEASE_COUNT.store(0, Ordering::SeqCst);
464        SET_CANCEL_COUNT.store(0, Ordering::SeqCst);
465        CANCELLED_VALUE.store(false, Ordering::SeqCst);
466        COMPLETE_STATUS.store(sys::MLN_STATUS_OK, Ordering::SeqCst);
467    }
468
469    fn fake_handle() -> ResourceRequestHandle {
470        reset_fake_handle_state();
471        // A synthetic request handle that reaches only the fake functions above,
472        // never the C API.
473        ResourceRequestHandle::from_raw_with_fns(
474            sys::mln_resource_request_handle(0x0c00_0000_0000_0034),
475            fake_fns(),
476        )
477        .unwrap()
478    }
479
480    fn request() -> sys::mln_resource_request {
481        sys::mln_resource_request {
482            size: std::mem::size_of::<sys::mln_resource_request>() as u32,
483            requested_url: c"maplibre://tiles/2/1/1.pbf".as_ptr(),
484            resolved_url: c"https://example.test/tile.pbf".as_ptr(),
485            kind: sys::MLN_RESOURCE_KIND_TILE,
486            loading_method: sys::MLN_RESOURCE_LOADING_METHOD_NETWORK_ONLY,
487            priority: sys::MLN_RESOURCE_PRIORITY_LOW,
488            usage: sys::MLN_RESOURCE_USAGE_OFFLINE,
489            storage_policy: sys::MLN_RESOURCE_STORAGE_POLICY_VOLATILE,
490            has_range: true,
491            range_start: 7,
492            range_end: 11,
493            has_prior_modified: true,
494            prior_modified_unix_ms: 123,
495            has_prior_expires: true,
496            prior_expires_unix_ms: 456,
497            prior_etag: c"etag".as_ptr(),
498            prior_data: [1u8, 2, 3].as_ptr(),
499            prior_data_size: 3,
500        }
501    }
502
503    fn response() -> sys::mln_resource_transform_response {
504        sys::mln_resource_transform_response {
505            size: std::mem::size_of::<sys::mln_resource_transform_response>() as u32,
506            url: ptr::null(),
507            context: ptr::null_mut(),
508        }
509    }
510
511    #[test]
512    // Rust regression: Rust request handles are transferable for deferred
513    // completion, but shared references are not safe to use concurrently.
514    fn resource_request_handle_is_send_but_not_sync() {
515        assert_impl_all!(ResourceRequestHandle: Send);
516        assert_not_impl_any!(ResourceRequestHandle: Sync);
517    }
518
519    #[test]
520    // Spec coverage: BND-142.
521    fn provider_callback_copies_request_and_pass_through_does_not_release() {
522        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
523        reset_fake_handle_state();
524        let state = ResourceProviderState {
525            callback: Box::new(|request, handle| {
526                assert_eq!(request.requested_url, "maplibre://tiles/2/1/1.pbf");
527                assert_eq!(request.resolved_url, "https://example.test/tile.pbf");
528                assert_eq!(request.kind, ResourceKind::Tile);
529                assert_eq!(request.raw_kind, sys::MLN_RESOURCE_KIND_TILE);
530                assert_eq!(request.loading_method, ResourceLoadingMethod::NetworkOnly);
531                assert_eq!(request.priority, ResourcePriority::Low);
532                assert_eq!(request.usage, ResourceUsage::Offline);
533                assert_eq!(request.storage_policy, ResourceStoragePolicy::Volatile);
534                let range = request.range.unwrap();
535                assert_eq!((range.start, range.end), (7, 11));
536                assert_eq!(request.prior_modified_unix_ms, Some(123));
537                assert_eq!(request.prior_expires_unix_ms, Some(456));
538                assert_eq!(request.prior_etag.as_deref(), Some("etag"));
539                assert_eq!(request.prior_data, vec![1, 2, 3]);
540                drop(handle);
541                ResourceProviderDecision::PassThrough
542            }),
543            handle_fns: fake_fns(),
544        };
545        let raw_request = request();
546
547        let decision = state.invoke(
548            &raw_request,
549            sys::mln_resource_request_handle(0x0c00_0000_0000_0034),
550        );
551
552        assert_eq!(decision, sys::MLN_RESOURCE_PROVIDER_DECISION_PASS_THROUGH);
553        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
554    }
555
556    #[test]
557    // Spec coverage: BND-151.
558    fn retained_pass_through_request_handle_is_stale_after_callback() {
559        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
560        reset_fake_handle_state();
561        let (sender, receiver) = std::sync::mpsc::channel();
562        let state = ResourceProviderState {
563            callback: Box::new(move |_, handle| {
564                sender.send(handle).unwrap();
565                ResourceProviderDecision::PassThrough
566            }),
567            handle_fns: fake_fns(),
568        };
569        let raw_request = request();
570
571        let decision = state.invoke(
572            &raw_request,
573            sys::mln_resource_request_handle(0x0c00_0000_0000_0034),
574        );
575        let handle = receiver.recv().unwrap();
576
577        assert_eq!(decision, sys::MLN_RESOURCE_PROVIDER_DECISION_PASS_THROUGH);
578        let error = handle.is_cancelled().unwrap_err();
579        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
580        let error = handle.complete(ResourceResponse::no_content()).unwrap_err();
581        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
582        handle.close();
583        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 0);
584        assert_eq!(CANCELLED_COUNT.load(Ordering::SeqCst), 0);
585        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
586    }
587
588    #[test]
589    // Spec coverage: BND-143 and BND-150.
590    fn inline_completion_returns_handle_and_releases_once() {
591        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
592        reset_fake_handle_state();
593        let state = ResourceProviderState {
594            callback: Box::new(|_, handle| {
595                handle.complete(ResourceResponse::ok([1, 2, 3])).unwrap();
596                ResourceProviderDecision::PassThrough
597            }),
598            handle_fns: fake_fns(),
599        };
600        let raw_request = request();
601
602        let decision = state.invoke(
603            &raw_request,
604            sys::mln_resource_request_handle(0x0c00_0000_0000_0034),
605        );
606
607        assert_eq!(decision, sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE);
608        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
609        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
610    }
611
612    #[test]
613    // Spec coverage: BND-144 and BND-145.
614    fn deferred_completion_from_another_thread_releases_once() {
615        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
616        let handle = fake_handle();
617        assert_eq!(
618            handle
619                .state
620                .finish_provider_decision(ResourceProviderDecision::Handle),
621            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
622        );
623
624        let thread = std::thread::spawn(move || {
625            handle
626                .complete(ResourceResponse::ok(vec![4, 5, 6]))
627                .unwrap();
628        });
629        thread.join().unwrap();
630
631        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
632        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
633    }
634
635    #[test]
636    // Spec coverage: BND-152.
637    fn failed_completion_is_terminal_after_reaching_c() {
638        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
639        let handle = fake_handle();
640        assert_eq!(
641            handle
642                .state
643                .finish_provider_decision(ResourceProviderDecision::Handle),
644            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
645        );
646        COMPLETE_STATUS.store(sys::MLN_STATUS_INVALID_STATE, Ordering::SeqCst);
647
648        let error = handle
649            .complete(ResourceResponse::ok(Vec::new()))
650            .unwrap_err();
651
652        assert_eq!(error.kind(), ErrorKind::InvalidState);
653        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
654        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
655        let error = handle.complete(ResourceResponse::no_content()).unwrap_err();
656        assert_eq!(error.kind(), ErrorKind::InvalidState);
657        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
658    }
659
660    #[test]
661    // Spec coverage: BND-146.
662    fn validation_failure_before_completion_keeps_handle_retryable() {
663        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
664        let handle = fake_handle();
665        assert_eq!(
666            handle
667                .state
668                .finish_provider_decision(ResourceProviderDecision::Handle),
669            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
670        );
671
672        let error = handle
673            .complete(ResourceResponse::error(
674                crate::ResourceErrorReason::Other,
675                "bad\0message",
676            ))
677            .unwrap_err();
678
679        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
680        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 0);
681        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
682        handle.complete(ResourceResponse::no_content()).unwrap();
683        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
684        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
685    }
686
687    #[test]
688    // Spec coverage: BND-147 and BND-153.
689    fn drop_releases_uncompleted_handled_request_once() {
690        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
691        let handle = fake_handle();
692        assert_eq!(
693            handle
694                .state
695                .finish_provider_decision(ResourceProviderDecision::Handle),
696            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
697        );
698        drop(handle);
699
700        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
701    }
702
703    #[test]
704    // Spec coverage: BND-153.
705    fn close_before_handle_decision_releases_after_decision() {
706        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
707        let handle = fake_handle();
708        let state = Arc::clone(&handle.state);
709        handle.close();
710        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
711        assert_eq!(
712            state.finish_provider_decision(ResourceProviderDecision::Handle),
713            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
714        );
715
716        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
717    }
718
719    #[test]
720    // Spec coverage: BND-147.
721    fn cancelled_queries_use_native_function_until_closed() {
722        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
723        let handle = fake_handle();
724        CANCELLED_VALUE.store(true, Ordering::SeqCst);
725
726        assert!(handle.is_cancelled().unwrap());
727        handle.close();
728    }
729
730    #[test]
731    // Spec coverage: BND-023 and BND-148.
732    fn late_completion_after_observed_cancellation_maps_native_status_and_closes() {
733        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
734        let handle = fake_handle();
735        assert_eq!(
736            handle
737                .state
738                .finish_provider_decision(ResourceProviderDecision::Handle),
739            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
740        );
741        CANCELLED_VALUE.store(true, Ordering::SeqCst);
742
743        assert!(handle.is_cancelled().unwrap());
744        COMPLETE_STATUS.store(sys::MLN_STATUS_INVALID_STATE, Ordering::SeqCst);
745        let error = handle.complete(ResourceResponse::no_content()).unwrap_err();
746
747        assert_eq!(error.kind(), ErrorKind::InvalidState);
748        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
749        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
750        let error = handle.complete(ResourceResponse::no_content()).unwrap_err();
751        assert_eq!(error.kind(), ErrorKind::InvalidState);
752        assert_eq!(COMPLETE_COUNT.load(Ordering::SeqCst), 1);
753    }
754
755    #[test]
756    // Spec coverage: BND-121.
757    fn provider_panics_produce_unknown_decision_without_unwinding() {
758        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
759        reset_fake_handle_state();
760        let state = ResourceProviderState {
761            callback: Box::new(|_, _| panic!("boom")),
762            handle_fns: fake_fns(),
763        };
764        let raw_request = request();
765
766        let decision = state.invoke(
767            &raw_request,
768            sys::mln_resource_request_handle(0x0c00_0000_0000_0034),
769        );
770
771        assert_eq!(decision, UNKNOWN_PROVIDER_DECISION);
772        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 0);
773    }
774
775    #[test]
776    // Spec coverage: BND-198.
777    fn stale_request_rejects_cancel_registration_as_closed() {
778        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
779        reset_fake_handle_state();
780        let (sender, receiver) = std::sync::mpsc::channel();
781        let state = ResourceProviderState {
782            callback: Box::new(move |_, handle| {
783                sender.send(handle).unwrap();
784                ResourceProviderDecision::PassThrough
785            }),
786            handle_fns: fake_fns(),
787        };
788        let raw_request = request();
789        let decision = state.invoke(
790            &raw_request,
791            sys::mln_resource_request_handle(0x0c00_0000_0000_0034),
792        );
793        let handle = receiver.recv().unwrap();
794        assert_eq!(decision, sys::MLN_RESOURCE_PROVIDER_DECISION_PASS_THROUGH);
795
796        let error = handle.set_cancel_callback(|| {}).unwrap_err();
797
798        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
799        assert_eq!(SET_CANCEL_COUNT.load(Ordering::SeqCst), 0);
800    }
801
802    #[test]
803    // Spec coverage: BND-198 and BND-121. Cancellation reports no status, so a
804    // panic inside the callback is contained and discarded.
805    fn cancel_callback_panic_does_not_unwind_into_c() {
806        let _guard = FAKE_HANDLE_TEST_LOCK.lock().unwrap();
807        let handle = fake_handle();
808        assert_eq!(
809            handle
810                .state
811                .finish_provider_decision(ResourceProviderDecision::Handle),
812            sys::MLN_RESOURCE_PROVIDER_DECISION_HANDLE
813        );
814        // The fake registration reports the request as already cancelled, so
815        // the binding runs the callback before registration returns.
816        CANCELLED_VALUE.store(true, Ordering::SeqCst);
817        let calls = Arc::new(AtomicUsize::new(0));
818        let callback_calls = Arc::clone(&calls);
819
820        handle
821            .set_cancel_callback(move || {
822                callback_calls.fetch_add(1, Ordering::SeqCst);
823                panic!("boom");
824            })
825            .unwrap();
826
827        assert_eq!(calls.load(Ordering::SeqCst), 1);
828        handle.close();
829        assert_eq!(RELEASE_COUNT.load(Ordering::SeqCst), 1);
830    }
831
832    #[test]
833    // Spec coverage: BND-141.
834    fn transform_callback_copies_request_when_keeping_original_url() {
835        let state = ResourceTransformState::new(|request| {
836            assert_eq!(request.kind, ResourceKind::Style);
837            assert_eq!(request.raw_kind, sys::MLN_RESOURCE_KIND_STYLE);
838            assert_eq!(request.url, "https://example.test/style.json");
839            None
840        });
841        let descriptor = state.descriptor();
842        let callback = descriptor.callback.unwrap();
843        let url = CString::new("https://example.test/style.json").unwrap();
844        let mut response = response();
845
846        let status = unsafe {
847            callback(
848                descriptor.user_data,
849                sys::MLN_RESOURCE_KIND_STYLE,
850                url.as_ptr(),
851                &mut response,
852            )
853        };
854
855        assert_eq!(status, sys::MLN_STATUS_OK);
856        assert!(response.url.is_null());
857    }
858
859    #[test]
860    // Spec coverage: BND-140.
861    fn transform_callback_clears_stale_response_when_keeping_original_url() {
862        let state = ResourceTransformState::new(|_| None);
863        let descriptor = state.descriptor();
864        let callback = descriptor.callback.unwrap();
865        let url = CString::new("https://example.test/style.json").unwrap();
866        let stale = CString::new("https://stale.test/style.json").unwrap();
867        let mut response = response();
868        response.url = stale.as_ptr();
869
870        let status = unsafe {
871            callback(
872                descriptor.user_data,
873                sys::MLN_RESOURCE_KIND_STYLE,
874                url.as_ptr(),
875                &mut response,
876            )
877        };
878
879        assert_eq!(status, sys::MLN_STATUS_OK);
880        assert!(response.url.is_null());
881    }
882
883    #[test]
884    // Spec coverage: BND-121.
885    fn transform_callback_contains_panics() {
886        let state = ResourceTransformState::new(|_| panic!("boom"));
887        let descriptor = state.descriptor();
888        let callback = descriptor.callback.unwrap();
889        let url = CString::new("https://example.test/style.json").unwrap();
890        let mut response = response();
891
892        let status = unsafe {
893            callback(
894                descriptor.user_data,
895                sys::MLN_RESOURCE_KIND_STYLE,
896                url.as_ptr(),
897                &mut response,
898            )
899        };
900
901        assert_eq!(status, sys::MLN_STATUS_NATIVE_ERROR);
902        assert!(response.url.is_null());
903    }
904
905    #[test]
906    // Spec coverage: BND-024.
907    fn transform_callback_rejects_embedded_nul_replacements() {
908        let state = ResourceTransformState::new(|_| Some("https://example.test/\0bad".to_owned()));
909        let descriptor = state.descriptor();
910        let callback = descriptor.callback.unwrap();
911        let url = CString::new("https://example.test/style.json").unwrap();
912        let mut response = response();
913
914        let status = unsafe {
915            callback(
916                descriptor.user_data,
917                sys::MLN_RESOURCE_KIND_STYLE,
918                url.as_ptr(),
919                &mut response,
920            )
921        };
922
923        assert_eq!(status, sys::MLN_STATUS_INVALID_ARGUMENT);
924        assert!(response.url.is_null());
925    }
926
927    #[test]
928    // Rust regression: proves callback captures are released by the Rust
929    // transform-state implementation after native unregistration.
930    fn transform_state_drops_callback_capture() {
931        let token = Arc::new(());
932        let callback_token = Arc::clone(&token);
933        let state = ResourceTransformState::new(move |_| {
934            let _ = &callback_token;
935            None
936        });
937        assert_eq!(Arc::strong_count(&token), 2);
938        drop(state);
939        assert_eq!(Arc::strong_count(&token), 1);
940    }
941
942    #[test]
943    // Spec coverage: BND-123.
944    fn callback_can_run_from_multiple_threads() {
945        let calls = Arc::new(AtomicUsize::new(0));
946        let callback_calls = Arc::clone(&calls);
947        let state = Arc::new(ResourceTransformState {
948            callback: Box::new(move |request| {
949                callback_calls.fetch_add(1, Ordering::SeqCst);
950                let _ = request;
951                None
952            }),
953        });
954
955        let handles = (0..2)
956            .map(|_| {
957                let state = Arc::clone(&state);
958                std::thread::spawn(move || {
959                    let descriptor = state.descriptor();
960                    let callback = descriptor.callback.unwrap();
961                    let url = CString::new("https://example.test/tile").unwrap();
962                    let mut response = response();
963                    let status = unsafe {
964                        callback(
965                            descriptor.user_data,
966                            sys::MLN_RESOURCE_KIND_TILE,
967                            url.as_ptr(),
968                            &mut response,
969                        )
970                    };
971                    assert_eq!(status, sys::MLN_STATUS_OK);
972                    assert!(response.url.is_null());
973                })
974            })
975            .collect::<Vec<_>>();
976
977        for handle in handles {
978            handle.join().unwrap();
979        }
980        assert_eq!(calls.load(Ordering::SeqCst), 2);
981    }
982}