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/// Options for creating a new empty CASC storage.
92pub struct CreateOptions {
93    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_CascCreateOptions>,
94}
95
96impl Drop for CreateOptions {
97    fn drop(&mut self) {
98        // SAFETY: `raw` came from a native constructor and Drop runs once.
99        unsafe { ffi::whiteout_casc_CascCreateOptions_delete(self.raw.as_ptr()) }
100    }
101}
102
103impl CreateOptions {
104    /// # Safety
105    /// `raw` must be a live handle this value takes ownership of.
106    #[allow(dead_code)] // used by whichever methods return this type
107    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_CascCreateOptions) -> Option<Self> {
108        core::ptr::NonNull::new(raw).map(|raw| CreateOptions { raw })
109    }
110}
111
112// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
113// is deliberately NOT implemented — the C++ types make no documented
114// guarantee about concurrent use, and claiming one we haven't verified
115// would be unsound. See `@bind thread_safe` in the plan.
116unsafe impl Send for CreateOptions {}
117
118impl core::fmt::Debug for CreateOptions {
119    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
120        f.debug_struct("CreateOptions").finish_non_exhaustive()
121    }
122}
123
124impl CreateOptions {
125    /// # Panics
126    /// Panics if the native allocation fails.
127    pub fn new() -> Self {
128        // SAFETY: the native constructor returns a live handle; a null here
129        // means the library is unusable.
130        unsafe {
131            let raw = ffi::whiteout_casc_CascCreateOptions_new();
132            Self::from_raw(raw).expect("native CreateOptions allocation failed")
133        }
134    }
135
136    pub fn product(&self) -> String {
137        // SAFETY: the native side hands over an owned CString.
138        unsafe {
139            crate::support::take_string(ffi::whiteout_casc_CascCreateOptions_get_product(
140                self.raw.as_ptr(),
141            ))
142        }
143    }
144
145    pub fn set_product(&mut self, value: &str) {
146        let value = std::ffi::CString::new(value).unwrap_or_default();
147        // SAFETY: the pointer outlives the call.
148        unsafe {
149            ffi::whiteout_casc_CascCreateOptions_set_product(self.raw.as_ptr(), value.as_ptr())
150        }
151    }
152
153    pub fn version(&self) -> String {
154        // SAFETY: the native side hands over an owned CString.
155        unsafe {
156            crate::support::take_string(ffi::whiteout_casc_CascCreateOptions_get_version(
157                self.raw.as_ptr(),
158            ))
159        }
160    }
161
162    pub fn set_version(&mut self, value: &str) {
163        let value = std::ffi::CString::new(value).unwrap_or_default();
164        // SAFETY: the pointer outlives the call.
165        unsafe {
166            ffi::whiteout_casc_CascCreateOptions_set_version(self.raw.as_ptr(), value.as_ptr())
167        }
168    }
169
170    /// 1 GB.
171    pub fn archive_max_size(&self) -> u32 {
172        // SAFETY: plain scalar read through a live handle.
173        unsafe { ffi::whiteout_casc_CascCreateOptions_get_archiveMaxSize(self.raw.as_ptr()) }
174    }
175
176    pub fn set_archive_max_size(&mut self, value: u32) {
177        // SAFETY: plain scalar write through a live handle.
178        unsafe { ffi::whiteout_casc_CascCreateOptions_set_archiveMaxSize(self.raw.as_ptr(), value) }
179    }
180
181    /// 64 KB.
182    pub fn blte_frame_size(&self) -> u32 {
183        // SAFETY: plain scalar read through a live handle.
184        unsafe { ffi::whiteout_casc_CascCreateOptions_get_blteFrameSize(self.raw.as_ptr()) }
185    }
186
187    pub fn set_blte_frame_size(&mut self, value: u32) {
188        // SAFETY: plain scalar write through a live handle.
189        unsafe { ffi::whiteout_casc_CascCreateOptions_set_blteFrameSize(self.raw.as_ptr(), value) }
190    }
191
192    pub fn root_format(&self) -> RootFormat {
193        // SAFETY: scalar read; the discriminant is validated below.
194        unsafe { ffi::whiteout_casc_CascCreateOptions_get_rootFormat(self.raw.as_ptr()) }
195            .try_into()
196            .expect("unknown enum discriminant from the native library")
197    }
198
199    pub fn set_root_format(&mut self, value: RootFormat) {
200        // SAFETY: scalar write through a live handle.
201        unsafe {
202            ffi::whiteout_casc_CascCreateOptions_set_rootFormat(self.raw.as_ptr(), value as i32)
203        }
204    }
205}
206
207impl Default for CreateOptions {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213/// Options for writing a file into a CASC storage.
214#[derive(Clone, Debug, PartialEq)]
215pub struct WriteOptions {
216    pub locale_flags: u32,
217    pub content_flags: u32,
218    pub compress: bool,
219}
220
221impl Default for WriteOptions {
222    fn default() -> Self {
223        // SAFETY: `_new` always returns a live handle; freed before return.
224        unsafe {
225            let h = ffi::whiteout_casc_CascWriteOptions_new();
226            let out = WriteOptions {
227                locale_flags: ffi::whiteout_casc_CascWriteOptions_get_localeFlags(h),
228                content_flags: ffi::whiteout_casc_CascWriteOptions_get_contentFlags(h),
229                compress: ffi::whiteout_casc_CascWriteOptions_get_compress(h) != 0,
230            };
231            ffi::whiteout_casc_CascWriteOptions_delete(h);
232            out
233        }
234    }
235}
236
237impl WriteOptions {
238    /// Build a native handle carrying these values. Caller frees it.
239    #[allow(dead_code)] // consumed once the methods taking these options bind
240    pub(crate) unsafe fn to_native(&self) -> *mut ffi::whiteout_CascWriteOptions {
241        unsafe {
242            let h = ffi::whiteout_casc_CascWriteOptions_new();
243            ffi::whiteout_casc_CascWriteOptions_set_localeFlags(h, self.locale_flags);
244            ffi::whiteout_casc_CascWriteOptions_set_contentFlags(h, self.content_flags);
245            ffi::whiteout_casc_CascWriteOptions_set_compress(h, if self.compress { 1 } else { 0 });
246            h
247        }
248    }
249
250    /// Free a handle produced by [`Self::to_native`].
251    ///
252    /// # Safety
253    /// `h` must have come from `to_native` and not been freed already.
254    #[allow(dead_code)]
255    pub(crate) unsafe fn free_native(h: *mut ffi::whiteout_CascWriteOptions) {
256        unsafe { ffi::whiteout_casc_CascWriteOptions_delete(h) }
257    }
258}
259
260/// Unified read-only CASC storage (local disk or CDN)
261///
262/// 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.
263///
264/// All public methods are thread-safe: read operations acquire a shared lock.
265///
266/// Uses the PImpl (Pointer to Implementation) idiom to hide internals.
267///
268/// @see StorageWritable for write + persist operations.
269pub struct Storage {
270    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_CascStorage>,
271}
272
273impl Drop for Storage {
274    fn drop(&mut self) {
275        // SAFETY: `raw` came from a native constructor and Drop runs once.
276        unsafe { ffi::whiteout_casc_CascStorage_delete(self.raw.as_ptr()) }
277    }
278}
279
280impl Storage {
281    /// # Safety
282    /// `raw` must be a live handle this value takes ownership of.
283    #[allow(dead_code)] // used by whichever methods return this type
284    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_CascStorage) -> Option<Self> {
285        core::ptr::NonNull::new(raw).map(|raw| Storage { raw })
286    }
287}
288
289// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
290// is deliberately NOT implemented — the C++ types make no documented
291// guarantee about concurrent use, and claiming one we haven't verified
292// would be unsound. See `@bind thread_safe` in the plan.
293unsafe impl Send for Storage {}
294
295impl core::fmt::Debug for Storage {
296    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
297        f.debug_struct("Storage").finish_non_exhaustive()
298    }
299}
300
301impl Storage {
302    /// 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.
303    pub fn open(path: &str, pool: Option<&crate::interfaces::HostWorkerPool>) -> Option<Storage> {
304        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
305        // SAFETY: handle is live for the duration of the call.
306        unsafe {
307            Storage::from_raw(ffi::whiteout_casc_CascStorage_open(
308                path_cstr.as_ptr(),
309                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
310            ))
311        }
312    }
313
314    /// @overload Open with locale mask.
315    pub fn open_path_locale_mask_pool(
316        path: &str,
317        locale_mask: u32,
318        pool: Option<&crate::interfaces::HostWorkerPool>,
319    ) -> Option<Storage> {
320        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
321        // SAFETY: handle is live for the duration of the call.
322        unsafe {
323            Storage::from_raw(ffi::whiteout_casc_CascStorage_open_path_localeMask_pool(
324                path_cstr.as_ptr(),
325                locale_mask,
326                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
327            ))
328        }
329    }
330
331    /// @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.
332    pub fn open_path_product_pool(
333        path: &str,
334        product: &str,
335        pool: Option<&crate::interfaces::HostWorkerPool>,
336    ) -> Option<Storage> {
337        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
338        let product_cstr = std::ffi::CString::new(product).unwrap_or_default();
339        // SAFETY: handle is live for the duration of the call.
340        unsafe {
341            Storage::from_raw(ffi::whiteout_casc_CascStorage_open_path_product_pool(
342                path_cstr.as_ptr(),
343                product_cstr.as_ptr(),
344                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
345            ))
346        }
347    }
348
349    /// Release all resources and invalidate the storage.
350    pub fn close(&mut self) {
351        // SAFETY: handle is live for the duration of the call.
352        unsafe {
353            ffi::whiteout_casc_CascStorage_close(self.raw.as_ptr());
354        }
355    }
356
357    /// @return True if this storage reads from local disk.
358    pub fn is_local(&self) -> bool {
359        // SAFETY: handle is live for the duration of the call.
360        unsafe { ffi::whiteout_casc_CascStorage_isLocal(self.raw.as_ptr()) != 0 }
361    }
362
363    /// @return True if this storage reads from CDN.
364    pub fn is_online(&self) -> bool {
365        // SAFETY: handle is live for the duration of the call.
366        unsafe { ffi::whiteout_casc_CascStorage_isOnline(self.raw.as_ptr()) != 0 }
367    }
368
369    /// @return True if this storage has a write overlay (StorageWritable).
370    pub fn is_writable(&self) -> bool {
371        // SAFETY: handle is live for the duration of the call.
372        unsafe { ffi::whiteout_casc_CascStorage_isWritable(self.raw.as_ptr()) != 0 }
373    }
374
375    /// @return The root manifest format, or RootFormat::Unknown.
376    pub fn root_format(&self) -> RootFormat {
377        // SAFETY: handle is live for the duration of the call.
378        unsafe {
379            RootFormat::try_from(ffi::whiteout_casc_CascStorage_rootFormat(self.raw.as_ptr()))
380                .expect("unknown enum discriminant from the native library (ABI version skew)")
381        }
382    }
383
384    /// @return File contents, or std::nullopt if the path is not found.
385    pub fn read_file(&self, casc_path: &str) -> Option<Bytes> {
386        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
387        // SAFETY: handle is live for the duration of the call.
388        unsafe {
389            Bytes::from_raw(ffi::whiteout_casc_CascStorage_readFile(
390                self.raw.as_ptr(),
391                casc_path_cstr.as_ptr(),
392            ))
393        }
394    }
395
396    /// @overload Read a file by path with locale and open flags.
397    pub fn read_file_casc_path_locale_flags_open_flags(
398        &self,
399        casc_path: &str,
400        locale_flags: u32,
401        open_flags: u32,
402    ) -> Option<Bytes> {
403        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
404        // SAFETY: handle is live for the duration of the call.
405        unsafe {
406            Bytes::from_raw(
407                ffi::whiteout_casc_CascStorage_readFile_cascPath_localeFlags_openFlags(
408                    self.raw.as_ptr(),
409                    casc_path_cstr.as_ptr(),
410                    locale_flags,
411                    open_flags,
412                ),
413            )
414        }
415    }
416
417    /// @overload Read a file by WoW-style FileDataId.
418    pub fn read_file_file_id_hint(&self, file_id: i32, hint: FileIdHint) -> Option<Bytes> {
419        // SAFETY: handle is live for the duration of the call.
420        unsafe {
421            Bytes::from_raw(ffi::whiteout_casc_CascStorage_readFile_fileId_hint(
422                self.raw.as_ptr(),
423                file_id,
424                hint as i32,
425            ))
426        }
427    }
428
429    /// @overload Read a file by FileDataId with locale and open flags.
430    pub fn read_file_file_id_locale_flags_open_flags_hint(
431        &self,
432        file_id: i32,
433        locale_flags: u32,
434        open_flags: u32,
435        hint: FileIdHint,
436    ) -> Option<Bytes> {
437        // SAFETY: handle is live for the duration of the call.
438        unsafe {
439            Bytes::from_raw(
440                ffi::whiteout_casc_CascStorage_readFile_fileId_localeFlags_openFlags_hint(
441                    self.raw.as_ptr(),
442                    file_id,
443                    locale_flags,
444                    open_flags,
445                    hint as i32,
446                ),
447            )
448        }
449    }
450
451    /// @return True if the path resolves to a known file.
452    pub fn file_exists(&self, casc_path: &str) -> bool {
453        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
454        // SAFETY: handle is live for the duration of the call.
455        unsafe {
456            ffi::whiteout_casc_CascStorage_fileExists(self.raw.as_ptr(), casc_path_cstr.as_ptr())
457                != 0
458        }
459    }
460
461    /// @overload Check existence by FileDataId.
462    pub fn file_exists_file_id_hint(&self, file_id: i32, hint: FileIdHint) -> bool {
463        // SAFETY: handle is live for the duration of the call.
464        unsafe {
465            ffi::whiteout_casc_CascStorage_fileExists_fileId_hint(
466                self.raw.as_ptr(),
467                file_id,
468                hint as i32,
469            ) != 0
470        }
471    }
472
473    /// @return Uncompressed file size, or std::nullopt if not found.
474    pub fn file_size(&self, casc_path: &str) -> Option<u64> {
475        let casc_path_cstr = std::ffi::CString::new(casc_path).unwrap_or_default();
476        let mut __v: u64 = 0;
477        // SAFETY: `__v` is a live local, written by the
478        // native side only when it returns 1.
479        let __has = unsafe {
480            ffi::whiteout_casc_CascStorage_fileSize(
481                self.raw.as_ptr(),
482                casc_path_cstr.as_ptr(),
483                &mut __v,
484            )
485        };
486        (__has != 0).then_some(__v)
487    }
488
489    /// @overload
490    pub fn file_size_file_id_hint(&self, file_id: i32, hint: FileIdHint) -> Option<u64> {
491        let mut __v: u64 = 0;
492        // SAFETY: `__v` is a live local, written by the
493        // native side only when it returns 1.
494        let __has = unsafe {
495            ffi::whiteout_casc_CascStorage_fileSize_fileId_hint(
496                self.raw.as_ptr(),
497                file_id,
498                hint as i32,
499                &mut __v,
500            )
501        };
502        (__has != 0).then_some(__v)
503    }
504
505    /// @return All known file paths.
506    pub fn list_files(&self) -> Vec<String> {
507        // SAFETY: index stays below the reported count.
508        unsafe {
509            let n = ffi::whiteout_casc_CascStorage_listFiles_count(self.raw.as_ptr());
510            (0..n)
511                .map(|i| {
512                    crate::support::take_string(ffi::whiteout_casc_CascStorage_listFiles_at(
513                        self.raw.as_ptr(),
514                        i,
515                    ))
516                })
517                .collect()
518        }
519    }
520
521    /// @return Total number of files in the root manifest.
522    pub fn total_file_count(&self) -> Option<u32> {
523        let mut __v: u32 = 0;
524        // SAFETY: `__v` is a live local, written by the
525        // native side only when it returns 1.
526        let __has =
527            unsafe { ffi::whiteout_casc_CascStorage_totalFileCount(self.raw.as_ptr(), &mut __v) };
528        (__has != 0).then_some(__v)
529    }
530
531    /// Import encryption keys from a formatted string (one per line).
532    pub fn import_keys_from_string(&mut self, key_list: &str) -> bool {
533        let key_list_cstr = std::ffi::CString::new(key_list).unwrap_or_default();
534        // SAFETY: handle is live for the duration of the call.
535        unsafe {
536            ffi::whiteout_casc_CascStorage_importKeysFromString(
537                self.raw.as_ptr(),
538                key_list_cstr.as_ptr(),
539            ) != 0
540        }
541    }
542
543    /// Import encryption keys from a file.
544    pub fn import_keys_from_file(&mut self, key_file_path: &str) -> bool {
545        let key_file_path_cstr = std::ffi::CString::new(key_file_path).unwrap_or_default();
546        // SAFETY: handle is live for the duration of the call.
547        unsafe {
548            ffi::whiteout_casc_CascStorage_importKeysFromFile(
549                self.raw.as_ptr(),
550                key_file_path_cstr.as_ptr(),
551            ) != 0
552        }
553    }
554
555    /// @return The encryption key for @p keyName, or std::nullopt if not found.
556    pub fn find_encryption_key(&self, key_name: u64) -> Option<[u8; 16]> {
557        let mut __v: [u8; 16] = Default::default();
558        // SAFETY: `__v` is a live local of exactly the
559        // length the native side writes.
560        let __has = unsafe {
561            ffi::whiteout_casc_CascStorage_findEncryptionKey(
562                self.raw.as_ptr(),
563                key_name,
564                __v.as_mut_ptr(),
565            )
566        };
567        (__has != 0).then_some(__v)
568    }
569
570    /// Clear the in-memory decoded-data cache (container cache).
571    pub fn flush_cache(&mut self) {
572        // SAFETY: handle is live for the duration of the call.
573        unsafe {
574            ffi::whiteout_casc_CascStorage_flushCache(self.raw.as_ptr());
575        }
576    }
577
578    /// Force every deferred load (encoding, root, VFS, index files, orphan bitvector) to resolve. Idempotent.
579    pub fn prefetch(&mut self) -> bool {
580        // SAFETY: handle is live for the duration of the call.
581        unsafe { ffi::whiteout_casc_CascStorage_prefetch(self.raw.as_ptr()) != 0 }
582    }
583
584    /// @return Last error code (thread-local).
585    pub fn last_error() -> u32 {
586        // SAFETY: handle is live for the duration of the call.
587        unsafe { ffi::whiteout_casc_CascStorage_lastError() }
588    }
589}
590
591/// Writable CASC storage (read + write + save)
592///
593/// Inherits all read operations from Storage. Adds write overlay and persist-to-disk support.
594///
595/// Only local-backed storages can be writable (CDN is read-only).
596///
597/// extends=whiteout::storages::casc::Storage
598pub struct StorageWritable {
599    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_CascStorageWritable>,
600}
601
602impl Drop for StorageWritable {
603    fn drop(&mut self) {
604        // SAFETY: `raw` came from a native constructor and Drop runs once.
605        unsafe { ffi::whiteout_casc_CascStorageWritable_delete(self.raw.as_ptr()) }
606    }
607}
608
609impl StorageWritable {
610    /// # Safety
611    /// `raw` must be a live handle this value takes ownership of.
612    #[allow(dead_code)] // used by whichever methods return this type
613    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_CascStorageWritable) -> Option<Self> {
614        core::ptr::NonNull::new(raw).map(|raw| StorageWritable { raw })
615    }
616}
617
618// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
619// is deliberately NOT implemented — the C++ types make no documented
620// guarantee about concurrent use, and claiming one we haven't verified
621// would be unsound. See `@bind thread_safe` in the plan.
622unsafe impl Send for StorageWritable {}
623
624impl core::fmt::Debug for StorageWritable {
625    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
626        f.debug_struct("StorageWritable").finish_non_exhaustive()
627    }
628}
629
630impl StorageWritable {
631    /// Create a new empty storage in memory.
632    ///
633    /// No file is written to disk until save() is called.
634    ///
635    /// @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.
636    pub fn create(
637        opts: &CreateOptions,
638        pool: Option<&crate::interfaces::HostWorkerPool>,
639    ) -> Option<StorageWritable> {
640        // SAFETY: handle is live for the duration of the call.
641        unsafe {
642            StorageWritable::from_raw(ffi::whiteout_casc_CascStorageWritable_create(
643                opts.raw.as_ptr(),
644                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
645            ))
646        }
647    }
648
649    /// Reserve a file-data-ID for a named asset.
650    ///
651    /// Allocates the next available file-data-ID and associates it with @p name.  The interpretation of @p name depends on the root format:
652    ///
653    /// - **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.
654    ///
655    /// Returns @c std::nullopt if the name already exists in the root or in a previous reservation.
656    ///
657    /// @code auto id = storage.reserveFileId("my_beast.app"); if (id) storage.writeFile(*id, data); @endcode
658    pub fn reserve_file_id(&mut self, name: &str) -> Option<u32> {
659        let name_cstr = std::ffi::CString::new(name).unwrap_or_default();
660        let mut __v: u32 = 0;
661        // SAFETY: `__v` is a live local, written by the
662        // native side only when it returns 1.
663        let __has = unsafe {
664            ffi::whiteout_casc_CascStorageWritable_reserveFileId(
665                self.raw.as_ptr(),
666                name_cstr.as_ptr(),
667                &mut __v,
668            )
669        };
670        (__has != 0).then_some(__v)
671    }
672
673    /// Write a file by path.
674    ///
675    /// Data is stored in an in-memory overlay until save() is called.
676    ///
677    /// @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.
678    pub fn write_file(&mut self, path: &str, data: &[u8], opts: &WriteOptions) -> bool {
679        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
680        let opts_native = unsafe { opts.to_native() };
681        // SAFETY: handle is live for the call; the staged
682        // option handles are freed immediately after.
683        unsafe {
684            let __r = ffi::whiteout_casc_CascStorageWritable_writeFile(
685                self.raw.as_ptr(),
686                path_cstr.as_ptr(),
687                data.as_ptr(),
688                data.len(),
689                opts_native,
690            ) != 0;
691            WriteOptions::free_native(opts_native);
692            __r
693        }
694    }
695
696    /// @overload Write a file by FileDataId.
697    pub fn write_file_file_id_data_opts_hint(
698        &mut self,
699        file_id: i32,
700        data: &[u8],
701        opts: &WriteOptions,
702        hint: FileIdHint,
703    ) -> bool {
704        let opts_native = unsafe { opts.to_native() };
705        // SAFETY: handle is live for the call; the staged
706        // option handles are freed immediately after.
707        unsafe {
708            let __r = ffi::whiteout_casc_CascStorageWritable_writeFile_fileId_data_opts_hint(
709                self.raw.as_ptr(),
710                file_id,
711                data.as_ptr(),
712                data.len(),
713                opts_native,
714                hint as i32,
715            ) != 0;
716            WriteOptions::free_native(opts_native);
717            __r
718        }
719    }
720
721    /// Mark a file for deletion (effective on next save).
722    pub fn delete_file(&mut self, path: &str) -> bool {
723        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
724        // SAFETY: handle is live for the duration of the call.
725        unsafe {
726            ffi::whiteout_casc_CascStorageWritable_deleteFile(self.raw.as_ptr(), path_cstr.as_ptr())
727                != 0
728        }
729    }
730
731    /// @overload
732    pub fn delete_file_file_id_hint(&mut self, file_id: i32, hint: FileIdHint) -> bool {
733        // SAFETY: handle is live for the duration of the call.
734        unsafe {
735            ffi::whiteout_casc_CascStorageWritable_deleteFile_fileId_hint(
736                self.raw.as_ptr(),
737                file_id,
738                hint as i32,
739            ) != 0
740        }
741    }
742
743    /// Persist all pending changes to disk (writes to the original location).
744    pub fn save(&mut self) -> bool {
745        // SAFETY: handle is live for the duration of the call.
746        unsafe { ffi::whiteout_casc_CascStorageWritable_save(self.raw.as_ptr()) != 0 }
747    }
748
749    /// @overload Persist to a specific output path.
750    pub fn save_path(&mut self, path: &str) -> bool {
751        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
752        // SAFETY: handle is live for the duration of the call.
753        unsafe {
754            ffi::whiteout_casc_CascStorageWritable_save_path(self.raw.as_ptr(), path_cstr.as_ptr())
755                != 0
756        }
757    }
758}
759
760// Not yet bound (shape unsupported by the emitter):
761//   - Storage::open_opts (parameter shape)
762
763#[doc(hidden)]
764pub mod ffi {
765    #![allow(missing_debug_implementations)]
766
767    #[allow(unused_imports)]
768    use crate::support::{RawBytes, RawCString};
769
770    #[repr(C)]
771    pub struct whiteout_CascCreateOptions {
772        _private: [u8; 0],
773    }
774    #[repr(C)]
775    pub struct whiteout_CascWriteOptions {
776        _private: [u8; 0],
777    }
778    #[repr(C)]
779    pub struct whiteout_CascStorage {
780        _private: [u8; 0],
781    }
782    #[repr(C)]
783    pub struct whiteout_CascStorageWritable {
784        _private: [u8; 0],
785    }
786
787    extern "C" {
788        // CreateOptions
789        pub fn whiteout_casc_CascCreateOptions_new() -> *mut whiteout_CascCreateOptions;
790        pub fn whiteout_casc_CascCreateOptions_delete(self_: *mut whiteout_CascCreateOptions);
791        pub fn whiteout_casc_CascCreateOptions_get_product(
792            self_: *mut whiteout_CascCreateOptions,
793        ) -> RawCString;
794        pub fn whiteout_casc_CascCreateOptions_set_product(
795            self_: *mut whiteout_CascCreateOptions,
796            value: *const core::ffi::c_char,
797        );
798        pub fn whiteout_casc_CascCreateOptions_get_version(
799            self_: *mut whiteout_CascCreateOptions,
800        ) -> RawCString;
801        pub fn whiteout_casc_CascCreateOptions_set_version(
802            self_: *mut whiteout_CascCreateOptions,
803            value: *const core::ffi::c_char,
804        );
805        pub fn whiteout_casc_CascCreateOptions_get_archiveMaxSize(
806            self_: *mut whiteout_CascCreateOptions,
807        ) -> u32;
808        pub fn whiteout_casc_CascCreateOptions_set_archiveMaxSize(
809            self_: *mut whiteout_CascCreateOptions,
810            value: u32,
811        );
812        pub fn whiteout_casc_CascCreateOptions_get_blteFrameSize(
813            self_: *mut whiteout_CascCreateOptions,
814        ) -> u32;
815        pub fn whiteout_casc_CascCreateOptions_set_blteFrameSize(
816            self_: *mut whiteout_CascCreateOptions,
817            value: u32,
818        );
819        pub fn whiteout_casc_CascCreateOptions_get_rootFormat(
820            self_: *mut whiteout_CascCreateOptions,
821        ) -> i32;
822        pub fn whiteout_casc_CascCreateOptions_set_rootFormat(
823            self_: *mut whiteout_CascCreateOptions,
824            value: i32,
825        );
826        // WriteOptions
827        pub fn whiteout_casc_CascWriteOptions_new() -> *mut whiteout_CascWriteOptions;
828        pub fn whiteout_casc_CascWriteOptions_delete(self_: *mut whiteout_CascWriteOptions);
829        pub fn whiteout_casc_CascWriteOptions_get_localeFlags(
830            self_: *mut whiteout_CascWriteOptions,
831        ) -> u32;
832        pub fn whiteout_casc_CascWriteOptions_set_localeFlags(
833            self_: *mut whiteout_CascWriteOptions,
834            value: u32,
835        );
836        pub fn whiteout_casc_CascWriteOptions_get_contentFlags(
837            self_: *mut whiteout_CascWriteOptions,
838        ) -> u32;
839        pub fn whiteout_casc_CascWriteOptions_set_contentFlags(
840            self_: *mut whiteout_CascWriteOptions,
841            value: u32,
842        );
843        pub fn whiteout_casc_CascWriteOptions_get_compress(
844            self_: *mut whiteout_CascWriteOptions,
845        ) -> i32;
846        pub fn whiteout_casc_CascWriteOptions_set_compress(
847            self_: *mut whiteout_CascWriteOptions,
848            value: i32,
849        );
850        // Storage
851        pub fn whiteout_casc_CascStorage_delete(self_: *mut whiteout_CascStorage);
852        pub fn whiteout_casc_CascStorage_open(
853            path: *const core::ffi::c_char,
854            pool: *mut core::ffi::c_void,
855        ) -> *mut whiteout_CascStorage;
856        pub fn whiteout_casc_CascStorage_open_path_localeMask_pool(
857            path: *const core::ffi::c_char,
858            locale_mask: u32,
859            pool: *mut core::ffi::c_void,
860        ) -> *mut whiteout_CascStorage;
861        pub fn whiteout_casc_CascStorage_open_path_product_pool(
862            path: *const core::ffi::c_char,
863            product: *const core::ffi::c_char,
864            pool: *mut core::ffi::c_void,
865        ) -> *mut whiteout_CascStorage;
866        pub fn whiteout_casc_CascStorage_close(self_: *mut whiteout_CascStorage);
867        pub fn whiteout_casc_CascStorage_isLocal(self_: *mut whiteout_CascStorage) -> i32;
868        pub fn whiteout_casc_CascStorage_isOnline(self_: *mut whiteout_CascStorage) -> i32;
869        pub fn whiteout_casc_CascStorage_isWritable(self_: *mut whiteout_CascStorage) -> i32;
870        pub fn whiteout_casc_CascStorage_rootFormat(self_: *mut whiteout_CascStorage) -> i32;
871        pub fn whiteout_casc_CascStorage_readFile(
872            self_: *mut whiteout_CascStorage,
873            casc_path: *const core::ffi::c_char,
874        ) -> RawBytes;
875        pub fn whiteout_casc_CascStorage_readFile_cascPath_localeFlags_openFlags(
876            self_: *mut whiteout_CascStorage,
877            casc_path: *const core::ffi::c_char,
878            locale_flags: u32,
879            open_flags: u32,
880        ) -> RawBytes;
881        pub fn whiteout_casc_CascStorage_readFile_fileId_hint(
882            self_: *mut whiteout_CascStorage,
883            file_id: i32,
884            hint: i32,
885        ) -> RawBytes;
886        pub fn whiteout_casc_CascStorage_readFile_fileId_localeFlags_openFlags_hint(
887            self_: *mut whiteout_CascStorage,
888            file_id: i32,
889            locale_flags: u32,
890            open_flags: u32,
891            hint: i32,
892        ) -> RawBytes;
893        pub fn whiteout_casc_CascStorage_fileExists(
894            self_: *mut whiteout_CascStorage,
895            casc_path: *const core::ffi::c_char,
896        ) -> i32;
897        pub fn whiteout_casc_CascStorage_fileExists_fileId_hint(
898            self_: *mut whiteout_CascStorage,
899            file_id: i32,
900            hint: i32,
901        ) -> i32;
902        pub fn whiteout_casc_CascStorage_fileSize(
903            self_: *mut whiteout_CascStorage,
904            casc_path: *const core::ffi::c_char,
905            out_value: *mut u64,
906        ) -> i32;
907        pub fn whiteout_casc_CascStorage_fileSize_fileId_hint(
908            self_: *mut whiteout_CascStorage,
909            file_id: i32,
910            hint: i32,
911            out_value: *mut u64,
912        ) -> i32;
913        pub fn whiteout_casc_CascStorage_listFiles_count(self_: *mut whiteout_CascStorage)
914            -> usize;
915        pub fn whiteout_casc_CascStorage_listFiles_at(
916            self_: *mut whiteout_CascStorage,
917            index: usize,
918        ) -> RawCString;
919        pub fn whiteout_casc_CascStorage_totalFileCount(
920            self_: *mut whiteout_CascStorage,
921            out_value: *mut u32,
922        ) -> i32;
923        pub fn whiteout_casc_CascStorage_importKeysFromString(
924            self_: *mut whiteout_CascStorage,
925            key_list: *const core::ffi::c_char,
926        ) -> i32;
927        pub fn whiteout_casc_CascStorage_importKeysFromFile(
928            self_: *mut whiteout_CascStorage,
929            key_file_path: *const core::ffi::c_char,
930        ) -> i32;
931        pub fn whiteout_casc_CascStorage_findEncryptionKey(
932            self_: *mut whiteout_CascStorage,
933            key_name: u64,
934            out_value: *mut u8,
935        ) -> i32;
936        pub fn whiteout_casc_CascStorage_flushCache(self_: *mut whiteout_CascStorage);
937        pub fn whiteout_casc_CascStorage_prefetch(self_: *mut whiteout_CascStorage) -> i32;
938        pub fn whiteout_casc_CascStorage_lastError() -> u32;
939        // StorageWritable
940        pub fn whiteout_casc_CascStorageWritable_delete(self_: *mut whiteout_CascStorageWritable);
941        pub fn whiteout_casc_CascStorageWritable_create(
942            opts: *mut whiteout_CascCreateOptions,
943            pool: *mut core::ffi::c_void,
944        ) -> *mut whiteout_CascStorageWritable;
945        pub fn whiteout_casc_CascStorageWritable_reserveFileId(
946            self_: *mut whiteout_CascStorageWritable,
947            name: *const core::ffi::c_char,
948            out_value: *mut u32,
949        ) -> i32;
950        pub fn whiteout_casc_CascStorageWritable_writeFile(
951            self_: *mut whiteout_CascStorageWritable,
952            path: *const core::ffi::c_char,
953            data: *const u8,
954            data_size: usize,
955            opts: *mut whiteout_CascWriteOptions,
956        ) -> i32;
957        pub fn whiteout_casc_CascStorageWritable_writeFile_fileId_data_opts_hint(
958            self_: *mut whiteout_CascStorageWritable,
959            file_id: i32,
960            data: *const u8,
961            data_size: usize,
962            opts: *mut whiteout_CascWriteOptions,
963            hint: i32,
964        ) -> i32;
965        pub fn whiteout_casc_CascStorageWritable_deleteFile(
966            self_: *mut whiteout_CascStorageWritable,
967            path: *const core::ffi::c_char,
968        ) -> i32;
969        pub fn whiteout_casc_CascStorageWritable_deleteFile_fileId_hint(
970            self_: *mut whiteout_CascStorageWritable,
971            file_id: i32,
972            hint: i32,
973        ) -> i32;
974        pub fn whiteout_casc_CascStorageWritable_save(
975            self_: *mut whiteout_CascStorageWritable,
976        ) -> i32;
977        pub fn whiteout_casc_CascStorageWritable_save_path(
978            self_: *mut whiteout_CascStorageWritable,
979            path: *const core::ffi::c_char,
980        ) -> i32;
981    }
982}