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: one call materialises the list; the
508        // elements are borrowed out of it and it is freed
509        // before returning. Reading is O(1) per element.
510        unsafe {
511            let list = ffi::whiteout_casc_CascStorage_listFiles(self.raw.as_ptr());
512            if list.is_null() {
513                return Vec::new();
514            }
515            let n = ffi::whiteout_casc_StringList_size(list);
516            let out = (0..n)
517                .map(|i| crate::support::take_string(ffi::whiteout_casc_StringList_at(list, i)))
518                .collect();
519            ffi::whiteout_casc_StringList_delete(list);
520            out
521        }
522    }
523
524    /// @return Total number of files in the root manifest.
525    pub fn total_file_count(&self) -> Option<u32> {
526        let mut __v: u32 = 0;
527        // SAFETY: `__v` is a live local, written by the
528        // native side only when it returns 1.
529        let __has =
530            unsafe { ffi::whiteout_casc_CascStorage_totalFileCount(self.raw.as_ptr(), &mut __v) };
531        (__has != 0).then_some(__v)
532    }
533
534    /// Import encryption keys from a formatted string (one per line).
535    pub fn import_keys_from_string(&mut self, key_list: &str) -> bool {
536        let key_list_cstr = std::ffi::CString::new(key_list).unwrap_or_default();
537        // SAFETY: handle is live for the duration of the call.
538        unsafe {
539            ffi::whiteout_casc_CascStorage_importKeysFromString(
540                self.raw.as_ptr(),
541                key_list_cstr.as_ptr(),
542            ) != 0
543        }
544    }
545
546    /// Import encryption keys from a file.
547    pub fn import_keys_from_file(&mut self, key_file_path: &str) -> bool {
548        let key_file_path_cstr = std::ffi::CString::new(key_file_path).unwrap_or_default();
549        // SAFETY: handle is live for the duration of the call.
550        unsafe {
551            ffi::whiteout_casc_CascStorage_importKeysFromFile(
552                self.raw.as_ptr(),
553                key_file_path_cstr.as_ptr(),
554            ) != 0
555        }
556    }
557
558    /// @return The encryption key for @p keyName, or std::nullopt if not found.
559    pub fn find_encryption_key(&self, key_name: u64) -> Option<[u8; 16]> {
560        let mut __v: [u8; 16] = Default::default();
561        // SAFETY: `__v` is a live local of exactly the
562        // length the native side writes.
563        let __has = unsafe {
564            ffi::whiteout_casc_CascStorage_findEncryptionKey(
565                self.raw.as_ptr(),
566                key_name,
567                __v.as_mut_ptr(),
568            )
569        };
570        (__has != 0).then_some(__v)
571    }
572
573    /// Clear the in-memory decoded-data cache (container cache).
574    pub fn flush_cache(&mut self) {
575        // SAFETY: handle is live for the duration of the call.
576        unsafe {
577            ffi::whiteout_casc_CascStorage_flushCache(self.raw.as_ptr());
578        }
579    }
580
581    /// Force every deferred load (encoding, root, VFS, index files, orphan bitvector) to resolve. Idempotent.
582    pub fn prefetch(&mut self) -> bool {
583        // SAFETY: handle is live for the duration of the call.
584        unsafe { ffi::whiteout_casc_CascStorage_prefetch(self.raw.as_ptr()) != 0 }
585    }
586
587    /// @return Last error code (thread-local).
588    pub fn last_error() -> u32 {
589        // SAFETY: handle is live for the duration of the call.
590        unsafe { ffi::whiteout_casc_CascStorage_lastError() }
591    }
592}
593
594/// Writable CASC storage (read + write + save)
595///
596/// Inherits all read operations from Storage. Adds write overlay and persist-to-disk support.
597///
598/// Only local-backed storages can be writable (CDN is read-only).
599///
600/// extends=whiteout::storages::casc::Storage
601pub struct StorageWritable {
602    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_CascStorageWritable>,
603}
604
605impl Drop for StorageWritable {
606    fn drop(&mut self) {
607        // SAFETY: `raw` came from a native constructor and Drop runs once.
608        unsafe { ffi::whiteout_casc_CascStorageWritable_delete(self.raw.as_ptr()) }
609    }
610}
611
612impl StorageWritable {
613    /// # Safety
614    /// `raw` must be a live handle this value takes ownership of.
615    #[allow(dead_code)] // used by whichever methods return this type
616    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_CascStorageWritable) -> Option<Self> {
617        core::ptr::NonNull::new(raw).map(|raw| StorageWritable { raw })
618    }
619}
620
621// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
622// is deliberately NOT implemented — the C++ types make no documented
623// guarantee about concurrent use, and claiming one we haven't verified
624// would be unsound. See `@bind thread_safe` in the plan.
625unsafe impl Send for StorageWritable {}
626
627impl core::fmt::Debug for StorageWritable {
628    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
629        f.debug_struct("StorageWritable").finish_non_exhaustive()
630    }
631}
632
633impl StorageWritable {
634    /// Create a new empty storage in memory.
635    ///
636    /// No file is written to disk until save() is called.
637    ///
638    /// @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.
639    pub fn create(
640        opts: &CreateOptions,
641        pool: Option<&crate::interfaces::HostWorkerPool>,
642    ) -> Option<StorageWritable> {
643        // SAFETY: handle is live for the duration of the call.
644        unsafe {
645            StorageWritable::from_raw(ffi::whiteout_casc_CascStorageWritable_create(
646                opts.raw.as_ptr(),
647                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
648            ))
649        }
650    }
651
652    /// Reserve a file-data-ID for a named asset.
653    ///
654    /// Allocates the next available file-data-ID and associates it with @p name.  The interpretation of @p name depends on the root format:
655    ///
656    /// - **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.
657    ///
658    /// Returns @c std::nullopt if the name already exists in the root or in a previous reservation.
659    ///
660    /// @code auto id = storage.reserveFileId("my_beast.app"); if (id) storage.writeFile(*id, data); @endcode
661    pub fn reserve_file_id(&mut self, name: &str) -> Option<u32> {
662        let name_cstr = std::ffi::CString::new(name).unwrap_or_default();
663        let mut __v: u32 = 0;
664        // SAFETY: `__v` is a live local, written by the
665        // native side only when it returns 1.
666        let __has = unsafe {
667            ffi::whiteout_casc_CascStorageWritable_reserveFileId(
668                self.raw.as_ptr(),
669                name_cstr.as_ptr(),
670                &mut __v,
671            )
672        };
673        (__has != 0).then_some(__v)
674    }
675
676    /// Write a file by path.
677    ///
678    /// Data is stored in an in-memory overlay until save() is called.
679    ///
680    /// @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.
681    pub fn write_file(&mut self, path: &str, data: &[u8], opts: &WriteOptions) -> bool {
682        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
683        let opts_native = unsafe { opts.to_native() };
684        // SAFETY: handle is live for the call; the staged
685        // option handles are freed immediately after.
686        unsafe {
687            let __r = ffi::whiteout_casc_CascStorageWritable_writeFile(
688                self.raw.as_ptr(),
689                path_cstr.as_ptr(),
690                data.as_ptr(),
691                data.len(),
692                opts_native,
693            ) != 0;
694            WriteOptions::free_native(opts_native);
695            __r
696        }
697    }
698
699    /// @overload Write a file by FileDataId.
700    pub fn write_file_file_id_data_opts_hint(
701        &mut self,
702        file_id: i32,
703        data: &[u8],
704        opts: &WriteOptions,
705        hint: FileIdHint,
706    ) -> bool {
707        let opts_native = unsafe { opts.to_native() };
708        // SAFETY: handle is live for the call; the staged
709        // option handles are freed immediately after.
710        unsafe {
711            let __r = ffi::whiteout_casc_CascStorageWritable_writeFile_fileId_data_opts_hint(
712                self.raw.as_ptr(),
713                file_id,
714                data.as_ptr(),
715                data.len(),
716                opts_native,
717                hint as i32,
718            ) != 0;
719            WriteOptions::free_native(opts_native);
720            __r
721        }
722    }
723
724    /// Mark a file for deletion (effective on next save).
725    pub fn delete_file(&mut self, path: &str) -> bool {
726        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
727        // SAFETY: handle is live for the duration of the call.
728        unsafe {
729            ffi::whiteout_casc_CascStorageWritable_deleteFile(self.raw.as_ptr(), path_cstr.as_ptr())
730                != 0
731        }
732    }
733
734    /// @overload
735    pub fn delete_file_file_id_hint(&mut self, file_id: i32, hint: FileIdHint) -> bool {
736        // SAFETY: handle is live for the duration of the call.
737        unsafe {
738            ffi::whiteout_casc_CascStorageWritable_deleteFile_fileId_hint(
739                self.raw.as_ptr(),
740                file_id,
741                hint as i32,
742            ) != 0
743        }
744    }
745
746    /// Persist all pending changes to disk (writes to the original location).
747    pub fn save(&mut self) -> bool {
748        // SAFETY: handle is live for the duration of the call.
749        unsafe { ffi::whiteout_casc_CascStorageWritable_save(self.raw.as_ptr()) != 0 }
750    }
751
752    /// @overload Persist to a specific output path.
753    pub fn save_path(&mut self, path: &str) -> bool {
754        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
755        // SAFETY: handle is live for the duration of the call.
756        unsafe {
757            ffi::whiteout_casc_CascStorageWritable_save_path(self.raw.as_ptr(), path_cstr.as_ptr())
758                != 0
759        }
760    }
761}
762
763// Not yet bound (shape unsupported by the emitter):
764//   - Storage::open_opts (parameter shape)
765
766#[doc(hidden)]
767pub mod ffi {
768    #![allow(missing_debug_implementations)]
769
770    #[allow(unused_imports)]
771    use crate::support::{RawBytes, RawCString};
772
773    #[repr(C)]
774    pub struct whiteout_CascCreateOptions {
775        _private: [u8; 0],
776    }
777    #[repr(C)]
778    pub struct whiteout_CascWriteOptions {
779        _private: [u8; 0],
780    }
781    #[repr(C)]
782    pub struct whiteout_CascStorage {
783        _private: [u8; 0],
784    }
785    #[repr(C)]
786    pub struct whiteout_CascStorageWritable {
787        _private: [u8; 0],
788    }
789    #[repr(C)]
790    pub struct whiteout_StringList {
791        _private: [u8; 0],
792    }
793
794    extern "C" {
795        pub fn whiteout_casc_StringList_size(self_: *mut whiteout_StringList) -> usize;
796        pub fn whiteout_casc_StringList_at(
797            self_: *mut whiteout_StringList,
798            index: usize,
799        ) -> RawCString;
800        pub fn whiteout_casc_StringList_delete(self_: *mut whiteout_StringList);
801        // CreateOptions
802        pub fn whiteout_casc_CascCreateOptions_new() -> *mut whiteout_CascCreateOptions;
803        pub fn whiteout_casc_CascCreateOptions_delete(self_: *mut whiteout_CascCreateOptions);
804        pub fn whiteout_casc_CascCreateOptions_get_product(
805            self_: *mut whiteout_CascCreateOptions,
806        ) -> RawCString;
807        pub fn whiteout_casc_CascCreateOptions_set_product(
808            self_: *mut whiteout_CascCreateOptions,
809            value: *const core::ffi::c_char,
810        );
811        pub fn whiteout_casc_CascCreateOptions_get_version(
812            self_: *mut whiteout_CascCreateOptions,
813        ) -> RawCString;
814        pub fn whiteout_casc_CascCreateOptions_set_version(
815            self_: *mut whiteout_CascCreateOptions,
816            value: *const core::ffi::c_char,
817        );
818        pub fn whiteout_casc_CascCreateOptions_get_archiveMaxSize(
819            self_: *mut whiteout_CascCreateOptions,
820        ) -> u32;
821        pub fn whiteout_casc_CascCreateOptions_set_archiveMaxSize(
822            self_: *mut whiteout_CascCreateOptions,
823            value: u32,
824        );
825        pub fn whiteout_casc_CascCreateOptions_get_blteFrameSize(
826            self_: *mut whiteout_CascCreateOptions,
827        ) -> u32;
828        pub fn whiteout_casc_CascCreateOptions_set_blteFrameSize(
829            self_: *mut whiteout_CascCreateOptions,
830            value: u32,
831        );
832        pub fn whiteout_casc_CascCreateOptions_get_rootFormat(
833            self_: *mut whiteout_CascCreateOptions,
834        ) -> i32;
835        pub fn whiteout_casc_CascCreateOptions_set_rootFormat(
836            self_: *mut whiteout_CascCreateOptions,
837            value: i32,
838        );
839        // WriteOptions
840        pub fn whiteout_casc_CascWriteOptions_new() -> *mut whiteout_CascWriteOptions;
841        pub fn whiteout_casc_CascWriteOptions_delete(self_: *mut whiteout_CascWriteOptions);
842        pub fn whiteout_casc_CascWriteOptions_get_localeFlags(
843            self_: *mut whiteout_CascWriteOptions,
844        ) -> u32;
845        pub fn whiteout_casc_CascWriteOptions_set_localeFlags(
846            self_: *mut whiteout_CascWriteOptions,
847            value: u32,
848        );
849        pub fn whiteout_casc_CascWriteOptions_get_contentFlags(
850            self_: *mut whiteout_CascWriteOptions,
851        ) -> u32;
852        pub fn whiteout_casc_CascWriteOptions_set_contentFlags(
853            self_: *mut whiteout_CascWriteOptions,
854            value: u32,
855        );
856        pub fn whiteout_casc_CascWriteOptions_get_compress(
857            self_: *mut whiteout_CascWriteOptions,
858        ) -> i32;
859        pub fn whiteout_casc_CascWriteOptions_set_compress(
860            self_: *mut whiteout_CascWriteOptions,
861            value: i32,
862        );
863        // Storage
864        pub fn whiteout_casc_CascStorage_delete(self_: *mut whiteout_CascStorage);
865        pub fn whiteout_casc_CascStorage_open(
866            path: *const core::ffi::c_char,
867            pool: *mut core::ffi::c_void,
868        ) -> *mut whiteout_CascStorage;
869        pub fn whiteout_casc_CascStorage_open_path_localeMask_pool(
870            path: *const core::ffi::c_char,
871            locale_mask: u32,
872            pool: *mut core::ffi::c_void,
873        ) -> *mut whiteout_CascStorage;
874        pub fn whiteout_casc_CascStorage_open_path_product_pool(
875            path: *const core::ffi::c_char,
876            product: *const core::ffi::c_char,
877            pool: *mut core::ffi::c_void,
878        ) -> *mut whiteout_CascStorage;
879        pub fn whiteout_casc_CascStorage_close(self_: *mut whiteout_CascStorage);
880        pub fn whiteout_casc_CascStorage_isLocal(self_: *mut whiteout_CascStorage) -> i32;
881        pub fn whiteout_casc_CascStorage_isOnline(self_: *mut whiteout_CascStorage) -> i32;
882        pub fn whiteout_casc_CascStorage_isWritable(self_: *mut whiteout_CascStorage) -> i32;
883        pub fn whiteout_casc_CascStorage_rootFormat(self_: *mut whiteout_CascStorage) -> i32;
884        pub fn whiteout_casc_CascStorage_readFile(
885            self_: *mut whiteout_CascStorage,
886            casc_path: *const core::ffi::c_char,
887        ) -> RawBytes;
888        pub fn whiteout_casc_CascStorage_readFile_cascPath_localeFlags_openFlags(
889            self_: *mut whiteout_CascStorage,
890            casc_path: *const core::ffi::c_char,
891            locale_flags: u32,
892            open_flags: u32,
893        ) -> RawBytes;
894        pub fn whiteout_casc_CascStorage_readFile_fileId_hint(
895            self_: *mut whiteout_CascStorage,
896            file_id: i32,
897            hint: i32,
898        ) -> RawBytes;
899        pub fn whiteout_casc_CascStorage_readFile_fileId_localeFlags_openFlags_hint(
900            self_: *mut whiteout_CascStorage,
901            file_id: i32,
902            locale_flags: u32,
903            open_flags: u32,
904            hint: i32,
905        ) -> RawBytes;
906        pub fn whiteout_casc_CascStorage_fileExists(
907            self_: *mut whiteout_CascStorage,
908            casc_path: *const core::ffi::c_char,
909        ) -> i32;
910        pub fn whiteout_casc_CascStorage_fileExists_fileId_hint(
911            self_: *mut whiteout_CascStorage,
912            file_id: i32,
913            hint: i32,
914        ) -> i32;
915        pub fn whiteout_casc_CascStorage_fileSize(
916            self_: *mut whiteout_CascStorage,
917            casc_path: *const core::ffi::c_char,
918            out_value: *mut u64,
919        ) -> i32;
920        pub fn whiteout_casc_CascStorage_fileSize_fileId_hint(
921            self_: *mut whiteout_CascStorage,
922            file_id: i32,
923            hint: i32,
924            out_value: *mut u64,
925        ) -> i32;
926        pub fn whiteout_casc_CascStorage_listFiles(
927            self_: *mut whiteout_CascStorage,
928        ) -> *mut whiteout_StringList;
929        pub fn whiteout_casc_CascStorage_totalFileCount(
930            self_: *mut whiteout_CascStorage,
931            out_value: *mut u32,
932        ) -> i32;
933        pub fn whiteout_casc_CascStorage_importKeysFromString(
934            self_: *mut whiteout_CascStorage,
935            key_list: *const core::ffi::c_char,
936        ) -> i32;
937        pub fn whiteout_casc_CascStorage_importKeysFromFile(
938            self_: *mut whiteout_CascStorage,
939            key_file_path: *const core::ffi::c_char,
940        ) -> i32;
941        pub fn whiteout_casc_CascStorage_findEncryptionKey(
942            self_: *mut whiteout_CascStorage,
943            key_name: u64,
944            out_value: *mut u8,
945        ) -> i32;
946        pub fn whiteout_casc_CascStorage_flushCache(self_: *mut whiteout_CascStorage);
947        pub fn whiteout_casc_CascStorage_prefetch(self_: *mut whiteout_CascStorage) -> i32;
948        pub fn whiteout_casc_CascStorage_lastError() -> u32;
949        // StorageWritable
950        pub fn whiteout_casc_CascStorageWritable_delete(self_: *mut whiteout_CascStorageWritable);
951        pub fn whiteout_casc_CascStorageWritable_create(
952            opts: *mut whiteout_CascCreateOptions,
953            pool: *mut core::ffi::c_void,
954        ) -> *mut whiteout_CascStorageWritable;
955        pub fn whiteout_casc_CascStorageWritable_reserveFileId(
956            self_: *mut whiteout_CascStorageWritable,
957            name: *const core::ffi::c_char,
958            out_value: *mut u32,
959        ) -> i32;
960        pub fn whiteout_casc_CascStorageWritable_writeFile(
961            self_: *mut whiteout_CascStorageWritable,
962            path: *const core::ffi::c_char,
963            data: *const u8,
964            data_size: usize,
965            opts: *mut whiteout_CascWriteOptions,
966        ) -> i32;
967        pub fn whiteout_casc_CascStorageWritable_writeFile_fileId_data_opts_hint(
968            self_: *mut whiteout_CascStorageWritable,
969            file_id: i32,
970            data: *const u8,
971            data_size: usize,
972            opts: *mut whiteout_CascWriteOptions,
973            hint: i32,
974        ) -> i32;
975        pub fn whiteout_casc_CascStorageWritable_deleteFile(
976            self_: *mut whiteout_CascStorageWritable,
977            path: *const core::ffi::c_char,
978        ) -> i32;
979        pub fn whiteout_casc_CascStorageWritable_deleteFile_fileId_hint(
980            self_: *mut whiteout_CascStorageWritable,
981            file_id: i32,
982            hint: i32,
983        ) -> i32;
984        pub fn whiteout_casc_CascStorageWritable_save(
985            self_: *mut whiteout_CascStorageWritable,
986        ) -> i32;
987        pub fn whiteout_casc_CascStorageWritable_save_path(
988            self_: *mut whiteout_CascStorageWritable,
989            path: *const core::ffi::c_char,
990        ) -> i32;
991    }
992}