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    /// How many entries enumerate() will visit.
396    ///
397    /// The denominator a caller needs to report progress across a walk: on a StarCraft II install that is three quarters of a million entries, and without a total the only honest thing a UI can draw is a marquee.
398    ///
399    /// Cheap — the root manifest already knows — but it forces the deferred load on a LoadOnDemand storage, exactly as enumerate() would.
400    ///
401    /// @return 0 when the storage has no root, or the root cannot say.
402    pub fn entry_count(&self) -> u64 {
403        // SAFETY: handle is live for the duration of the call.
404        unsafe { ffi::whiteout_casc_CascStorage_entryCount(self.raw.as_ptr()) }
405    }
406
407    /// @return File contents, or std::nullopt if the path is not found.
408    pub fn read_file(&self, casc_path: &str) -> Option<Bytes> {
409        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
410        // SAFETY: handle is live for the duration of the call.
411        unsafe {
412            Bytes::from_raw(ffi::whiteout_casc_CascStorage_readFile(
413                self.raw.as_ptr(),
414                casc_path_cstr.as_ptr(),
415            ))
416        }
417    }
418
419    /// @overload Read a file by path with locale and open flags.
420    pub fn read_file_casc_path_locale_flags_open_flags(
421        &self,
422        casc_path: &str,
423        locale_flags: u32,
424        open_flags: u32,
425    ) -> Option<Bytes> {
426        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
427        // SAFETY: handle is live for the duration of the call.
428        unsafe {
429            Bytes::from_raw(
430                ffi::whiteout_casc_CascStorage_readFile_cascPath_localeFlags_openFlags(
431                    self.raw.as_ptr(),
432                    casc_path_cstr.as_ptr(),
433                    locale_flags,
434                    open_flags,
435                ),
436            )
437        }
438    }
439
440    /// @overload Read a file by WoW-style FileDataId.
441    pub fn read_file_file_id_hint(&self, file_id: i32, hint: FileIdHint) -> Option<Bytes> {
442        // SAFETY: handle is live for the duration of the call.
443        unsafe {
444            Bytes::from_raw(ffi::whiteout_casc_CascStorage_readFile_fileId_hint(
445                self.raw.as_ptr(),
446                file_id,
447                hint as i32,
448            ))
449        }
450    }
451
452    /// @overload Read a file by FileDataId with locale and open flags.
453    pub fn read_file_file_id_locale_flags_open_flags_hint(
454        &self,
455        file_id: i32,
456        locale_flags: u32,
457        open_flags: u32,
458        hint: FileIdHint,
459    ) -> Option<Bytes> {
460        // SAFETY: handle is live for the duration of the call.
461        unsafe {
462            Bytes::from_raw(
463                ffi::whiteout_casc_CascStorage_readFile_fileId_localeFlags_openFlags_hint(
464                    self.raw.as_ptr(),
465                    file_id,
466                    locale_flags,
467                    open_flags,
468                    hint as i32,
469                ),
470            )
471        }
472    }
473
474    /// @return True if the path resolves to a known file.
475    pub fn file_exists(&self, casc_path: &str) -> bool {
476        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
477        // SAFETY: handle is live for the duration of the call.
478        unsafe {
479            ffi::whiteout_casc_CascStorage_fileExists(self.raw.as_ptr(), casc_path_cstr.as_ptr())
480                != 0
481        }
482    }
483
484    /// @overload Check existence by FileDataId.
485    pub fn file_exists_file_id_hint(&self, file_id: i32, hint: FileIdHint) -> bool {
486        // SAFETY: handle is live for the duration of the call.
487        unsafe {
488            ffi::whiteout_casc_CascStorage_fileExists_fileId_hint(
489                self.raw.as_ptr(),
490                file_id,
491                hint as i32,
492            ) != 0
493        }
494    }
495
496    /// @return Uncompressed file size, or std::nullopt if not found.
497    pub fn file_size(&self, casc_path: &str) -> Option<u64> {
498        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
499        let mut __v: u64 = 0;
500        // SAFETY: `__v` is a live local, written by the
501        // native side only when it returns 1.
502        let __has = unsafe {
503            ffi::whiteout_casc_CascStorage_fileSize(
504                self.raw.as_ptr(),
505                casc_path_cstr.as_ptr(),
506                &mut __v,
507            )
508        };
509        (__has != 0).then_some(__v)
510    }
511
512    /// @overload
513    pub fn file_size_file_id_hint(&self, file_id: i32, hint: FileIdHint) -> Option<u64> {
514        let mut __v: u64 = 0;
515        // SAFETY: `__v` is a live local, written by the
516        // native side only when it returns 1.
517        let __has = unsafe {
518            ffi::whiteout_casc_CascStorage_fileSize_fileId_hint(
519                self.raw.as_ptr(),
520                file_id,
521                hint as i32,
522                &mut __v,
523            )
524        };
525        (__has != 0).then_some(__v)
526    }
527
528    /// @return All known file paths.
529    pub fn list_files(&self) -> Vec<String> {
530        // SAFETY: one call materialises the list; the
531        // elements are borrowed out of it and it is freed
532        // before returning. Reading is O(1) per element.
533        unsafe {
534            let list = ffi::whiteout_casc_CascStorage_listFiles(self.raw.as_ptr());
535            if list.is_null() {
536                return Vec::new();
537            }
538            let n = ffi::whiteout_casc_StringList_size(list);
539            let out = (0..n)
540                .map(|i| crate::support::take_string(ffi::whiteout_casc_StringList_at(list, i)))
541                .collect();
542            ffi::whiteout_casc_StringList_delete(list);
543            out
544        }
545    }
546
547    /// @return All entries with metadata.
548    pub fn list_entries(&self) -> Vec<FindEntry> {
549        // SAFETY: one call materialises the snapshot; each
550        // field is read by index and the snapshot is freed
551        // before returning. Reading is O(1) per element.
552        unsafe {
553            let snap = ffi::whiteout_casc_CascStorage_listEntries_snapshot(self.raw.as_ptr());
554            if snap.is_null() {
555                return Vec::new();
556            }
557            let n = ffi::whiteout_casc_CascStorage_listEntries_count(snap);
558            let mut out = Vec::with_capacity(n);
559            for i in 0..n {
560                out.push(FindEntry {
561                    c_key: crate::support::Bytes::from_raw(
562                        ffi::whiteout_casc_CascStorage_listEntries_cKey_at(snap, i),
563                    )
564                    .map(|b| b.to_vec())
565                    .unwrap_or_default(),
566                    file_size: ffi::whiteout_casc_CascStorage_listEntries_fileSize_at(snap, i),
567                    locale_flags: ffi::whiteout_casc_CascStorage_listEntries_localeFlags_at(
568                        snap, i,
569                    ),
570                    content_flags: ffi::whiteout_casc_CascStorage_listEntries_contentFlags_at(
571                        snap, i,
572                    ),
573                    file_data_id: ffi::whiteout_casc_CascStorage_listEntries_fileDataId_at(snap, i),
574                    path: crate::support::take_string(
575                        ffi::whiteout_casc_CascStorage_listEntries_path_at(snap, i),
576                    ),
577                });
578            }
579            ffi::whiteout_casc_CascStorage_listEntries_free(snap);
580            out
581        }
582    }
583
584    /// @return Total number of files in the root manifest.
585    pub fn total_file_count(&self) -> Option<u32> {
586        let mut __v: u32 = 0;
587        // SAFETY: `__v` is a live local, written by the
588        // native side only when it returns 1.
589        let __has =
590            unsafe { ffi::whiteout_casc_CascStorage_totalFileCount(self.raw.as_ptr(), &mut __v) };
591        (__has != 0).then_some(__v)
592    }
593
594    /// Import encryption keys from a formatted string (one per line).
595    pub fn import_keys_from_string(&mut self, key_list: &str) -> bool {
596        let key_list_cstr = std::ffi::CString::new(key_list).unwrap_or_default();
597        // SAFETY: handle is live for the duration of the call.
598        unsafe {
599            ffi::whiteout_casc_CascStorage_importKeysFromString(
600                self.raw.as_ptr(),
601                key_list_cstr.as_ptr(),
602            ) != 0
603        }
604    }
605
606    /// Import encryption keys from a file.
607    pub fn import_keys_from_file(&mut self, key_file_path: &str) -> bool {
608        let key_file_path_cstr = std::ffi::CString::new(key_file_path).unwrap_or_default();
609        // SAFETY: handle is live for the duration of the call.
610        unsafe {
611            ffi::whiteout_casc_CascStorage_importKeysFromFile(
612                self.raw.as_ptr(),
613                key_file_path_cstr.as_ptr(),
614            ) != 0
615        }
616    }
617
618    /// Substitute zeros for any frame whose encryption key is unavailable, instead of failing the read. Off by default.
619    ///
620    /// Unreleased content ships encrypted with keys that are not published, and a single such frame otherwise takes a whole file with it — a client database that is 99% readable is worth more than none of it. CascLib spells this CASC_OVERCOME_ENCRYPTED. Turn it on only where a partly blank file is more useful than no file.
621    pub fn set_zero_fill_encrypted(&mut self, on: bool) {
622        // SAFETY: handle is live for the duration of the call.
623        unsafe {
624            ffi::whiteout_casc_CascStorage_setZeroFillEncrypted(
625                self.raw.as_ptr(),
626                if on { 1 } else { 0 },
627            );
628        }
629    }
630
631    /// @return The encryption key for @p keyName, or std::nullopt if not found.
632    pub fn find_encryption_key(&self, key_name: u64) -> Option<[u8; 16]> {
633        let mut __v: [u8; 16] = Default::default();
634        // SAFETY: `__v` is a live local of exactly the
635        // length the native side writes.
636        let __has = unsafe {
637            ffi::whiteout_casc_CascStorage_findEncryptionKey(
638                self.raw.as_ptr(),
639                key_name,
640                __v.as_mut_ptr(),
641            )
642        };
643        (__has != 0).then_some(__v)
644    }
645
646    /// Clear the in-memory decoded-data cache (container cache).
647    pub fn flush_cache(&mut self) {
648        // SAFETY: handle is live for the duration of the call.
649        unsafe {
650            ffi::whiteout_casc_CascStorage_flushCache(self.raw.as_ptr());
651        }
652    }
653
654    /// Force every deferred load (encoding, root, VFS, index files, orphan bitvector) to resolve. Idempotent.
655    pub fn prefetch(&mut self) -> bool {
656        // SAFETY: handle is live for the duration of the call.
657        unsafe { ffi::whiteout_casc_CascStorage_prefetch(self.raw.as_ptr()) != 0 }
658    }
659
660    /// @return Last error code (thread-local).
661    pub fn last_error() -> u32 {
662        // SAFETY: handle is live for the duration of the call.
663        unsafe { ffi::whiteout_casc_CascStorage_lastError() }
664    }
665}
666
667/// Writable CASC storage (read + write + save)
668///
669/// Inherits all read operations from Storage. Adds write overlay and persist-to-disk support.
670///
671/// Only local-backed storages can be writable (CDN is read-only).
672///
673/// extends=whiteout::storages::casc::Storage
674pub struct StorageWritable {
675    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_CascStorageWritable>,
676}
677
678impl Drop for StorageWritable {
679    fn drop(&mut self) {
680        // SAFETY: `raw` came from a native constructor and Drop runs once.
681        unsafe { ffi::whiteout_casc_CascStorageWritable_delete(self.raw.as_ptr()) }
682    }
683}
684
685impl StorageWritable {
686    /// # Safety
687    /// `raw` must be a live handle this value takes ownership of.
688    #[allow(dead_code)] // used by whichever methods return this type
689    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_CascStorageWritable) -> Option<Self> {
690        core::ptr::NonNull::new(raw).map(|raw| StorageWritable { raw })
691    }
692}
693
694// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
695// is deliberately NOT implemented — the C++ types make no documented
696// guarantee about concurrent use, and claiming one we haven't verified
697// would be unsound. See `@bind thread_safe` in the plan.
698unsafe impl Send for StorageWritable {}
699
700impl core::fmt::Debug for StorageWritable {
701    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
702        f.debug_struct("StorageWritable").finish_non_exhaustive()
703    }
704}
705
706impl StorageWritable {
707    /// Create a new empty storage in memory.
708    ///
709    /// No file is written to disk until save() is called.
710    ///
711    /// @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.
712    pub fn create(
713        opts: &CreateOptions,
714        pool: Option<&crate::interfaces::HostWorkerPool>,
715    ) -> Option<StorageWritable> {
716        // SAFETY: handle is live for the duration of the call.
717        unsafe {
718            StorageWritable::from_raw(ffi::whiteout_casc_CascStorageWritable_create(
719                opts.raw.as_ptr(),
720                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
721            ))
722        }
723    }
724
725    /// Reserve a file-data-ID for a named asset.
726    ///
727    /// Allocates the next available file-data-ID and associates it with @p name.  The interpretation of @p name depends on the root format:
728    ///
729    /// - **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.
730    ///
731    /// Returns @c std::nullopt if the name already exists in the root or in a previous reservation.
732    ///
733    /// @code auto id = storage.reserveFileId("my_beast.app"); if (id) storage.writeFile(*id, data); @endcode
734    pub fn reserve_file_id(&mut self, name: &str) -> Option<u32> {
735        let name_cstr = std::ffi::CString::new(name).unwrap_or_default();
736        let mut __v: u32 = 0;
737        // SAFETY: `__v` is a live local, written by the
738        // native side only when it returns 1.
739        let __has = unsafe {
740            ffi::whiteout_casc_CascStorageWritable_reserveFileId(
741                self.raw.as_ptr(),
742                name_cstr.as_ptr(),
743                &mut __v,
744            )
745        };
746        (__has != 0).then_some(__v)
747    }
748
749    /// Write a file by path.
750    ///
751    /// Data is stored in an in-memory overlay until save() is called.
752    ///
753    /// @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.
754    pub fn write_file(&mut self, path: &str, data: &[u8], opts: &WriteOptions) -> bool {
755        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
756        let opts_native = unsafe { opts.to_native() };
757        // SAFETY: handle is live for the call; the staged
758        // option handles are freed immediately after.
759        unsafe {
760            let __r = ffi::whiteout_casc_CascStorageWritable_writeFile(
761                self.raw.as_ptr(),
762                path_cstr.as_ptr(),
763                data.as_ptr(),
764                data.len(),
765                opts_native,
766            ) != 0;
767            WriteOptions::free_native(opts_native);
768            __r
769        }
770    }
771
772    /// @overload Write a file by FileDataId.
773    pub fn write_file_file_id_data_opts_hint(
774        &mut self,
775        file_id: i32,
776        data: &[u8],
777        opts: &WriteOptions,
778        hint: FileIdHint,
779    ) -> bool {
780        let opts_native = unsafe { opts.to_native() };
781        // SAFETY: handle is live for the call; the staged
782        // option handles are freed immediately after.
783        unsafe {
784            let __r = ffi::whiteout_casc_CascStorageWritable_writeFile_fileId_data_opts_hint(
785                self.raw.as_ptr(),
786                file_id,
787                data.as_ptr(),
788                data.len(),
789                opts_native,
790                hint as i32,
791            ) != 0;
792            WriteOptions::free_native(opts_native);
793            __r
794        }
795    }
796
797    /// Mark a file for deletion (effective on next save).
798    pub fn delete_file(&mut self, path: &str) -> bool {
799        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
800        // SAFETY: handle is live for the duration of the call.
801        unsafe {
802            ffi::whiteout_casc_CascStorageWritable_deleteFile(self.raw.as_ptr(), path_cstr.as_ptr())
803                != 0
804        }
805    }
806
807    /// @overload
808    pub fn delete_file_file_id_hint(&mut self, file_id: i32, hint: FileIdHint) -> bool {
809        // SAFETY: handle is live for the duration of the call.
810        unsafe {
811            ffi::whiteout_casc_CascStorageWritable_deleteFile_fileId_hint(
812                self.raw.as_ptr(),
813                file_id,
814                hint as i32,
815            ) != 0
816        }
817    }
818
819    /// Persist all pending changes to disk (writes to the original location).
820    pub fn save(&mut self) -> bool {
821        // SAFETY: handle is live for the duration of the call.
822        unsafe { ffi::whiteout_casc_CascStorageWritable_save(self.raw.as_ptr()) != 0 }
823    }
824
825    /// @overload Persist to a specific output path.
826    pub fn save_path(&mut self, path: &str) -> bool {
827        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
828        // SAFETY: handle is live for the duration of the call.
829        unsafe {
830            ffi::whiteout_casc_CascStorageWritable_save_path(self.raw.as_ptr(), path_cstr.as_ptr())
831                != 0
832        }
833    }
834}
835
836// Not yet bound (shape unsupported by the emitter):
837//   - Storage::open_opts (parameter shape)
838
839#[doc(hidden)]
840pub mod ffi {
841    #![allow(missing_debug_implementations)]
842
843    #[allow(unused_imports)]
844    use crate::support::{RawBytes, RawCString};
845
846    #[repr(C)]
847    pub struct whiteout_CascCreateOptions {
848        _private: [u8; 0],
849    }
850    #[repr(C)]
851    pub struct whiteout_CascWriteOptions {
852        _private: [u8; 0],
853    }
854    #[repr(C)]
855    pub struct whiteout_CascStorage {
856        _private: [u8; 0],
857    }
858    #[repr(C)]
859    pub struct whiteout_CascStorageWritable {
860        _private: [u8; 0],
861    }
862    #[repr(C)]
863    pub struct whiteout_StringList {
864        _private: [u8; 0],
865    }
866
867    extern "C" {
868        pub fn whiteout_casc_StringList_size(self_: *mut whiteout_StringList) -> usize;
869        pub fn whiteout_casc_StringList_at(
870            self_: *mut whiteout_StringList,
871            index: usize,
872        ) -> RawCString;
873        pub fn whiteout_casc_StringList_delete(self_: *mut whiteout_StringList);
874        // CreateOptions
875        pub fn whiteout_casc_CascCreateOptions_new() -> *mut whiteout_CascCreateOptions;
876        pub fn whiteout_casc_CascCreateOptions_delete(self_: *mut whiteout_CascCreateOptions);
877        pub fn whiteout_casc_CascCreateOptions_get_product(
878            self_: *mut whiteout_CascCreateOptions,
879        ) -> RawCString;
880        pub fn whiteout_casc_CascCreateOptions_set_product(
881            self_: *mut whiteout_CascCreateOptions,
882            value: *const core::ffi::c_char,
883        );
884        pub fn whiteout_casc_CascCreateOptions_get_version(
885            self_: *mut whiteout_CascCreateOptions,
886        ) -> RawCString;
887        pub fn whiteout_casc_CascCreateOptions_set_version(
888            self_: *mut whiteout_CascCreateOptions,
889            value: *const core::ffi::c_char,
890        );
891        pub fn whiteout_casc_CascCreateOptions_get_archiveMaxSize(
892            self_: *mut whiteout_CascCreateOptions,
893        ) -> u32;
894        pub fn whiteout_casc_CascCreateOptions_set_archiveMaxSize(
895            self_: *mut whiteout_CascCreateOptions,
896            value: u32,
897        );
898        pub fn whiteout_casc_CascCreateOptions_get_blteFrameSize(
899            self_: *mut whiteout_CascCreateOptions,
900        ) -> u32;
901        pub fn whiteout_casc_CascCreateOptions_set_blteFrameSize(
902            self_: *mut whiteout_CascCreateOptions,
903            value: u32,
904        );
905        pub fn whiteout_casc_CascCreateOptions_get_rootFormat(
906            self_: *mut whiteout_CascCreateOptions,
907        ) -> i32;
908        pub fn whiteout_casc_CascCreateOptions_set_rootFormat(
909            self_: *mut whiteout_CascCreateOptions,
910            value: i32,
911        );
912        // WriteOptions
913        pub fn whiteout_casc_CascWriteOptions_new() -> *mut whiteout_CascWriteOptions;
914        pub fn whiteout_casc_CascWriteOptions_delete(self_: *mut whiteout_CascWriteOptions);
915        pub fn whiteout_casc_CascWriteOptions_get_localeFlags(
916            self_: *mut whiteout_CascWriteOptions,
917        ) -> u32;
918        pub fn whiteout_casc_CascWriteOptions_set_localeFlags(
919            self_: *mut whiteout_CascWriteOptions,
920            value: u32,
921        );
922        pub fn whiteout_casc_CascWriteOptions_get_contentFlags(
923            self_: *mut whiteout_CascWriteOptions,
924        ) -> u32;
925        pub fn whiteout_casc_CascWriteOptions_set_contentFlags(
926            self_: *mut whiteout_CascWriteOptions,
927            value: u32,
928        );
929        pub fn whiteout_casc_CascWriteOptions_get_compress(
930            self_: *mut whiteout_CascWriteOptions,
931        ) -> i32;
932        pub fn whiteout_casc_CascWriteOptions_set_compress(
933            self_: *mut whiteout_CascWriteOptions,
934            value: i32,
935        );
936        // Storage
937        pub fn whiteout_casc_CascStorage_delete(self_: *mut whiteout_CascStorage);
938        pub fn whiteout_casc_CascStorage_open(
939            path: *const core::ffi::c_char,
940            pool: *mut core::ffi::c_void,
941        ) -> *mut whiteout_CascStorage;
942        pub fn whiteout_casc_CascStorage_open_path_localeMask_pool(
943            path: *const core::ffi::c_char,
944            locale_mask: u32,
945            pool: *mut core::ffi::c_void,
946        ) -> *mut whiteout_CascStorage;
947        pub fn whiteout_casc_CascStorage_open_path_product_pool(
948            path: *const core::ffi::c_char,
949            product: *const core::ffi::c_char,
950            pool: *mut core::ffi::c_void,
951        ) -> *mut whiteout_CascStorage;
952        pub fn whiteout_casc_CascStorage_close(self_: *mut whiteout_CascStorage);
953        pub fn whiteout_casc_CascStorage_isLocal(self_: *mut whiteout_CascStorage) -> i32;
954        pub fn whiteout_casc_CascStorage_isOnline(self_: *mut whiteout_CascStorage) -> i32;
955        pub fn whiteout_casc_CascStorage_isWritable(self_: *mut whiteout_CascStorage) -> i32;
956        pub fn whiteout_casc_CascStorage_rootFormat(self_: *mut whiteout_CascStorage) -> i32;
957        pub fn whiteout_casc_CascStorage_entryCount(self_: *mut whiteout_CascStorage) -> u64;
958        pub fn whiteout_casc_CascStorage_readFile(
959            self_: *mut whiteout_CascStorage,
960            casc_path: *const core::ffi::c_char,
961        ) -> RawBytes;
962        pub fn whiteout_casc_CascStorage_readFile_cascPath_localeFlags_openFlags(
963            self_: *mut whiteout_CascStorage,
964            casc_path: *const core::ffi::c_char,
965            locale_flags: u32,
966            open_flags: u32,
967        ) -> RawBytes;
968        pub fn whiteout_casc_CascStorage_readFile_fileId_hint(
969            self_: *mut whiteout_CascStorage,
970            file_id: i32,
971            hint: i32,
972        ) -> RawBytes;
973        pub fn whiteout_casc_CascStorage_readFile_fileId_localeFlags_openFlags_hint(
974            self_: *mut whiteout_CascStorage,
975            file_id: i32,
976            locale_flags: u32,
977            open_flags: u32,
978            hint: i32,
979        ) -> RawBytes;
980        pub fn whiteout_casc_CascStorage_fileExists(
981            self_: *mut whiteout_CascStorage,
982            casc_path: *const core::ffi::c_char,
983        ) -> i32;
984        pub fn whiteout_casc_CascStorage_fileExists_fileId_hint(
985            self_: *mut whiteout_CascStorage,
986            file_id: i32,
987            hint: i32,
988        ) -> i32;
989        pub fn whiteout_casc_CascStorage_fileSize(
990            self_: *mut whiteout_CascStorage,
991            casc_path: *const core::ffi::c_char,
992            out_value: *mut u64,
993        ) -> i32;
994        pub fn whiteout_casc_CascStorage_fileSize_fileId_hint(
995            self_: *mut whiteout_CascStorage,
996            file_id: i32,
997            hint: i32,
998            out_value: *mut u64,
999        ) -> i32;
1000        pub fn whiteout_casc_CascStorage_listFiles(
1001            self_: *mut whiteout_CascStorage,
1002        ) -> *mut whiteout_StringList;
1003        pub fn whiteout_casc_CascStorage_listEntries_snapshot(
1004            self_: *mut whiteout_CascStorage,
1005        ) -> *mut core::ffi::c_void;
1006        pub fn whiteout_casc_CascStorage_listEntries_count(
1007            snapshot: *mut core::ffi::c_void,
1008        ) -> usize;
1009        pub fn whiteout_casc_CascStorage_listEntries_cKey_at(
1010            snapshot: *mut core::ffi::c_void,
1011            index: usize,
1012        ) -> RawBytes;
1013        pub fn whiteout_casc_CascStorage_listEntries_fileSize_at(
1014            snapshot: *mut core::ffi::c_void,
1015            index: usize,
1016        ) -> u64;
1017        pub fn whiteout_casc_CascStorage_listEntries_localeFlags_at(
1018            snapshot: *mut core::ffi::c_void,
1019            index: usize,
1020        ) -> u32;
1021        pub fn whiteout_casc_CascStorage_listEntries_contentFlags_at(
1022            snapshot: *mut core::ffi::c_void,
1023            index: usize,
1024        ) -> u32;
1025        pub fn whiteout_casc_CascStorage_listEntries_fileDataId_at(
1026            snapshot: *mut core::ffi::c_void,
1027            index: usize,
1028        ) -> i32;
1029        pub fn whiteout_casc_CascStorage_listEntries_path_at(
1030            snapshot: *mut core::ffi::c_void,
1031            index: usize,
1032        ) -> RawCString;
1033        pub fn whiteout_casc_CascStorage_listEntries_free(snapshot: *mut core::ffi::c_void);
1034        pub fn whiteout_casc_CascStorage_totalFileCount(
1035            self_: *mut whiteout_CascStorage,
1036            out_value: *mut u32,
1037        ) -> i32;
1038        pub fn whiteout_casc_CascStorage_importKeysFromString(
1039            self_: *mut whiteout_CascStorage,
1040            key_list: *const core::ffi::c_char,
1041        ) -> i32;
1042        pub fn whiteout_casc_CascStorage_importKeysFromFile(
1043            self_: *mut whiteout_CascStorage,
1044            key_file_path: *const core::ffi::c_char,
1045        ) -> i32;
1046        pub fn whiteout_casc_CascStorage_setZeroFillEncrypted(
1047            self_: *mut whiteout_CascStorage,
1048            on: i32,
1049        );
1050        pub fn whiteout_casc_CascStorage_findEncryptionKey(
1051            self_: *mut whiteout_CascStorage,
1052            key_name: u64,
1053            out_value: *mut u8,
1054        ) -> i32;
1055        pub fn whiteout_casc_CascStorage_flushCache(self_: *mut whiteout_CascStorage);
1056        pub fn whiteout_casc_CascStorage_prefetch(self_: *mut whiteout_CascStorage) -> i32;
1057        pub fn whiteout_casc_CascStorage_lastError() -> u32;
1058        // StorageWritable
1059        pub fn whiteout_casc_CascStorageWritable_delete(self_: *mut whiteout_CascStorageWritable);
1060        pub fn whiteout_casc_CascStorageWritable_create(
1061            opts: *mut whiteout_CascCreateOptions,
1062            pool: *mut core::ffi::c_void,
1063        ) -> *mut whiteout_CascStorageWritable;
1064        pub fn whiteout_casc_CascStorageWritable_reserveFileId(
1065            self_: *mut whiteout_CascStorageWritable,
1066            name: *const core::ffi::c_char,
1067            out_value: *mut u32,
1068        ) -> i32;
1069        pub fn whiteout_casc_CascStorageWritable_writeFile(
1070            self_: *mut whiteout_CascStorageWritable,
1071            path: *const core::ffi::c_char,
1072            data: *const u8,
1073            data_size: usize,
1074            opts: *mut whiteout_CascWriteOptions,
1075        ) -> i32;
1076        pub fn whiteout_casc_CascStorageWritable_writeFile_fileId_data_opts_hint(
1077            self_: *mut whiteout_CascStorageWritable,
1078            file_id: i32,
1079            data: *const u8,
1080            data_size: usize,
1081            opts: *mut whiteout_CascWriteOptions,
1082            hint: i32,
1083        ) -> i32;
1084        pub fn whiteout_casc_CascStorageWritable_deleteFile(
1085            self_: *mut whiteout_CascStorageWritable,
1086            path: *const core::ffi::c_char,
1087        ) -> i32;
1088        pub fn whiteout_casc_CascStorageWritable_deleteFile_fileId_hint(
1089            self_: *mut whiteout_CascStorageWritable,
1090            file_id: i32,
1091            hint: i32,
1092        ) -> i32;
1093        pub fn whiteout_casc_CascStorageWritable_save(
1094            self_: *mut whiteout_CascStorageWritable,
1095        ) -> i32;
1096        pub fn whiteout_casc_CascStorageWritable_save_path(
1097            self_: *mut whiteout_CascStorageWritable,
1098            path: *const core::ffi::c_char,
1099        ) -> i32;
1100    }
1101}