Skip to main content

whiteout/
casc.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (c) 2026 Fernando Sahmkow
3// AUTOGENERATED by tools/codegen/emit_rust.py — do not edit.
4// Regenerate via:  python -m tools.codegen.codegen casc --backend rust
5
6#![allow(clippy::too_many_arguments)]
7
8// Which of these a module needs depends on its shapes; the modules that
9// have no span accessors would otherwise trip the unused-import lint.
10#[allow(unused_imports)]
11use crate::support::{BorrowedSlice, Bytes};
12
13/// Root manifest format.
14#[repr(i32)]
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub enum RootFormat {
17    /// Could not determine format.
18    Unknown = 0,
19    /// World of Warcraft root (FileDataId-based, legacy MFST).
20    Wow = 1,
21    /// World of Warcraft root (FileDataId-based, TVFS-backed, 11.x+).
22    WowTvfs = 2,
23    /// Diablo III root (hierarchical directory).
24    Diablo3 = 3,
25    /// Diablo IV root (TVFS enriched with CoreTOC paths).
26    Diablo4 = 4,
27    /// TVFS prefix-tree root (WC3 Reforged and general purpose).
28    Tvfs = 5,
29    /// MNDX trie-based root (StarCraft II, Heroes of the Storm).
30    Mndx = 6,
31    /// Overwatch root (text manifest + CMF content manifests).
32    Overwatch = 7,
33    /// Agent/S1 text root (SC:R, Hearthstone, etc.).
34    Agent = 8,
35}
36
37impl TryFrom<i32> for RootFormat {
38    type Error = crate::Error;
39    fn try_from(v: i32) -> Result<Self, crate::Error> {
40        match v {
41            0 => Ok(RootFormat::Unknown),
42            1 => Ok(RootFormat::Wow),
43            2 => Ok(RootFormat::WowTvfs),
44            3 => Ok(RootFormat::Diablo3),
45            4 => Ok(RootFormat::Diablo4),
46            5 => Ok(RootFormat::Tvfs),
47            6 => Ok(RootFormat::Mndx),
48            7 => Ok(RootFormat::Overwatch),
49            8 => Ok(RootFormat::Agent),
50            other => Err(crate::Error::UnknownEnum {
51                name: "RootFormat",
52                value: other,
53            }),
54        }
55    }
56}
57
58/// Hint for disambiguating FileDataId-based lookups. In Diablo IV, a single SNO ID can map to multiple entries (child, meta, payload, etc.).  The hint tells the root which variant to return. Roots that don't use sub-types (e.g. WoW) ignore the hint.
59#[repr(i32)]
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
61pub enum FileIdHint {
62    /// Default — return the primary entry (child/main content).
63    None = 0,
64    /// Metadata entry.
65    Meta = 1,
66    /// Full-resolution payload.
67    Payload = 2,
68    /// Low-resolution payload.
69    Paylow = 3,
70    /// Medium-resolution payload.
71    Paymed = 4,
72}
73
74impl TryFrom<i32> for FileIdHint {
75    type Error = crate::Error;
76    fn try_from(v: i32) -> Result<Self, crate::Error> {
77        match v {
78            0 => Ok(FileIdHint::None),
79            1 => Ok(FileIdHint::Meta),
80            2 => Ok(FileIdHint::Payload),
81            3 => Ok(FileIdHint::Paylow),
82            4 => Ok(FileIdHint::Paymed),
83            other => Err(crate::Error::UnknownEnum {
84                name: "FileIdHint",
85                value: other,
86            }),
87        }
88    }
89}
90
91/// Entry returned by enumerate/list operations.
92#[derive(Clone, Debug, PartialEq)]
93pub struct FindEntry {
94    pub c_key: Vec<u8>,
95    pub file_size: u64,
96    pub locale_flags: u32,
97    pub content_flags: u32,
98    pub file_data_id: i32,
99    pub path: String,
100}
101
102/// Options for creating a new empty CASC storage.
103pub struct CreateOptions {
104    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_CascCreateOptions>,
105}
106
107impl Drop for CreateOptions {
108    fn drop(&mut self) {
109        // SAFETY: `raw` came from a native constructor and Drop runs once.
110        unsafe { ffi::whiteout_casc_CascCreateOptions_delete(self.raw.as_ptr()) }
111    }
112}
113
114impl CreateOptions {
115    /// # Safety
116    /// `raw` must be a live handle this value takes ownership of.
117    #[allow(dead_code)] // used by whichever methods return this type
118    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_CascCreateOptions) -> Option<Self> {
119        core::ptr::NonNull::new(raw).map(|raw| CreateOptions { raw })
120    }
121}
122
123// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
124// is deliberately NOT implemented — the C++ types make no documented
125// guarantee about concurrent use, and claiming one we haven't verified
126// would be unsound. See `@bind thread_safe` in the plan.
127unsafe impl Send for CreateOptions {}
128
129impl core::fmt::Debug for CreateOptions {
130    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
131        f.debug_struct("CreateOptions").finish_non_exhaustive()
132    }
133}
134
135impl CreateOptions {
136    /// # Panics
137    /// Panics if the native allocation fails.
138    pub fn new() -> Self {
139        // SAFETY: the native constructor returns a live handle; a null here
140        // means the library is unusable.
141        unsafe {
142            let raw = ffi::whiteout_casc_CascCreateOptions_new();
143            Self::from_raw(raw).expect("native CreateOptions allocation failed")
144        }
145    }
146
147    pub fn product(&self) -> String {
148        // SAFETY: the native side hands over an owned CString.
149        unsafe {
150            crate::support::take_string(ffi::whiteout_casc_CascCreateOptions_get_product(
151                self.raw.as_ptr(),
152            ))
153        }
154    }
155
156    pub fn set_product(&mut self, value: &str) {
157        let value = std::ffi::CString::new(value).unwrap_or_default();
158        // SAFETY: the pointer outlives the call.
159        unsafe {
160            ffi::whiteout_casc_CascCreateOptions_set_product(self.raw.as_ptr(), value.as_ptr())
161        }
162    }
163
164    pub fn version(&self) -> String {
165        // SAFETY: the native side hands over an owned CString.
166        unsafe {
167            crate::support::take_string(ffi::whiteout_casc_CascCreateOptions_get_version(
168                self.raw.as_ptr(),
169            ))
170        }
171    }
172
173    pub fn set_version(&mut self, value: &str) {
174        let value = std::ffi::CString::new(value).unwrap_or_default();
175        // SAFETY: the pointer outlives the call.
176        unsafe {
177            ffi::whiteout_casc_CascCreateOptions_set_version(self.raw.as_ptr(), value.as_ptr())
178        }
179    }
180
181    /// 1 GB.
182    pub fn archive_max_size(&self) -> u32 {
183        // SAFETY: plain scalar read through a live handle.
184        unsafe { ffi::whiteout_casc_CascCreateOptions_get_archiveMaxSize(self.raw.as_ptr()) }
185    }
186
187    pub fn set_archive_max_size(&mut self, value: u32) {
188        // SAFETY: plain scalar write through a live handle.
189        unsafe { ffi::whiteout_casc_CascCreateOptions_set_archiveMaxSize(self.raw.as_ptr(), value) }
190    }
191
192    /// 64 KB.
193    pub fn blte_frame_size(&self) -> u32 {
194        // SAFETY: plain scalar read through a live handle.
195        unsafe { ffi::whiteout_casc_CascCreateOptions_get_blteFrameSize(self.raw.as_ptr()) }
196    }
197
198    pub fn set_blte_frame_size(&mut self, value: u32) {
199        // SAFETY: plain scalar write through a live handle.
200        unsafe { ffi::whiteout_casc_CascCreateOptions_set_blteFrameSize(self.raw.as_ptr(), value) }
201    }
202
203    pub fn root_format(&self) -> RootFormat {
204        // SAFETY: scalar read; the discriminant is validated below.
205        unsafe { ffi::whiteout_casc_CascCreateOptions_get_rootFormat(self.raw.as_ptr()) }
206            .try_into()
207            .expect("unknown enum discriminant from the native library")
208    }
209
210    pub fn set_root_format(&mut self, value: RootFormat) {
211        // SAFETY: scalar write through a live handle.
212        unsafe {
213            ffi::whiteout_casc_CascCreateOptions_set_rootFormat(self.raw.as_ptr(), value as i32)
214        }
215    }
216}
217
218impl Default for CreateOptions {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224/// Options for writing a file into a CASC storage.
225#[derive(Clone, Debug, PartialEq)]
226pub struct WriteOptions {
227    pub locale_flags: u32,
228    pub content_flags: u32,
229    pub compress: bool,
230}
231
232impl Default for WriteOptions {
233    fn default() -> Self {
234        // SAFETY: `_new` always returns a live handle; freed before return.
235        unsafe {
236            let h = ffi::whiteout_casc_CascWriteOptions_new();
237            let out = WriteOptions {
238                locale_flags: ffi::whiteout_casc_CascWriteOptions_get_localeFlags(h),
239                content_flags: ffi::whiteout_casc_CascWriteOptions_get_contentFlags(h),
240                compress: ffi::whiteout_casc_CascWriteOptions_get_compress(h) != 0,
241            };
242            ffi::whiteout_casc_CascWriteOptions_delete(h);
243            out
244        }
245    }
246}
247
248impl WriteOptions {
249    /// Build a native handle carrying these values. Caller frees it.
250    #[allow(dead_code)] // consumed once the methods taking these options bind
251    pub(crate) unsafe fn to_native(&self) -> *mut ffi::whiteout_CascWriteOptions {
252        unsafe {
253            let h = ffi::whiteout_casc_CascWriteOptions_new();
254            ffi::whiteout_casc_CascWriteOptions_set_localeFlags(h, self.locale_flags);
255            ffi::whiteout_casc_CascWriteOptions_set_contentFlags(h, self.content_flags);
256            ffi::whiteout_casc_CascWriteOptions_set_compress(h, if self.compress { 1 } else { 0 });
257            h
258        }
259    }
260
261    /// Free a handle produced by [`Self::to_native`].
262    ///
263    /// # Safety
264    /// `h` must have come from `to_native` and not been freed already.
265    #[allow(dead_code)]
266    pub(crate) unsafe fn free_native(h: *mut ffi::whiteout_CascWriteOptions) {
267        unsafe { ffi::whiteout_casc_CascWriteOptions_delete(h) }
268    }
269}
270
271/// Unified read-only CASC storage (local disk or CDN)
272///
273/// Storage is the primary entry point for reading CASC archives. Use `open()` for local disk, `openOnline()` for CDN-backed access. The same public read API works identically regardless of backing store.
274///
275/// All public methods are thread-safe: read operations acquire a shared lock.
276///
277/// Uses the PImpl (Pointer to Implementation) idiom to hide internals.
278///
279/// @see StorageWritable for write + persist operations.
280pub struct Storage {
281    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_CascStorage>,
282}
283
284impl Drop for Storage {
285    fn drop(&mut self) {
286        // SAFETY: `raw` came from a native constructor and Drop runs once.
287        unsafe { ffi::whiteout_casc_CascStorage_delete(self.raw.as_ptr()) }
288    }
289}
290
291impl Storage {
292    /// # Safety
293    /// `raw` must be a live handle this value takes ownership of.
294    #[allow(dead_code)] // used by whichever methods return this type
295    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_CascStorage) -> Option<Self> {
296        core::ptr::NonNull::new(raw).map(|raw| Storage { raw })
297    }
298}
299
300// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
301// is deliberately NOT implemented — the C++ types make no documented
302// guarantee about concurrent use, and claiming one we haven't verified
303// would be unsound. See `@bind thread_safe` in the plan.
304unsafe impl Send for Storage {}
305
306impl core::fmt::Debug for Storage {
307    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
308        f.debug_struct("Storage").finish_non_exhaustive()
309    }
310}
311
312impl Storage {
313    /// Open an existing local CASC storage. @param path Path to the game's top-level directory (containing .build.info) or its Data subdirectory. @param pool Optional WorkerPool for parallel I/O (non-owning). @return A valid Storage, or std::nullopt on failure.
314    pub fn open(path: &str, pool: Option<&crate::interfaces::HostWorkerPool>) -> Option<Storage> {
315        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
316        // SAFETY: handle is live for the duration of the call.
317        unsafe {
318            Storage::from_raw(ffi::whiteout_casc_CascStorage_open(
319                path_cstr.as_ptr(),
320                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
321            ))
322        }
323    }
324
325    /// @overload Open with locale mask.
326    pub fn open_path_locale_mask_pool(
327        path: &str,
328        locale_mask: u32,
329        pool: Option<&crate::interfaces::HostWorkerPool>,
330    ) -> Option<Storage> {
331        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
332        // SAFETY: handle is live for the duration of the call.
333        unsafe {
334            Storage::from_raw(ffi::whiteout_casc_CascStorage_open_path_localeMask_pool(
335                path_cstr.as_ptr(),
336                locale_mask,
337                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
338            ))
339        }
340    }
341
342    /// @overload Open a specific product from a multi-product `.build.info`. @param product Product code selecting the build, e.g. "w3" (Warcraft III retail) vs "w3t" (its PTR). Matched case-insensitively against the active builds; empty selects the first active build. See OpenOptions::product. Open fails if the product has no active build.
343    pub fn open_path_product_pool(
344        path: &str,
345        product: &str,
346        pool: Option<&crate::interfaces::HostWorkerPool>,
347    ) -> Option<Storage> {
348        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
349        let product_cstr = std::ffi::CString::new(product).unwrap_or_default();
350        // SAFETY: handle is live for the duration of the call.
351        unsafe {
352            Storage::from_raw(ffi::whiteout_casc_CascStorage_open_path_product_pool(
353                path_cstr.as_ptr(),
354                product_cstr.as_ptr(),
355                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
356            ))
357        }
358    }
359
360    /// Release all resources and invalidate the storage.
361    pub fn close(&mut self) {
362        // SAFETY: handle is live for the duration of the call.
363        unsafe {
364            ffi::whiteout_casc_CascStorage_close(self.raw.as_ptr());
365        }
366    }
367
368    /// @return True if this storage reads from local disk.
369    pub fn is_local(&self) -> bool {
370        // SAFETY: handle is live for the duration of the call.
371        unsafe { ffi::whiteout_casc_CascStorage_isLocal(self.raw.as_ptr()) != 0 }
372    }
373
374    /// @return True if this storage reads from CDN.
375    pub fn is_online(&self) -> bool {
376        // SAFETY: handle is live for the duration of the call.
377        unsafe { ffi::whiteout_casc_CascStorage_isOnline(self.raw.as_ptr()) != 0 }
378    }
379
380    /// @return True if this storage has a write overlay (StorageWritable).
381    pub fn is_writable(&self) -> bool {
382        // SAFETY: handle is live for the duration of the call.
383        unsafe { ffi::whiteout_casc_CascStorage_isWritable(self.raw.as_ptr()) != 0 }
384    }
385
386    /// @return The root manifest format, or RootFormat::Unknown.
387    pub fn root_format(&self) -> RootFormat {
388        // SAFETY: handle is live for the duration of the call.
389        unsafe {
390            RootFormat::try_from(ffi::whiteout_casc_CascStorage_rootFormat(self.raw.as_ptr()))
391                .expect("unknown enum discriminant from the native library (ABI version skew)")
392        }
393    }
394
395    /// @return File contents, or std::nullopt if the path is not found.
396    pub fn read_file(&self, casc_path: &str) -> Option<Bytes> {
397        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
398        // SAFETY: handle is live for the duration of the call.
399        unsafe {
400            Bytes::from_raw(ffi::whiteout_casc_CascStorage_readFile(
401                self.raw.as_ptr(),
402                casc_path_cstr.as_ptr(),
403            ))
404        }
405    }
406
407    /// @overload Read a file by path with locale and open flags.
408    pub fn read_file_casc_path_locale_flags_open_flags(
409        &self,
410        casc_path: &str,
411        locale_flags: u32,
412        open_flags: u32,
413    ) -> Option<Bytes> {
414        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
415        // SAFETY: handle is live for the duration of the call.
416        unsafe {
417            Bytes::from_raw(
418                ffi::whiteout_casc_CascStorage_readFile_cascPath_localeFlags_openFlags(
419                    self.raw.as_ptr(),
420                    casc_path_cstr.as_ptr(),
421                    locale_flags,
422                    open_flags,
423                ),
424            )
425        }
426    }
427
428    /// @overload Read a file by WoW-style FileDataId.
429    pub fn read_file_file_id_hint(&self, file_id: i32, hint: FileIdHint) -> Option<Bytes> {
430        // SAFETY: handle is live for the duration of the call.
431        unsafe {
432            Bytes::from_raw(ffi::whiteout_casc_CascStorage_readFile_fileId_hint(
433                self.raw.as_ptr(),
434                file_id,
435                hint as i32,
436            ))
437        }
438    }
439
440    /// @overload Read a file by FileDataId with locale and open flags.
441    pub fn read_file_file_id_locale_flags_open_flags_hint(
442        &self,
443        file_id: i32,
444        locale_flags: u32,
445        open_flags: u32,
446        hint: FileIdHint,
447    ) -> Option<Bytes> {
448        // SAFETY: handle is live for the duration of the call.
449        unsafe {
450            Bytes::from_raw(
451                ffi::whiteout_casc_CascStorage_readFile_fileId_localeFlags_openFlags_hint(
452                    self.raw.as_ptr(),
453                    file_id,
454                    locale_flags,
455                    open_flags,
456                    hint as i32,
457                ),
458            )
459        }
460    }
461
462    /// @return True if the path resolves to a known file.
463    pub fn file_exists(&self, casc_path: &str) -> bool {
464        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
465        // SAFETY: handle is live for the duration of the call.
466        unsafe {
467            ffi::whiteout_casc_CascStorage_fileExists(self.raw.as_ptr(), casc_path_cstr.as_ptr())
468                != 0
469        }
470    }
471
472    /// @overload Check existence by FileDataId.
473    pub fn file_exists_file_id_hint(&self, file_id: i32, hint: FileIdHint) -> bool {
474        // SAFETY: handle is live for the duration of the call.
475        unsafe {
476            ffi::whiteout_casc_CascStorage_fileExists_fileId_hint(
477                self.raw.as_ptr(),
478                file_id,
479                hint as i32,
480            ) != 0
481        }
482    }
483
484    /// @return Uncompressed file size, or std::nullopt if not found.
485    pub fn file_size(&self, casc_path: &str) -> Option<u64> {
486        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
487        let mut __v: u64 = 0;
488        // SAFETY: `__v` is a live local, written by the
489        // native side only when it returns 1.
490        let __has = unsafe {
491            ffi::whiteout_casc_CascStorage_fileSize(
492                self.raw.as_ptr(),
493                casc_path_cstr.as_ptr(),
494                &mut __v,
495            )
496        };
497        (__has != 0).then_some(__v)
498    }
499
500    /// @overload
501    pub fn file_size_file_id_hint(&self, file_id: i32, hint: FileIdHint) -> Option<u64> {
502        let mut __v: u64 = 0;
503        // SAFETY: `__v` is a live local, written by the
504        // native side only when it returns 1.
505        let __has = unsafe {
506            ffi::whiteout_casc_CascStorage_fileSize_fileId_hint(
507                self.raw.as_ptr(),
508                file_id,
509                hint as i32,
510                &mut __v,
511            )
512        };
513        (__has != 0).then_some(__v)
514    }
515
516    /// @return All known file paths.
517    pub fn list_files(&self) -> Vec<String> {
518        // SAFETY: one call materialises the list; the
519        // elements are borrowed out of it and it is freed
520        // before returning. Reading is O(1) per element.
521        unsafe {
522            let list = ffi::whiteout_casc_CascStorage_listFiles(self.raw.as_ptr());
523            if list.is_null() {
524                return Vec::new();
525            }
526            let n = ffi::whiteout_casc_StringList_size(list);
527            let out = (0..n)
528                .map(|i| crate::support::take_string(ffi::whiteout_casc_StringList_at(list, i)))
529                .collect();
530            ffi::whiteout_casc_StringList_delete(list);
531            out
532        }
533    }
534
535    /// @return All entries with metadata.
536    pub fn list_entries(&self) -> Vec<FindEntry> {
537        // SAFETY: one call materialises the snapshot; each
538        // field is read by index and the snapshot is freed
539        // before returning. Reading is O(1) per element.
540        unsafe {
541            let snap = ffi::whiteout_casc_CascStorage_listEntries_snapshot(self.raw.as_ptr());
542            if snap.is_null() {
543                return Vec::new();
544            }
545            let n = ffi::whiteout_casc_CascStorage_listEntries_count(snap);
546            let mut out = Vec::with_capacity(n);
547            for i in 0..n {
548                out.push(FindEntry {
549                    c_key: crate::support::Bytes::from_raw(
550                        ffi::whiteout_casc_CascStorage_listEntries_cKey_at(snap, i),
551                    )
552                    .map(|b| b.to_vec())
553                    .unwrap_or_default(),
554                    file_size: ffi::whiteout_casc_CascStorage_listEntries_fileSize_at(snap, i),
555                    locale_flags: ffi::whiteout_casc_CascStorage_listEntries_localeFlags_at(
556                        snap, i,
557                    ),
558                    content_flags: ffi::whiteout_casc_CascStorage_listEntries_contentFlags_at(
559                        snap, i,
560                    ),
561                    file_data_id: ffi::whiteout_casc_CascStorage_listEntries_fileDataId_at(snap, i),
562                    path: crate::support::take_string(
563                        ffi::whiteout_casc_CascStorage_listEntries_path_at(snap, i),
564                    ),
565                });
566            }
567            ffi::whiteout_casc_CascStorage_listEntries_free(snap);
568            out
569        }
570    }
571
572    /// @return Total number of files in the root manifest.
573    pub fn total_file_count(&self) -> Option<u32> {
574        let mut __v: u32 = 0;
575        // SAFETY: `__v` is a live local, written by the
576        // native side only when it returns 1.
577        let __has =
578            unsafe { ffi::whiteout_casc_CascStorage_totalFileCount(self.raw.as_ptr(), &mut __v) };
579        (__has != 0).then_some(__v)
580    }
581
582    /// Import encryption keys from a formatted string (one per line).
583    pub fn import_keys_from_string(&mut self, key_list: &str) -> bool {
584        let key_list_cstr = std::ffi::CString::new(key_list).unwrap_or_default();
585        // SAFETY: handle is live for the duration of the call.
586        unsafe {
587            ffi::whiteout_casc_CascStorage_importKeysFromString(
588                self.raw.as_ptr(),
589                key_list_cstr.as_ptr(),
590            ) != 0
591        }
592    }
593
594    /// Import encryption keys from a file.
595    pub fn import_keys_from_file(&mut self, key_file_path: &str) -> bool {
596        let key_file_path_cstr = std::ffi::CString::new(key_file_path).unwrap_or_default();
597        // SAFETY: handle is live for the duration of the call.
598        unsafe {
599            ffi::whiteout_casc_CascStorage_importKeysFromFile(
600                self.raw.as_ptr(),
601                key_file_path_cstr.as_ptr(),
602            ) != 0
603        }
604    }
605
606    /// @return The encryption key for @p keyName, or std::nullopt if not found.
607    pub fn find_encryption_key(&self, key_name: u64) -> Option<[u8; 16]> {
608        let mut __v: [u8; 16] = Default::default();
609        // SAFETY: `__v` is a live local of exactly the
610        // length the native side writes.
611        let __has = unsafe {
612            ffi::whiteout_casc_CascStorage_findEncryptionKey(
613                self.raw.as_ptr(),
614                key_name,
615                __v.as_mut_ptr(),
616            )
617        };
618        (__has != 0).then_some(__v)
619    }
620
621    /// Clear the in-memory decoded-data cache (container cache).
622    pub fn flush_cache(&mut self) {
623        // SAFETY: handle is live for the duration of the call.
624        unsafe {
625            ffi::whiteout_casc_CascStorage_flushCache(self.raw.as_ptr());
626        }
627    }
628
629    /// Force every deferred load (encoding, root, VFS, index files, orphan bitvector) to resolve. Idempotent.
630    pub fn prefetch(&mut self) -> bool {
631        // SAFETY: handle is live for the duration of the call.
632        unsafe { ffi::whiteout_casc_CascStorage_prefetch(self.raw.as_ptr()) != 0 }
633    }
634
635    /// @return Last error code (thread-local).
636    pub fn last_error() -> u32 {
637        // SAFETY: handle is live for the duration of the call.
638        unsafe { ffi::whiteout_casc_CascStorage_lastError() }
639    }
640}
641
642/// Writable CASC storage (read + write + save)
643///
644/// Inherits all read operations from Storage. Adds write overlay and persist-to-disk support.
645///
646/// Only local-backed storages can be writable (CDN is read-only).
647///
648/// extends=whiteout::storages::casc::Storage
649pub struct StorageWritable {
650    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_CascStorageWritable>,
651}
652
653impl Drop for StorageWritable {
654    fn drop(&mut self) {
655        // SAFETY: `raw` came from a native constructor and Drop runs once.
656        unsafe { ffi::whiteout_casc_CascStorageWritable_delete(self.raw.as_ptr()) }
657    }
658}
659
660impl StorageWritable {
661    /// # Safety
662    /// `raw` must be a live handle this value takes ownership of.
663    #[allow(dead_code)] // used by whichever methods return this type
664    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_CascStorageWritable) -> Option<Self> {
665        core::ptr::NonNull::new(raw).map(|raw| StorageWritable { raw })
666    }
667}
668
669// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
670// is deliberately NOT implemented — the C++ types make no documented
671// guarantee about concurrent use, and claiming one we haven't verified
672// would be unsound. See `@bind thread_safe` in the plan.
673unsafe impl Send for StorageWritable {}
674
675impl core::fmt::Debug for StorageWritable {
676    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
677        f.debug_struct("StorageWritable").finish_non_exhaustive()
678    }
679}
680
681impl StorageWritable {
682    /// Create a new empty storage in memory.
683    ///
684    /// No file is written to disk until save() is called.
685    ///
686    /// @param opts Creation options (product name, version, root format). @param pool Optional WorkerPool for parallel I/O. @return A valid empty StorageWritable ready for writeFile() calls.
687    pub fn create(
688        opts: &CreateOptions,
689        pool: Option<&crate::interfaces::HostWorkerPool>,
690    ) -> Option<StorageWritable> {
691        // SAFETY: handle is live for the duration of the call.
692        unsafe {
693            StorageWritable::from_raw(ffi::whiteout_casc_CascStorageWritable_create(
694                opts.raw.as_ptr(),
695                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
696            ))
697        }
698    }
699
700    /// Reserve a file-data-ID for a named asset.
701    ///
702    /// Allocates the next available file-data-ID and associates it with @p name.  The interpretation of @p name depends on the root format:
703    ///
704    /// - **WoW / WoWTvfs**: @p name is a full CASC path (e.g. `"Base\\creatures\\beast\\beast.m2"`). - **Diablo 3 / Diablo 4 / TVFS**: @p name is `"asset_name.ext"`, where the extension determines the SNO group.  A CoreTOC entry is created automatically.
705    ///
706    /// Returns @c std::nullopt if the name already exists in the root or in a previous reservation.
707    ///
708    /// @code auto id = storage.reserveFileId("my_beast.app"); if (id) storage.writeFile(*id, data); @endcode
709    pub fn reserve_file_id(&mut self, name: &str) -> Option<u32> {
710        let name_cstr = std::ffi::CString::new(name).unwrap_or_default();
711        let mut __v: u32 = 0;
712        // SAFETY: `__v` is a live local, written by the
713        // native side only when it returns 1.
714        let __has = unsafe {
715            ffi::whiteout_casc_CascStorageWritable_reserveFileId(
716                self.raw.as_ptr(),
717                name_cstr.as_ptr(),
718                &mut __v,
719            )
720        };
721        (__has != 0).then_some(__v)
722    }
723
724    /// Write a file by path.
725    ///
726    /// Data is stored in an in-memory overlay until save() is called.
727    ///
728    /// @param path CASC path for the new or updated file. @param data File contents. @param opts Write options (locale, content flags, compression). @return True on success.
729    pub fn write_file(&mut self, path: &str, data: &[u8], opts: &WriteOptions) -> bool {
730        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
731        let opts_native = unsafe { opts.to_native() };
732        // SAFETY: handle is live for the call; the staged
733        // option handles are freed immediately after.
734        unsafe {
735            let __r = ffi::whiteout_casc_CascStorageWritable_writeFile(
736                self.raw.as_ptr(),
737                path_cstr.as_ptr(),
738                data.as_ptr(),
739                data.len(),
740                opts_native,
741            ) != 0;
742            WriteOptions::free_native(opts_native);
743            __r
744        }
745    }
746
747    /// @overload Write a file by FileDataId.
748    pub fn write_file_file_id_data_opts_hint(
749        &mut self,
750        file_id: i32,
751        data: &[u8],
752        opts: &WriteOptions,
753        hint: FileIdHint,
754    ) -> bool {
755        let opts_native = unsafe { opts.to_native() };
756        // SAFETY: handle is live for the call; the staged
757        // option handles are freed immediately after.
758        unsafe {
759            let __r = ffi::whiteout_casc_CascStorageWritable_writeFile_fileId_data_opts_hint(
760                self.raw.as_ptr(),
761                file_id,
762                data.as_ptr(),
763                data.len(),
764                opts_native,
765                hint as i32,
766            ) != 0;
767            WriteOptions::free_native(opts_native);
768            __r
769        }
770    }
771
772    /// Mark a file for deletion (effective on next save).
773    pub fn delete_file(&mut self, path: &str) -> bool {
774        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
775        // SAFETY: handle is live for the duration of the call.
776        unsafe {
777            ffi::whiteout_casc_CascStorageWritable_deleteFile(self.raw.as_ptr(), path_cstr.as_ptr())
778                != 0
779        }
780    }
781
782    /// @overload
783    pub fn delete_file_file_id_hint(&mut self, file_id: i32, hint: FileIdHint) -> bool {
784        // SAFETY: handle is live for the duration of the call.
785        unsafe {
786            ffi::whiteout_casc_CascStorageWritable_deleteFile_fileId_hint(
787                self.raw.as_ptr(),
788                file_id,
789                hint as i32,
790            ) != 0
791        }
792    }
793
794    /// Persist all pending changes to disk (writes to the original location).
795    pub fn save(&mut self) -> bool {
796        // SAFETY: handle is live for the duration of the call.
797        unsafe { ffi::whiteout_casc_CascStorageWritable_save(self.raw.as_ptr()) != 0 }
798    }
799
800    /// @overload Persist to a specific output path.
801    pub fn save_path(&mut self, path: &str) -> bool {
802        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
803        // SAFETY: handle is live for the duration of the call.
804        unsafe {
805            ffi::whiteout_casc_CascStorageWritable_save_path(self.raw.as_ptr(), path_cstr.as_ptr())
806                != 0
807        }
808    }
809}
810
811// Not yet bound (shape unsupported by the emitter):
812//   - Storage::open_opts (parameter shape)
813
814#[doc(hidden)]
815pub mod ffi {
816    #![allow(missing_debug_implementations)]
817
818    #[allow(unused_imports)]
819    use crate::support::{RawBytes, RawCString};
820
821    #[repr(C)]
822    pub struct whiteout_CascCreateOptions {
823        _private: [u8; 0],
824    }
825    #[repr(C)]
826    pub struct whiteout_CascWriteOptions {
827        _private: [u8; 0],
828    }
829    #[repr(C)]
830    pub struct whiteout_CascStorage {
831        _private: [u8; 0],
832    }
833    #[repr(C)]
834    pub struct whiteout_CascStorageWritable {
835        _private: [u8; 0],
836    }
837    #[repr(C)]
838    pub struct whiteout_StringList {
839        _private: [u8; 0],
840    }
841
842    extern "C" {
843        pub fn whiteout_casc_StringList_size(self_: *mut whiteout_StringList) -> usize;
844        pub fn whiteout_casc_StringList_at(
845            self_: *mut whiteout_StringList,
846            index: usize,
847        ) -> RawCString;
848        pub fn whiteout_casc_StringList_delete(self_: *mut whiteout_StringList);
849        // CreateOptions
850        pub fn whiteout_casc_CascCreateOptions_new() -> *mut whiteout_CascCreateOptions;
851        pub fn whiteout_casc_CascCreateOptions_delete(self_: *mut whiteout_CascCreateOptions);
852        pub fn whiteout_casc_CascCreateOptions_get_product(
853            self_: *mut whiteout_CascCreateOptions,
854        ) -> RawCString;
855        pub fn whiteout_casc_CascCreateOptions_set_product(
856            self_: *mut whiteout_CascCreateOptions,
857            value: *const core::ffi::c_char,
858        );
859        pub fn whiteout_casc_CascCreateOptions_get_version(
860            self_: *mut whiteout_CascCreateOptions,
861        ) -> RawCString;
862        pub fn whiteout_casc_CascCreateOptions_set_version(
863            self_: *mut whiteout_CascCreateOptions,
864            value: *const core::ffi::c_char,
865        );
866        pub fn whiteout_casc_CascCreateOptions_get_archiveMaxSize(
867            self_: *mut whiteout_CascCreateOptions,
868        ) -> u32;
869        pub fn whiteout_casc_CascCreateOptions_set_archiveMaxSize(
870            self_: *mut whiteout_CascCreateOptions,
871            value: u32,
872        );
873        pub fn whiteout_casc_CascCreateOptions_get_blteFrameSize(
874            self_: *mut whiteout_CascCreateOptions,
875        ) -> u32;
876        pub fn whiteout_casc_CascCreateOptions_set_blteFrameSize(
877            self_: *mut whiteout_CascCreateOptions,
878            value: u32,
879        );
880        pub fn whiteout_casc_CascCreateOptions_get_rootFormat(
881            self_: *mut whiteout_CascCreateOptions,
882        ) -> i32;
883        pub fn whiteout_casc_CascCreateOptions_set_rootFormat(
884            self_: *mut whiteout_CascCreateOptions,
885            value: i32,
886        );
887        // WriteOptions
888        pub fn whiteout_casc_CascWriteOptions_new() -> *mut whiteout_CascWriteOptions;
889        pub fn whiteout_casc_CascWriteOptions_delete(self_: *mut whiteout_CascWriteOptions);
890        pub fn whiteout_casc_CascWriteOptions_get_localeFlags(
891            self_: *mut whiteout_CascWriteOptions,
892        ) -> u32;
893        pub fn whiteout_casc_CascWriteOptions_set_localeFlags(
894            self_: *mut whiteout_CascWriteOptions,
895            value: u32,
896        );
897        pub fn whiteout_casc_CascWriteOptions_get_contentFlags(
898            self_: *mut whiteout_CascWriteOptions,
899        ) -> u32;
900        pub fn whiteout_casc_CascWriteOptions_set_contentFlags(
901            self_: *mut whiteout_CascWriteOptions,
902            value: u32,
903        );
904        pub fn whiteout_casc_CascWriteOptions_get_compress(
905            self_: *mut whiteout_CascWriteOptions,
906        ) -> i32;
907        pub fn whiteout_casc_CascWriteOptions_set_compress(
908            self_: *mut whiteout_CascWriteOptions,
909            value: i32,
910        );
911        // Storage
912        pub fn whiteout_casc_CascStorage_delete(self_: *mut whiteout_CascStorage);
913        pub fn whiteout_casc_CascStorage_open(
914            path: *const core::ffi::c_char,
915            pool: *mut core::ffi::c_void,
916        ) -> *mut whiteout_CascStorage;
917        pub fn whiteout_casc_CascStorage_open_path_localeMask_pool(
918            path: *const core::ffi::c_char,
919            locale_mask: u32,
920            pool: *mut core::ffi::c_void,
921        ) -> *mut whiteout_CascStorage;
922        pub fn whiteout_casc_CascStorage_open_path_product_pool(
923            path: *const core::ffi::c_char,
924            product: *const core::ffi::c_char,
925            pool: *mut core::ffi::c_void,
926        ) -> *mut whiteout_CascStorage;
927        pub fn whiteout_casc_CascStorage_close(self_: *mut whiteout_CascStorage);
928        pub fn whiteout_casc_CascStorage_isLocal(self_: *mut whiteout_CascStorage) -> i32;
929        pub fn whiteout_casc_CascStorage_isOnline(self_: *mut whiteout_CascStorage) -> i32;
930        pub fn whiteout_casc_CascStorage_isWritable(self_: *mut whiteout_CascStorage) -> i32;
931        pub fn whiteout_casc_CascStorage_rootFormat(self_: *mut whiteout_CascStorage) -> i32;
932        pub fn whiteout_casc_CascStorage_readFile(
933            self_: *mut whiteout_CascStorage,
934            casc_path: *const core::ffi::c_char,
935        ) -> RawBytes;
936        pub fn whiteout_casc_CascStorage_readFile_cascPath_localeFlags_openFlags(
937            self_: *mut whiteout_CascStorage,
938            casc_path: *const core::ffi::c_char,
939            locale_flags: u32,
940            open_flags: u32,
941        ) -> RawBytes;
942        pub fn whiteout_casc_CascStorage_readFile_fileId_hint(
943            self_: *mut whiteout_CascStorage,
944            file_id: i32,
945            hint: i32,
946        ) -> RawBytes;
947        pub fn whiteout_casc_CascStorage_readFile_fileId_localeFlags_openFlags_hint(
948            self_: *mut whiteout_CascStorage,
949            file_id: i32,
950            locale_flags: u32,
951            open_flags: u32,
952            hint: i32,
953        ) -> RawBytes;
954        pub fn whiteout_casc_CascStorage_fileExists(
955            self_: *mut whiteout_CascStorage,
956            casc_path: *const core::ffi::c_char,
957        ) -> i32;
958        pub fn whiteout_casc_CascStorage_fileExists_fileId_hint(
959            self_: *mut whiteout_CascStorage,
960            file_id: i32,
961            hint: i32,
962        ) -> i32;
963        pub fn whiteout_casc_CascStorage_fileSize(
964            self_: *mut whiteout_CascStorage,
965            casc_path: *const core::ffi::c_char,
966            out_value: *mut u64,
967        ) -> i32;
968        pub fn whiteout_casc_CascStorage_fileSize_fileId_hint(
969            self_: *mut whiteout_CascStorage,
970            file_id: i32,
971            hint: i32,
972            out_value: *mut u64,
973        ) -> i32;
974        pub fn whiteout_casc_CascStorage_listFiles(
975            self_: *mut whiteout_CascStorage,
976        ) -> *mut whiteout_StringList;
977        pub fn whiteout_casc_CascStorage_listEntries_snapshot(
978            self_: *mut whiteout_CascStorage,
979        ) -> *mut core::ffi::c_void;
980        pub fn whiteout_casc_CascStorage_listEntries_count(
981            snapshot: *mut core::ffi::c_void,
982        ) -> usize;
983        pub fn whiteout_casc_CascStorage_listEntries_cKey_at(
984            snapshot: *mut core::ffi::c_void,
985            index: usize,
986        ) -> RawBytes;
987        pub fn whiteout_casc_CascStorage_listEntries_fileSize_at(
988            snapshot: *mut core::ffi::c_void,
989            index: usize,
990        ) -> u64;
991        pub fn whiteout_casc_CascStorage_listEntries_localeFlags_at(
992            snapshot: *mut core::ffi::c_void,
993            index: usize,
994        ) -> u32;
995        pub fn whiteout_casc_CascStorage_listEntries_contentFlags_at(
996            snapshot: *mut core::ffi::c_void,
997            index: usize,
998        ) -> u32;
999        pub fn whiteout_casc_CascStorage_listEntries_fileDataId_at(
1000            snapshot: *mut core::ffi::c_void,
1001            index: usize,
1002        ) -> i32;
1003        pub fn whiteout_casc_CascStorage_listEntries_path_at(
1004            snapshot: *mut core::ffi::c_void,
1005            index: usize,
1006        ) -> RawCString;
1007        pub fn whiteout_casc_CascStorage_listEntries_free(snapshot: *mut core::ffi::c_void);
1008        pub fn whiteout_casc_CascStorage_totalFileCount(
1009            self_: *mut whiteout_CascStorage,
1010            out_value: *mut u32,
1011        ) -> i32;
1012        pub fn whiteout_casc_CascStorage_importKeysFromString(
1013            self_: *mut whiteout_CascStorage,
1014            key_list: *const core::ffi::c_char,
1015        ) -> i32;
1016        pub fn whiteout_casc_CascStorage_importKeysFromFile(
1017            self_: *mut whiteout_CascStorage,
1018            key_file_path: *const core::ffi::c_char,
1019        ) -> i32;
1020        pub fn whiteout_casc_CascStorage_findEncryptionKey(
1021            self_: *mut whiteout_CascStorage,
1022            key_name: u64,
1023            out_value: *mut u8,
1024        ) -> i32;
1025        pub fn whiteout_casc_CascStorage_flushCache(self_: *mut whiteout_CascStorage);
1026        pub fn whiteout_casc_CascStorage_prefetch(self_: *mut whiteout_CascStorage) -> i32;
1027        pub fn whiteout_casc_CascStorage_lastError() -> u32;
1028        // StorageWritable
1029        pub fn whiteout_casc_CascStorageWritable_delete(self_: *mut whiteout_CascStorageWritable);
1030        pub fn whiteout_casc_CascStorageWritable_create(
1031            opts: *mut whiteout_CascCreateOptions,
1032            pool: *mut core::ffi::c_void,
1033        ) -> *mut whiteout_CascStorageWritable;
1034        pub fn whiteout_casc_CascStorageWritable_reserveFileId(
1035            self_: *mut whiteout_CascStorageWritable,
1036            name: *const core::ffi::c_char,
1037            out_value: *mut u32,
1038        ) -> i32;
1039        pub fn whiteout_casc_CascStorageWritable_writeFile(
1040            self_: *mut whiteout_CascStorageWritable,
1041            path: *const core::ffi::c_char,
1042            data: *const u8,
1043            data_size: usize,
1044            opts: *mut whiteout_CascWriteOptions,
1045        ) -> i32;
1046        pub fn whiteout_casc_CascStorageWritable_writeFile_fileId_data_opts_hint(
1047            self_: *mut whiteout_CascStorageWritable,
1048            file_id: i32,
1049            data: *const u8,
1050            data_size: usize,
1051            opts: *mut whiteout_CascWriteOptions,
1052            hint: i32,
1053        ) -> i32;
1054        pub fn whiteout_casc_CascStorageWritable_deleteFile(
1055            self_: *mut whiteout_CascStorageWritable,
1056            path: *const core::ffi::c_char,
1057        ) -> i32;
1058        pub fn whiteout_casc_CascStorageWritable_deleteFile_fileId_hint(
1059            self_: *mut whiteout_CascStorageWritable,
1060            file_id: i32,
1061            hint: i32,
1062        ) -> i32;
1063        pub fn whiteout_casc_CascStorageWritable_save(
1064            self_: *mut whiteout_CascStorageWritable,
1065        ) -> i32;
1066        pub fn whiteout_casc_CascStorageWritable_save_path(
1067            self_: *mut whiteout_CascStorageWritable,
1068            path: *const core::ffi::c_char,
1069        ) -> i32;
1070    }
1071}