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