Skip to main content

whiteout/
casc_ext.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (c) 2026 Fernando Sahmkow
3
4//! CASC entry points that need hand-written marshalling.
5//!
6//! `Storage::openOnline` takes an options struct mixing an interface
7//! pointer with a `std::function`, and `Storage::readBatch` passes value
8//! objects in both directions — neither shape is something the codegen can
9//! express, so both cross through the shims in
10//! `bindings/c/whiteout_casc_shims.cpp`.
11//!
12//! The methods are inherent `impl`s on [`crate::casc::Storage`], so they are
13//! callable without importing anything from here. Only the request/result
14//! types below need a `use`.
15
16use core::ffi::{c_char, c_void};
17use std::ffi::CString;
18
19use crate::casc::{FileIdHint, Storage};
20use crate::support::RawCString;
21
22extern "C" {
23    fn whiteout_casc_shim_openOnline(
24        product: *const c_char,
25        region: *const c_char,
26        build_key: *const c_char,
27        http: *mut c_void,
28        cache_dir: *const c_char,
29        locale_mask: u32,
30        pool: *mut c_void,
31    ) -> *mut c_void;
32
33    fn whiteout_casc_shim_readBatch(
34        self_: *const c_void,
35        paths: *const *const c_char,
36        file_data_ids: *const i32,
37        hints: *const i32,
38        count: usize,
39    ) -> *mut c_void;
40
41    fn whiteout_casc_shim_readBatch_count(snapshot: *mut c_void) -> usize;
42    fn whiteout_casc_shim_readBatch_data_at(
43        snapshot: *mut c_void,
44        index: usize,
45    ) -> crate::support::RawBytes;
46    fn whiteout_casc_shim_readBatch_success_at(snapshot: *mut c_void, index: usize) -> i32;
47    fn whiteout_casc_shim_readBatch_error_at(snapshot: *mut c_void, index: usize) -> RawCString;
48    fn whiteout_casc_shim_readBatch_free(snapshot: *mut c_void);
49}
50
51/// Anything that can be handed to C++ as an `interfaces::HttpHandler*`.
52///
53/// Implemented for both the trampoline wrapper (your own Rust
54/// implementation) and the library's built-in client, so either can drive
55/// [`Storage::open_online`].
56pub trait AsHttpHandler {
57    /// The raw `interfaces::HttpHandler*` this value stands for.
58    fn as_http_ptr(&self) -> *mut c_void;
59}
60
61impl AsHttpHandler for crate::interfaces::HostHttpHandler {
62    fn as_http_ptr(&self) -> *mut c_void {
63        self.as_ptr()
64    }
65}
66
67impl AsHttpHandler for crate::host::SimpleHttpHandler {
68    fn as_http_ptr(&self) -> *mut c_void {
69        self.raw.as_ptr() as *mut c_void
70    }
71}
72
73/// One file to read in a batch: either by CASC path, or by WoW-style
74/// FileDataId.
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub enum BatchReadRequest {
77    /// Read by CASC path, e.g. `"Base\\creatures\\beast\\beast.m2"`.
78    Path(String),
79    /// Read by FileDataId, with a sub-type hint for the lookup.
80    FileId {
81        /// The FileDataId to resolve.
82        id: i32,
83        /// Which entry to pick when the id maps to several.
84        hint: FileIdHint,
85    },
86}
87
88impl BatchReadRequest {
89    /// Read by CASC path.
90    pub fn path(path: impl Into<String>) -> Self {
91        BatchReadRequest::Path(path.into())
92    }
93
94    /// Read by FileDataId, taking the primary entry.
95    pub fn file_id(id: i32) -> Self {
96        BatchReadRequest::FileId {
97            id,
98            hint: FileIdHint::None,
99        }
100    }
101}
102
103/// Result of a single file in a batch read, in request order.
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct BatchReadResult {
106    /// File contents, or `None` when the read failed.
107    pub data: Option<Vec<u8>>,
108    /// Diagnostic message; empty on success.
109    pub error: String,
110}
111
112impl BatchReadResult {
113    /// Whether this file was read successfully.
114    pub fn is_ok(&self) -> bool {
115        self.data.is_some()
116    }
117}
118
119impl Storage {
120    /// Open a CDN-backed (online) storage.
121    ///
122    /// The returned [`Storage`] exposes the same read API as a local one.
123    ///
124    /// - `product` — product code, e.g. `"wow"`, `"w3"`, `"d3"`, `"fenris"`.
125    /// - `region` — region for version lookup; empty defaults to `"us"`.
126    /// - `http` — HTTP transport; required.
127    /// - `build_key` — optional hex build-config key; `None` takes the
128    ///   latest active build.
129    /// - `cache_dir` — optional on-disk cache; `None` keeps everything in
130    ///   memory.
131    /// - `locale_mask` — locale filter, 0 accepts all.
132    pub fn open_online(
133        product: &str,
134        region: &str,
135        http: &dyn AsHttpHandler,
136        build_key: Option<&str>,
137        cache_dir: Option<&str>,
138        locale_mask: u32,
139        pool: Option<&crate::interfaces::HostWorkerPool>,
140    ) -> Option<Storage> {
141        let product_cstr = CString::new(product).unwrap_or_default();
142        let region_cstr = CString::new(if region.is_empty() { "us" } else { region })
143            .unwrap_or_default();
144        let build_key_cstr = CString::new(build_key.unwrap_or("")).unwrap_or_default();
145        let cache_dir_cstr = CString::new(cache_dir.unwrap_or("")).unwrap_or_default();
146
147        // SAFETY: every pointer is live for the duration of the call, and
148        // the shim either returns a fresh Storage* or null.
149        unsafe {
150            Storage::from_raw(whiteout_casc_shim_openOnline(
151                product_cstr.as_ptr(),
152                region_cstr.as_ptr(),
153                build_key_cstr.as_ptr(),
154                http.as_http_ptr(),
155                cache_dir_cstr.as_ptr(),
156                locale_mask,
157                pool.map_or(core::ptr::null_mut(), |p| p.as_ptr()),
158            ) as *mut _)
159        }
160    }
161
162    /// Read every requested file in one native call.
163    ///
164    /// Results come back in request order; an individual failure yields a
165    /// result with `data == None` and does not affect the others. When the
166    /// storage was opened with a worker pool, resolution / raw read / BLTE
167    /// decode overlap across files — considerably faster than reading one
168    /// file at a time.
169    pub fn read_batch(&self, requests: &[BatchReadRequest]) -> Vec<BatchReadResult> {
170        if requests.is_empty() {
171            return Vec::new();
172        }
173
174        // The CStrings must outlive the call, so they are kept alongside
175        // the pointer array rather than being built inline.
176        let mut owned: Vec<Option<CString>> = Vec::with_capacity(requests.len());
177        let mut ids: Vec<i32> = Vec::with_capacity(requests.len());
178        let mut hints: Vec<i32> = Vec::with_capacity(requests.len());
179        for r in requests {
180            match r {
181                BatchReadRequest::Path(p) => {
182                    owned.push(Some(CString::new(p.as_str()).unwrap_or_default()));
183                    ids.push(-1);
184                    hints.push(0);
185                }
186                BatchReadRequest::FileId { id, hint } => {
187                    owned.push(None);
188                    ids.push(*id);
189                    hints.push(*hint as i32);
190                }
191            }
192        }
193        let ptrs: Vec<*const c_char> = owned
194            .iter()
195            .map(|o| o.as_ref().map_or(core::ptr::null(), |c| c.as_ptr()))
196            .collect();
197
198        // SAFETY: the parallel arrays are all `requests.len()` long and stay
199        // alive across the call; the snapshot is freed before returning.
200        unsafe {
201            let snap = whiteout_casc_shim_readBatch(
202                self.raw.as_ptr() as *const c_void,
203                ptrs.as_ptr(),
204                ids.as_ptr(),
205                hints.as_ptr(),
206                requests.len(),
207            );
208            if snap.is_null() {
209                return Vec::new();
210            }
211            let n = whiteout_casc_shim_readBatch_count(snap);
212            let mut out = Vec::with_capacity(n);
213            for i in 0..n {
214                let ok = whiteout_casc_shim_readBatch_success_at(snap, i) != 0;
215                // Borrowed views into the snapshot (`owner` is null), so
216                // copy them out rather than taking ownership.
217                let data = if ok {
218                    let raw = whiteout_casc_shim_readBatch_data_at(snap, i);
219                    if raw.data.is_null() {
220                        Some(Vec::new())
221                    } else {
222                        Some(core::slice::from_raw_parts(raw.data, raw.size).to_vec())
223                    }
224                } else {
225                    None
226                };
227                let raw_err = whiteout_casc_shim_readBatch_error_at(snap, i);
228                let error = if raw_err.chars.is_null() {
229                    String::new()
230                } else {
231                    String::from_utf8_lossy(core::slice::from_raw_parts(
232                        raw_err.chars as *const u8,
233                        raw_err.length,
234                    ))
235                    .into_owned()
236                };
237                out.push(BatchReadResult { data, error });
238            }
239            whiteout_casc_shim_readBatch_free(snap);
240            out
241        }
242    }
243}