Skip to main content

whiteout/
mpq.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 mpq --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/// MPQ format version.
14#[repr(i32)]
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub enum FormatVersion {
17    /// Original format (up to 4 GB archives).
18    V1 = 0,
19    /// Extended format (>4 GB archives, hi-block table).
20    V2 = 1,
21}
22
23impl TryFrom<i32> for FormatVersion {
24    type Error = crate::Error;
25    fn try_from(v: i32) -> Result<Self, crate::Error> {
26        match v {
27            0 => Ok(FormatVersion::V1),
28            1 => Ok(FormatVersion::V2),
29            other => Err(crate::Error::UnknownEnum {
30                name: "FormatVersion",
31                value: other,
32            }),
33        }
34    }
35}
36
37/// Compression algorithm for writing files.
38#[repr(i32)]
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub enum Compression {
41    /// No compression; data stored verbatim.
42    None = 0,
43    /// Huffman coding (used for audio in older Blizzard games).
44    Huffman = 1,
45    /// zlib / DEFLATE compression (most common MPQ codec).
46    Zlib = 2,
47    /// PKware DCL (implode) compression.
48    PKware = 8,
49    /// bzip2 compression.
50    BZip2 = 16,
51    /// Sparse / RLE compression.
52    Sparse = 32,
53    /// IMA ADPCM mono (used for mono audio).
54    AdpcmMono = 64,
55    /// IMA ADPCM stereo (used for stereo audio).
56    AdpcmStereo = -128,
57}
58
59impl TryFrom<i32> for Compression {
60    type Error = crate::Error;
61    fn try_from(v: i32) -> Result<Self, crate::Error> {
62        match v {
63            0 => Ok(Compression::None),
64            1 => Ok(Compression::Huffman),
65            2 => Ok(Compression::Zlib),
66            8 => Ok(Compression::PKware),
67            16 => Ok(Compression::BZip2),
68            32 => Ok(Compression::Sparse),
69            64 => Ok(Compression::AdpcmMono),
70            -128 => Ok(Compression::AdpcmStereo),
71            other => Err(crate::Error::UnknownEnum {
72                name: "Compression",
73                value: other,
74            }),
75        }
76    }
77}
78
79/// Bit flags. Combine with `|`, test with [`FileFlags::contains`].
80#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
81pub struct FileFlags(pub i32);
82
83impl FileFlags {
84    pub const NONE: Self = Self(0);
85    /// File uses sector compression.
86    pub const COMPRESSED: Self = Self(512);
87    /// File data is encrypted.
88    pub const ENCRYPTED: Self = Self(65536);
89    /// File stored as a single unit (no sector splitting).
90    pub const SINGLE_UNIT: Self = Self(16777216);
91    /// Slot is occupied by a real file.
92    pub const EXISTS: Self = Self(-2147483648);
93
94    #[inline]
95    pub const fn contains(self, other: Self) -> bool {
96        (self.0 & other.0) == other.0
97    }
98
99    #[inline]
100    pub const fn is_empty(self) -> bool {
101        self.0 == 0
102    }
103}
104
105impl core::ops::BitOr for FileFlags {
106    type Output = Self;
107    #[inline]
108    fn bitor(self, rhs: Self) -> Self {
109        Self(self.0 | rhs.0)
110    }
111}
112
113impl core::ops::BitAnd for FileFlags {
114    type Output = Self;
115    #[inline]
116    fn bitand(self, rhs: Self) -> Self {
117        Self(self.0 & rhs.0)
118    }
119}
120
121impl core::ops::Not for FileFlags {
122    type Output = Self;
123    #[inline]
124    fn not(self) -> Self {
125        Self(!self.0)
126    }
127}
128
129impl core::fmt::Debug for FileFlags {
130    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
131        write!(f, "FileFlags({:#x})", self.0)
132    }
133}
134
135/// Information about a single file in the archive.
136pub struct FileInfo {
137    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_MpqFileInfo>,
138}
139
140impl Drop for FileInfo {
141    fn drop(&mut self) {
142        // SAFETY: `raw` came from a native constructor and Drop runs once.
143        unsafe { ffi::whiteout_mpq_MpqFileInfo_delete(self.raw.as_ptr()) }
144    }
145}
146
147impl FileInfo {
148    /// # Safety
149    /// `raw` must be a live handle this value takes ownership of.
150    #[allow(dead_code)] // used by whichever methods return this type
151    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_MpqFileInfo) -> Option<Self> {
152        core::ptr::NonNull::new(raw).map(|raw| FileInfo { raw })
153    }
154}
155
156// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
157// is deliberately NOT implemented — the C++ types make no documented
158// guarantee about concurrent use, and claiming one we haven't verified
159// would be unsound. See `@bind thread_safe` in the plan.
160unsafe impl Send for FileInfo {}
161
162impl core::fmt::Debug for FileInfo {
163    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
164        f.debug_struct("FileInfo").finish_non_exhaustive()
165    }
166}
167
168impl FileInfo {
169    /// # Panics
170    /// Panics if the native allocation fails.
171    pub fn new() -> Self {
172        // SAFETY: the native constructor returns a live handle; a null here
173        // means the library is unusable.
174        unsafe {
175            let raw = ffi::whiteout_mpq_MpqFileInfo_new();
176            Self::from_raw(raw).expect("native FileInfo allocation failed")
177        }
178    }
179
180    /// Filename (from listfile or hash table lookup).
181    pub fn name(&self) -> String {
182        // SAFETY: the native side hands over an owned CString.
183        unsafe {
184            crate::support::take_string(ffi::whiteout_mpq_MpqFileInfo_get_name(self.raw.as_ptr()))
185        }
186    }
187
188    pub fn set_name(&mut self, value: &str) {
189        let value = std::ffi::CString::new(value).unwrap_or_default();
190        // SAFETY: the pointer outlives the call.
191        unsafe { ffi::whiteout_mpq_MpqFileInfo_set_name(self.raw.as_ptr(), value.as_ptr()) }
192    }
193
194    /// Compressed storage size in bytes.
195    pub fn compressed_size(&self) -> u32 {
196        // SAFETY: plain scalar read through a live handle.
197        unsafe { ffi::whiteout_mpq_MpqFileInfo_get_compressedSize(self.raw.as_ptr()) }
198    }
199
200    pub fn set_compressed_size(&mut self, value: u32) {
201        // SAFETY: plain scalar write through a live handle.
202        unsafe { ffi::whiteout_mpq_MpqFileInfo_set_compressedSize(self.raw.as_ptr(), value) }
203    }
204
205    /// Uncompressed (original) file size in bytes.
206    pub fn uncompressed_size(&self) -> u32 {
207        // SAFETY: plain scalar read through a live handle.
208        unsafe { ffi::whiteout_mpq_MpqFileInfo_get_uncompressedSize(self.raw.as_ptr()) }
209    }
210
211    pub fn set_uncompressed_size(&mut self, value: u32) {
212        // SAFETY: plain scalar write through a live handle.
213        unsafe { ffi::whiteout_mpq_MpqFileInfo_set_uncompressedSize(self.raw.as_ptr(), value) }
214    }
215
216    /// Block entry flags (see FileFlags enum).
217    pub fn flags(&self) -> FileFlags {
218        // SAFETY: scalar read; a flag set accepts any bits.
219        FileFlags(unsafe { ffi::whiteout_mpq_MpqFileInfo_get_flags(self.raw.as_ptr()) })
220    }
221
222    pub fn set_flags(&mut self, value: FileFlags) {
223        // SAFETY: scalar write through a live handle.
224        unsafe { ffi::whiteout_mpq_MpqFileInfo_set_flags(self.raw.as_ptr(), value.0) }
225    }
226
227    /// Locale ID (typically Locale::Neutral).
228    pub fn locale(&self) -> u16 {
229        // SAFETY: plain scalar read through a live handle.
230        unsafe { ffi::whiteout_mpq_MpqFileInfo_get_locale(self.raw.as_ptr()) }
231    }
232
233    pub fn set_locale(&mut self, value: u16) {
234        // SAFETY: plain scalar write through a live handle.
235        unsafe { ffi::whiteout_mpq_MpqFileInfo_set_locale(self.raw.as_ptr(), value) }
236    }
237}
238
239impl Default for FileInfo {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245/// Summary information about the archive.
246pub struct ArchiveInfo {
247    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_MpqArchiveInfo>,
248}
249
250impl Drop for ArchiveInfo {
251    fn drop(&mut self) {
252        // SAFETY: `raw` came from a native constructor and Drop runs once.
253        unsafe { ffi::whiteout_mpq_MpqArchiveInfo_delete(self.raw.as_ptr()) }
254    }
255}
256
257impl ArchiveInfo {
258    /// # Safety
259    /// `raw` must be a live handle this value takes ownership of.
260    #[allow(dead_code)] // used by whichever methods return this type
261    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_MpqArchiveInfo) -> Option<Self> {
262        core::ptr::NonNull::new(raw).map(|raw| ArchiveInfo { raw })
263    }
264}
265
266// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
267// is deliberately NOT implemented — the C++ types make no documented
268// guarantee about concurrent use, and claiming one we haven't verified
269// would be unsound. See `@bind thread_safe` in the plan.
270unsafe impl Send for ArchiveInfo {}
271
272impl core::fmt::Debug for ArchiveInfo {
273    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
274        f.debug_struct("ArchiveInfo").finish_non_exhaustive()
275    }
276}
277
278impl ArchiveInfo {
279    /// # Panics
280    /// Panics if the native allocation fails.
281    pub fn new() -> Self {
282        // SAFETY: the native constructor returns a live handle; a null here
283        // means the library is unusable.
284        unsafe {
285            let raw = ffi::whiteout_mpq_MpqArchiveInfo_new();
286            Self::from_raw(raw).expect("native ArchiveInfo allocation failed")
287        }
288    }
289
290    /// 0 = V1, 1 = V2.
291    pub fn format_version(&self) -> u16 {
292        // SAFETY: plain scalar read through a live handle.
293        unsafe { ffi::whiteout_mpq_MpqArchiveInfo_get_formatVersion(self.raw.as_ptr()) }
294    }
295
296    pub fn set_format_version(&mut self, value: u16) {
297        // SAFETY: plain scalar write through a live handle.
298        unsafe { ffi::whiteout_mpq_MpqArchiveInfo_set_formatVersion(self.raw.as_ptr(), value) }
299    }
300
301    /// Hash table capacity (always a power of 2).
302    pub fn hash_table_entries(&self) -> u32 {
303        // SAFETY: plain scalar read through a live handle.
304        unsafe { ffi::whiteout_mpq_MpqArchiveInfo_get_hashTableEntries(self.raw.as_ptr()) }
305    }
306
307    pub fn set_hash_table_entries(&mut self, value: u32) {
308        // SAFETY: plain scalar write through a live handle.
309        unsafe { ffi::whiteout_mpq_MpqArchiveInfo_set_hashTableEntries(self.raw.as_ptr(), value) }
310    }
311
312    /// Number of occupied block table entries.
313    pub fn block_table_entries(&self) -> u32 {
314        // SAFETY: plain scalar read through a live handle.
315        unsafe { ffi::whiteout_mpq_MpqArchiveInfo_get_blockTableEntries(self.raw.as_ptr()) }
316    }
317
318    pub fn set_block_table_entries(&mut self, value: u32) {
319        // SAFETY: plain scalar write through a live handle.
320        unsafe { ffi::whiteout_mpq_MpqArchiveInfo_set_blockTableEntries(self.raw.as_ptr(), value) }
321    }
322
323    /// Sector size in bytes (512 << sectorSizeShift).
324    pub fn sector_size(&self) -> u32 {
325        // SAFETY: plain scalar read through a live handle.
326        unsafe { ffi::whiteout_mpq_MpqArchiveInfo_get_sectorSize(self.raw.as_ptr()) }
327    }
328
329    pub fn set_sector_size(&mut self, value: u32) {
330        // SAFETY: plain scalar write through a live handle.
331        unsafe { ffi::whiteout_mpq_MpqArchiveInfo_set_sectorSize(self.raw.as_ptr(), value) }
332    }
333
334    /// Total archive size in bytes.
335    pub fn archive_size(&self) -> u64 {
336        // SAFETY: plain scalar read through a live handle.
337        unsafe { ffi::whiteout_mpq_MpqArchiveInfo_get_archiveSize(self.raw.as_ptr()) }
338    }
339
340    pub fn set_archive_size(&mut self, value: u64) {
341        // SAFETY: plain scalar write through a live handle.
342        unsafe { ffi::whiteout_mpq_MpqArchiveInfo_set_archiveSize(self.raw.as_ptr(), value) }
343    }
344}
345
346impl Default for ArchiveInfo {
347    fn default() -> Self {
348        Self::new()
349    }
350}
351
352/// Options for writing a file into the archive.
353pub struct WriteOptions {
354    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_MpqWriteOptions>,
355}
356
357impl Drop for WriteOptions {
358    fn drop(&mut self) {
359        // SAFETY: `raw` came from a native constructor and Drop runs once.
360        unsafe { ffi::whiteout_mpq_MpqWriteOptions_delete(self.raw.as_ptr()) }
361    }
362}
363
364impl WriteOptions {
365    /// # Safety
366    /// `raw` must be a live handle this value takes ownership of.
367    #[allow(dead_code)] // used by whichever methods return this type
368    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_MpqWriteOptions) -> Option<Self> {
369        core::ptr::NonNull::new(raw).map(|raw| WriteOptions { raw })
370    }
371}
372
373// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
374// is deliberately NOT implemented — the C++ types make no documented
375// guarantee about concurrent use, and claiming one we haven't verified
376// would be unsound. See `@bind thread_safe` in the plan.
377unsafe impl Send for WriteOptions {}
378
379impl core::fmt::Debug for WriteOptions {
380    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
381        f.debug_struct("WriteOptions").finish_non_exhaustive()
382    }
383}
384
385impl WriteOptions {
386    /// # Panics
387    /// Panics if the native allocation fails.
388    pub fn new() -> Self {
389        // SAFETY: the native constructor returns a live handle; a null here
390        // means the library is unusable.
391        unsafe {
392            let raw = ffi::whiteout_mpq_MpqWriteOptions_new();
393            Self::from_raw(raw).expect("native WriteOptions allocation failed")
394        }
395    }
396
397    /// Compression algorithm to apply.
398    pub fn compression(&self) -> Compression {
399        // SAFETY: scalar read; the discriminant is validated below.
400        unsafe { ffi::whiteout_mpq_MpqWriteOptions_get_compression(self.raw.as_ptr()) }
401            .try_into()
402            .expect("unknown enum discriminant from the native library")
403    }
404
405    pub fn set_compression(&mut self, value: Compression) {
406        // SAFETY: scalar write through a live handle.
407        unsafe {
408            ffi::whiteout_mpq_MpqWriteOptions_set_compression(self.raw.as_ptr(), value as i32)
409        }
410    }
411
412    /// Locale ID for the hash table slot.
413    pub fn locale(&self) -> u16 {
414        // SAFETY: plain scalar read through a live handle.
415        unsafe { ffi::whiteout_mpq_MpqWriteOptions_get_locale(self.raw.as_ptr()) }
416    }
417
418    pub fn set_locale(&mut self, value: u16) {
419        // SAFETY: plain scalar write through a live handle.
420        unsafe { ffi::whiteout_mpq_MpqWriteOptions_set_locale(self.raw.as_ptr(), value) }
421    }
422
423    /// Encrypt file data with a derived key.
424    pub fn encrypt(&self) -> bool {
425        // SAFETY: plain scalar read through a live handle.
426        unsafe { ffi::whiteout_mpq_MpqWriteOptions_get_encrypt(self.raw.as_ptr()) != 0 }
427    }
428
429    pub fn set_encrypt(&mut self, value: bool) {
430        // SAFETY: plain scalar write through a live handle.
431        unsafe {
432            ffi::whiteout_mpq_MpqWriteOptions_set_encrypt(
433                self.raw.as_ptr(),
434                if value { 1 } else { 0 },
435            )
436        }
437    }
438
439    /// Store the file as a single unpartitioned unit.
440    pub fn single_unit(&self) -> bool {
441        // SAFETY: plain scalar read through a live handle.
442        unsafe { ffi::whiteout_mpq_MpqWriteOptions_get_singleUnit(self.raw.as_ptr()) != 0 }
443    }
444
445    pub fn set_single_unit(&mut self, value: bool) {
446        // SAFETY: plain scalar write through a live handle.
447        unsafe {
448            ffi::whiteout_mpq_MpqWriteOptions_set_singleUnit(
449                self.raw.as_ptr(),
450                if value { 1 } else { 0 },
451            )
452        }
453    }
454}
455
456impl Default for WriteOptions {
457    fn default() -> Self {
458        Self::new()
459    }
460}
461
462/// Options for creating a new archive.
463pub struct CreateOptions {
464    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_MpqCreateOptions>,
465}
466
467impl Drop for CreateOptions {
468    fn drop(&mut self) {
469        // SAFETY: `raw` came from a native constructor and Drop runs once.
470        unsafe { ffi::whiteout_mpq_MpqCreateOptions_delete(self.raw.as_ptr()) }
471    }
472}
473
474impl CreateOptions {
475    /// # Safety
476    /// `raw` must be a live handle this value takes ownership of.
477    #[allow(dead_code)] // used by whichever methods return this type
478    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_MpqCreateOptions) -> Option<Self> {
479        core::ptr::NonNull::new(raw).map(|raw| CreateOptions { raw })
480    }
481}
482
483// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
484// is deliberately NOT implemented — the C++ types make no documented
485// guarantee about concurrent use, and claiming one we haven't verified
486// would be unsound. See `@bind thread_safe` in the plan.
487unsafe impl Send for CreateOptions {}
488
489impl core::fmt::Debug for CreateOptions {
490    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
491        f.debug_struct("CreateOptions").finish_non_exhaustive()
492    }
493}
494
495impl CreateOptions {
496    /// # Panics
497    /// Panics if the native allocation fails.
498    pub fn new() -> Self {
499        // SAFETY: the native constructor returns a live handle; a null here
500        // means the library is unusable.
501        unsafe {
502            let raw = ffi::whiteout_mpq_MpqCreateOptions_new();
503            Self::from_raw(raw).expect("native CreateOptions allocation failed")
504        }
505    }
506
507    /// Archive format version (V1 or V2).
508    pub fn version(&self) -> FormatVersion {
509        // SAFETY: scalar read; the discriminant is validated below.
510        unsafe { ffi::whiteout_mpq_MpqCreateOptions_get_version(self.raw.as_ptr()) }
511            .try_into()
512            .expect("unknown enum discriminant from the native library")
513    }
514
515    pub fn set_version(&mut self, value: FormatVersion) {
516        // SAFETY: scalar write through a live handle.
517        unsafe { ffi::whiteout_mpq_MpqCreateOptions_set_version(self.raw.as_ptr(), value as i32) }
518    }
519
520    /// Initial hash table capacity; rounded up to the next power of 2.
521    pub fn hash_table_size(&self) -> u32 {
522        // SAFETY: plain scalar read through a live handle.
523        unsafe { ffi::whiteout_mpq_MpqCreateOptions_get_hashTableSize(self.raw.as_ptr()) }
524    }
525
526    pub fn set_hash_table_size(&mut self, value: u32) {
527        // SAFETY: plain scalar write through a live handle.
528        unsafe { ffi::whiteout_mpq_MpqCreateOptions_set_hashTableSize(self.raw.as_ptr(), value) }
529    }
530
531    /// Sector size = 512 << shift (default 3 → 4096 bytes).
532    pub fn sector_size_shift(&self) -> u16 {
533        // SAFETY: plain scalar read through a live handle.
534        unsafe { ffi::whiteout_mpq_MpqCreateOptions_get_sectorSizeShift(self.raw.as_ptr()) }
535    }
536
537    pub fn set_sector_size_shift(&mut self, value: u16) {
538        // SAFETY: plain scalar write through a live handle.
539        unsafe { ffi::whiteout_mpq_MpqCreateOptions_set_sectorSizeShift(self.raw.as_ptr(), value) }
540    }
541}
542
543impl Default for CreateOptions {
544    fn default() -> Self {
545        Self::new()
546    }
547}
548
549/// RAII wrapper for MPQ archive access
550///
551/// Provides full CRUD operations on MPQ archives.  Modifications are held in a virtual overlay until save() is called, which writes a complete new archive atomically (write to temp file, then rename).
552///
553/// All public methods are thread-safe: read operations acquire a shared lock; write and persist operations acquire an exclusive lock.
554pub struct Storage {
555    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_MpqStorage>,
556}
557
558impl Drop for Storage {
559    fn drop(&mut self) {
560        // SAFETY: `raw` came from a native constructor and Drop runs once.
561        unsafe { ffi::whiteout_mpq_MpqStorage_delete(self.raw.as_ptr()) }
562    }
563}
564
565impl Storage {
566    /// # Safety
567    /// `raw` must be a live handle this value takes ownership of.
568    #[allow(dead_code)] // used by whichever methods return this type
569    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_MpqStorage) -> Option<Self> {
570        core::ptr::NonNull::new(raw).map(|raw| Storage { raw })
571    }
572}
573
574// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
575// is deliberately NOT implemented — the C++ types make no documented
576// guarantee about concurrent use, and claiming one we haven't verified
577// would be unsound. See `@bind thread_safe` in the plan.
578unsafe impl Send for Storage {}
579
580impl core::fmt::Debug for Storage {
581    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
582        f.debug_struct("Storage").finish_non_exhaustive()
583    }
584}
585
586impl Storage {
587    /// Open an existing MPQ archive. Memory-maps the file and parses tables. @param path  Path to the .mpq file. @param pool  Optional WorkerPool for parallel compress/decompress (non-owning). @return A valid Storage, or std::nullopt on failure.
588    pub fn open(path: &str, pool: Option<&crate::interfaces::HostWorkerPool>) -> Option<Storage> {
589        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
590        // SAFETY: handle is live for the duration of the call.
591        unsafe {
592            Storage::from_raw(ffi::whiteout_mpq_MpqStorage_open(
593                path_cstr.as_ptr(),
594                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
595            ))
596        }
597    }
598
599    /// Create a new empty archive in memory. No file on disk until save(path).
600    pub fn create(
601        opts: &CreateOptions,
602        pool: Option<&crate::interfaces::HostWorkerPool>,
603    ) -> Option<Storage> {
604        // SAFETY: handle is live for the duration of the call.
605        unsafe {
606            Storage::from_raw(ffi::whiteout_mpq_MpqStorage_create(
607                opts.raw.as_ptr(),
608                pool.map_or(core::ptr::null_mut(), |v| v.as_ptr()),
609            ))
610        }
611    }
612
613    /// Release all resources. Same effect as letting the Storage go out of scope.
614    pub fn close(&mut self) {
615        // SAFETY: handle is live for the duration of the call.
616        unsafe {
617            ffi::whiteout_mpq_MpqStorage_close(self.raw.as_ptr());
618        }
619    }
620
621    /// Read a file from the archive. Checks the overlay first, then the source archive. @return File contents, or std::nullopt if not found or deleted.
622    pub fn read_file(&self, name: &str) -> Option<Bytes> {
623        let name_cstr = std::ffi::CString::new(name).unwrap_or_default();
624        // SAFETY: handle is live for the duration of the call.
625        unsafe {
626            Bytes::from_raw(ffi::whiteout_mpq_MpqStorage_readFile(
627                self.raw.as_ptr(),
628                name_cstr.as_ptr(),
629            ))
630        }
631    }
632
633    /// Read a file with a specific locale.
634    pub fn read_file_name_locale(&self, name: &str, locale: u16) -> Option<Bytes> {
635        let name_cstr = std::ffi::CString::new(name).unwrap_or_default();
636        // SAFETY: handle is live for the duration of the call.
637        unsafe {
638            Bytes::from_raw(ffi::whiteout_mpq_MpqStorage_readFile_name_locale(
639                self.raw.as_ptr(),
640                name_cstr.as_ptr(),
641                locale,
642            ))
643        }
644    }
645
646    /// Check if a file exists in the archive (including overlay).
647    pub fn file_exists(&self, name: &str) -> bool {
648        let name_cstr = std::ffi::CString::new(name).unwrap_or_default();
649        // SAFETY: handle is live for the duration of the call.
650        unsafe {
651            ffi::whiteout_mpq_MpqStorage_fileExists(self.raw.as_ptr(), name_cstr.as_ptr()) != 0
652        }
653    }
654
655    /// Get information about a file.
656    pub fn file_info(&self, name: &str) -> Option<FileInfo> {
657        let name_cstr = std::ffi::CString::new(name).unwrap_or_default();
658        // SAFETY: handle is live for the duration of the call.
659        unsafe {
660            FileInfo::from_raw(ffi::whiteout_mpq_MpqStorage_fileInfo(
661                self.raw.as_ptr(),
662                name_cstr.as_ptr(),
663            ))
664        }
665    }
666
667    /// Get summary information about the archive.
668    pub fn archive_info(&self) -> Option<ArchiveInfo> {
669        // SAFETY: handle is live for the duration of the call.
670        unsafe {
671            ArchiveInfo::from_raw(ffi::whiteout_mpq_MpqStorage_archiveInfo(self.raw.as_ptr()))
672        }
673    }
674
675    /// List all known filenames (from listfile + overlay additions − deletions).
676    pub fn list_files(&self) -> Vec<String> {
677        // SAFETY: one call materialises the list; the
678        // elements are borrowed out of it and it is freed
679        // before returning. Reading is O(1) per element.
680        unsafe {
681            let list = ffi::whiteout_mpq_MpqStorage_listFiles(self.raw.as_ptr());
682            if list.is_null() {
683                return Vec::new();
684            }
685            let n = ffi::whiteout_mpq_StringList_size(list);
686            let out = (0..n)
687                .map(|i| crate::support::take_string(ffi::whiteout_mpq_StringList_at(list, i)))
688                .collect();
689            ffi::whiteout_mpq_StringList_delete(list);
690            out
691        }
692    }
693
694    /// Write or overwrite a file. Data is held in overlay until save(). @return true on success, false if the hash table is full.
695    pub fn write_file(&mut self, name: &[u8], data: &[u8], opts: &WriteOptions) -> bool {
696        // SAFETY: handle is live for the duration of the call.
697        unsafe {
698            ffi::whiteout_mpq_MpqStorage_writeFile(
699                self.raw.as_ptr(),
700                name.as_ptr(),
701                name.len(),
702                data.as_ptr(),
703                data.len(),
704                opts.raw.as_ptr(),
705            ) != 0
706        }
707    }
708
709    /// Delete a file from the archive. @return true if the file was found (in source or overlay), false otherwise.
710    pub fn delete_file(&mut self, name: &str) -> bool {
711        let name_cstr = std::ffi::CString::new(name).unwrap_or_default();
712        // SAFETY: handle is live for the duration of the call.
713        unsafe {
714            ffi::whiteout_mpq_MpqStorage_deleteFile(self.raw.as_ptr(), name_cstr.as_ptr()) != 0
715        }
716    }
717
718    /// Save the archive to its original path (temp file + atomic rename). @return false if this Storage was created via create() with no prior save(path).
719    pub fn save(&mut self) -> bool {
720        // SAFETY: handle is live for the duration of the call.
721        unsafe { ffi::whiteout_mpq_MpqStorage_save(self.raw.as_ptr()) != 0 }
722    }
723
724    /// Save the archive to a specific path. After saving, the new file becomes the source archive and the overlay is cleared.
725    pub fn save_path(&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_mpq_MpqStorage_save_path(self.raw.as_ptr(), path_cstr.as_ptr()) != 0
730        }
731    }
732}
733
734/// VirtualPathFileSystem implementation backed by an MPQ archive.
735///
736/// The Storage must outlive this object — MpqFileSystem holds a non-owning reference to it.
737///
738/// Path separators: both '/' and '\\' are accepted and treated identically. Filename comparison is case-insensitive, matching MPQ archive semantics.
739///
740/// Requires the `whiteout_mpq` CMake target.
741///
742/// Example: auto storage = mpq::Storage::open("War3.mpq"); utils::MpqFileSystem fs(*storage); auto data = fs.readFile("units\\orc\\grunt\\grunt.mdx");
743pub struct FileSystem {
744    pub(crate) raw: core::ptr::NonNull<ffi::whiteout_MpqFileSystem>,
745}
746
747impl Drop for FileSystem {
748    fn drop(&mut self) {
749        // SAFETY: `raw` came from a native constructor and Drop runs once.
750        unsafe { ffi::whiteout_mpq_MpqFileSystem_delete(self.raw.as_ptr()) }
751    }
752}
753
754impl FileSystem {
755    /// # Safety
756    /// `raw` must be a live handle this value takes ownership of.
757    #[allow(dead_code)] // used by whichever methods return this type
758    pub(crate) unsafe fn from_raw(raw: *mut ffi::whiteout_MpqFileSystem) -> Option<Self> {
759        core::ptr::NonNull::new(raw).map(|raw| FileSystem { raw })
760    }
761}
762
763// SAFETY: handles are plain heap pointers with no thread affinity. `Sync`
764// is deliberately NOT implemented — the C++ types make no documented
765// guarantee about concurrent use, and claiming one we haven't verified
766// would be unsound. See `@bind thread_safe` in the plan.
767unsafe impl Send for FileSystem {}
768
769impl core::fmt::Debug for FileSystem {
770    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
771        f.debug_struct("FileSystem").finish_non_exhaustive()
772    }
773}
774
775impl FileSystem {
776    /// Read a file from the archive. Returns an empty vector if not found.
777    pub fn read_file(&self, path: &str) -> Bytes {
778        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
779        // SAFETY: handle is live for the duration of the call.
780        unsafe {
781            Bytes::from_raw(ffi::whiteout_mpq_MpqFileSystem_readFile(
782                self.raw.as_ptr(),
783                path_cstr.as_ptr(),
784            ))
785            .unwrap_or_else(Bytes::empty)
786        }
787    }
788
789    /// Write a file into the archive overlay. Changes are not persisted to disk until storage.save() is called on the underlying Storage.
790    pub fn write_file(&mut self, path: &str, data: &[u8]) -> bool {
791        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
792        // SAFETY: handle is live for the duration of the call.
793        unsafe {
794            ffi::whiteout_mpq_MpqFileSystem_writeFile(
795                self.raw.as_ptr(),
796                path_cstr.as_ptr(),
797                data.as_ptr(),
798                data.len(),
799            ) != 0
800        }
801    }
802
803    /// Check if a file exists in the archive (including the write overlay).
804    pub fn file_exists(&self, path: &str) -> bool {
805        let path_cstr = std::ffi::CString::new(path).unwrap_or_default();
806        // SAFETY: handle is live for the duration of the call.
807        unsafe {
808            ffi::whiteout_mpq_MpqFileSystem_fileExists(self.raw.as_ptr(), path_cstr.as_ptr()) != 0
809        }
810    }
811}
812
813#[doc(hidden)]
814pub mod ffi {
815    #![allow(missing_debug_implementations)]
816
817    #[allow(unused_imports)]
818    use crate::support::{RawBytes, RawCString};
819
820    #[repr(C)]
821    pub struct whiteout_MpqFileInfo {
822        _private: [u8; 0],
823    }
824    #[repr(C)]
825    pub struct whiteout_MpqArchiveInfo {
826        _private: [u8; 0],
827    }
828    #[repr(C)]
829    pub struct whiteout_MpqWriteOptions {
830        _private: [u8; 0],
831    }
832    #[repr(C)]
833    pub struct whiteout_MpqCreateOptions {
834        _private: [u8; 0],
835    }
836    #[repr(C)]
837    pub struct whiteout_MpqStorage {
838        _private: [u8; 0],
839    }
840    #[repr(C)]
841    pub struct whiteout_MpqFileSystem {
842        _private: [u8; 0],
843    }
844    #[repr(C)]
845    pub struct whiteout_StringList {
846        _private: [u8; 0],
847    }
848
849    extern "C" {
850        pub fn whiteout_mpq_StringList_size(self_: *mut whiteout_StringList) -> usize;
851        pub fn whiteout_mpq_StringList_at(
852            self_: *mut whiteout_StringList,
853            index: usize,
854        ) -> RawCString;
855        pub fn whiteout_mpq_StringList_delete(self_: *mut whiteout_StringList);
856        // FileInfo
857        pub fn whiteout_mpq_MpqFileInfo_new() -> *mut whiteout_MpqFileInfo;
858        pub fn whiteout_mpq_MpqFileInfo_delete(self_: *mut whiteout_MpqFileInfo);
859        pub fn whiteout_mpq_MpqFileInfo_get_name(self_: *mut whiteout_MpqFileInfo) -> RawCString;
860        pub fn whiteout_mpq_MpqFileInfo_set_name(
861            self_: *mut whiteout_MpqFileInfo,
862            value: *const core::ffi::c_char,
863        );
864        pub fn whiteout_mpq_MpqFileInfo_get_compressedSize(self_: *mut whiteout_MpqFileInfo)
865            -> u32;
866        pub fn whiteout_mpq_MpqFileInfo_set_compressedSize(
867            self_: *mut whiteout_MpqFileInfo,
868            value: u32,
869        );
870        pub fn whiteout_mpq_MpqFileInfo_get_uncompressedSize(
871            self_: *mut whiteout_MpqFileInfo,
872        ) -> u32;
873        pub fn whiteout_mpq_MpqFileInfo_set_uncompressedSize(
874            self_: *mut whiteout_MpqFileInfo,
875            value: u32,
876        );
877        pub fn whiteout_mpq_MpqFileInfo_get_flags(self_: *mut whiteout_MpqFileInfo) -> i32;
878        pub fn whiteout_mpq_MpqFileInfo_set_flags(self_: *mut whiteout_MpqFileInfo, value: i32);
879        pub fn whiteout_mpq_MpqFileInfo_get_locale(self_: *mut whiteout_MpqFileInfo) -> u16;
880        pub fn whiteout_mpq_MpqFileInfo_set_locale(self_: *mut whiteout_MpqFileInfo, value: u16);
881        // ArchiveInfo
882        pub fn whiteout_mpq_MpqArchiveInfo_new() -> *mut whiteout_MpqArchiveInfo;
883        pub fn whiteout_mpq_MpqArchiveInfo_delete(self_: *mut whiteout_MpqArchiveInfo);
884        pub fn whiteout_mpq_MpqArchiveInfo_get_formatVersion(
885            self_: *mut whiteout_MpqArchiveInfo,
886        ) -> u16;
887        pub fn whiteout_mpq_MpqArchiveInfo_set_formatVersion(
888            self_: *mut whiteout_MpqArchiveInfo,
889            value: u16,
890        );
891        pub fn whiteout_mpq_MpqArchiveInfo_get_hashTableEntries(
892            self_: *mut whiteout_MpqArchiveInfo,
893        ) -> u32;
894        pub fn whiteout_mpq_MpqArchiveInfo_set_hashTableEntries(
895            self_: *mut whiteout_MpqArchiveInfo,
896            value: u32,
897        );
898        pub fn whiteout_mpq_MpqArchiveInfo_get_blockTableEntries(
899            self_: *mut whiteout_MpqArchiveInfo,
900        ) -> u32;
901        pub fn whiteout_mpq_MpqArchiveInfo_set_blockTableEntries(
902            self_: *mut whiteout_MpqArchiveInfo,
903            value: u32,
904        );
905        pub fn whiteout_mpq_MpqArchiveInfo_get_sectorSize(
906            self_: *mut whiteout_MpqArchiveInfo,
907        ) -> u32;
908        pub fn whiteout_mpq_MpqArchiveInfo_set_sectorSize(
909            self_: *mut whiteout_MpqArchiveInfo,
910            value: u32,
911        );
912        pub fn whiteout_mpq_MpqArchiveInfo_get_archiveSize(
913            self_: *mut whiteout_MpqArchiveInfo,
914        ) -> u64;
915        pub fn whiteout_mpq_MpqArchiveInfo_set_archiveSize(
916            self_: *mut whiteout_MpqArchiveInfo,
917            value: u64,
918        );
919        // WriteOptions
920        pub fn whiteout_mpq_MpqWriteOptions_new() -> *mut whiteout_MpqWriteOptions;
921        pub fn whiteout_mpq_MpqWriteOptions_delete(self_: *mut whiteout_MpqWriteOptions);
922        pub fn whiteout_mpq_MpqWriteOptions_get_compression(
923            self_: *mut whiteout_MpqWriteOptions,
924        ) -> i32;
925        pub fn whiteout_mpq_MpqWriteOptions_set_compression(
926            self_: *mut whiteout_MpqWriteOptions,
927            value: i32,
928        );
929        pub fn whiteout_mpq_MpqWriteOptions_get_locale(self_: *mut whiteout_MpqWriteOptions)
930            -> u16;
931        pub fn whiteout_mpq_MpqWriteOptions_set_locale(
932            self_: *mut whiteout_MpqWriteOptions,
933            value: u16,
934        );
935        pub fn whiteout_mpq_MpqWriteOptions_get_encrypt(
936            self_: *mut whiteout_MpqWriteOptions,
937        ) -> i32;
938        pub fn whiteout_mpq_MpqWriteOptions_set_encrypt(
939            self_: *mut whiteout_MpqWriteOptions,
940            value: i32,
941        );
942        pub fn whiteout_mpq_MpqWriteOptions_get_singleUnit(
943            self_: *mut whiteout_MpqWriteOptions,
944        ) -> i32;
945        pub fn whiteout_mpq_MpqWriteOptions_set_singleUnit(
946            self_: *mut whiteout_MpqWriteOptions,
947            value: i32,
948        );
949        // CreateOptions
950        pub fn whiteout_mpq_MpqCreateOptions_new() -> *mut whiteout_MpqCreateOptions;
951        pub fn whiteout_mpq_MpqCreateOptions_delete(self_: *mut whiteout_MpqCreateOptions);
952        pub fn whiteout_mpq_MpqCreateOptions_get_version(
953            self_: *mut whiteout_MpqCreateOptions,
954        ) -> i32;
955        pub fn whiteout_mpq_MpqCreateOptions_set_version(
956            self_: *mut whiteout_MpqCreateOptions,
957            value: i32,
958        );
959        pub fn whiteout_mpq_MpqCreateOptions_get_hashTableSize(
960            self_: *mut whiteout_MpqCreateOptions,
961        ) -> u32;
962        pub fn whiteout_mpq_MpqCreateOptions_set_hashTableSize(
963            self_: *mut whiteout_MpqCreateOptions,
964            value: u32,
965        );
966        pub fn whiteout_mpq_MpqCreateOptions_get_sectorSizeShift(
967            self_: *mut whiteout_MpqCreateOptions,
968        ) -> u16;
969        pub fn whiteout_mpq_MpqCreateOptions_set_sectorSizeShift(
970            self_: *mut whiteout_MpqCreateOptions,
971            value: u16,
972        );
973        // Storage
974        pub fn whiteout_mpq_MpqStorage_delete(self_: *mut whiteout_MpqStorage);
975        pub fn whiteout_mpq_MpqStorage_open(
976            path: *const core::ffi::c_char,
977            pool: *mut core::ffi::c_void,
978        ) -> *mut whiteout_MpqStorage;
979        pub fn whiteout_mpq_MpqStorage_create(
980            opts: *mut whiteout_MpqCreateOptions,
981            pool: *mut core::ffi::c_void,
982        ) -> *mut whiteout_MpqStorage;
983        pub fn whiteout_mpq_MpqStorage_close(self_: *mut whiteout_MpqStorage);
984        pub fn whiteout_mpq_MpqStorage_readFile(
985            self_: *mut whiteout_MpqStorage,
986            name: *const core::ffi::c_char,
987        ) -> RawBytes;
988        pub fn whiteout_mpq_MpqStorage_readFile_name_locale(
989            self_: *mut whiteout_MpqStorage,
990            name: *const core::ffi::c_char,
991            locale: u16,
992        ) -> RawBytes;
993        pub fn whiteout_mpq_MpqStorage_fileExists(
994            self_: *mut whiteout_MpqStorage,
995            name: *const core::ffi::c_char,
996        ) -> i32;
997        pub fn whiteout_mpq_MpqStorage_fileInfo(
998            self_: *mut whiteout_MpqStorage,
999            name: *const core::ffi::c_char,
1000        ) -> *mut whiteout_MpqFileInfo;
1001        pub fn whiteout_mpq_MpqStorage_archiveInfo(
1002            self_: *mut whiteout_MpqStorage,
1003        ) -> *mut whiteout_MpqArchiveInfo;
1004        pub fn whiteout_mpq_MpqStorage_listFiles(
1005            self_: *mut whiteout_MpqStorage,
1006        ) -> *mut whiteout_StringList;
1007        pub fn whiteout_mpq_MpqStorage_writeFile(
1008            self_: *mut whiteout_MpqStorage,
1009            name: *const u8,
1010            name_size: usize,
1011            data: *const u8,
1012            data_size: usize,
1013            opts: *mut whiteout_MpqWriteOptions,
1014        ) -> i32;
1015        pub fn whiteout_mpq_MpqStorage_deleteFile(
1016            self_: *mut whiteout_MpqStorage,
1017            name: *const core::ffi::c_char,
1018        ) -> i32;
1019        pub fn whiteout_mpq_MpqStorage_save(self_: *mut whiteout_MpqStorage) -> i32;
1020        pub fn whiteout_mpq_MpqStorage_save_path(
1021            self_: *mut whiteout_MpqStorage,
1022            path: *const core::ffi::c_char,
1023        ) -> i32;
1024        // FileSystem
1025        pub fn whiteout_mpq_MpqFileSystem_new_storage(
1026            _0: *mut core::ffi::c_void,
1027        ) -> *mut whiteout_MpqFileSystem;
1028        pub fn whiteout_mpq_MpqFileSystem_delete(self_: *mut whiteout_MpqFileSystem);
1029        pub fn whiteout_mpq_MpqFileSystem_readFile(
1030            self_: *mut whiteout_MpqFileSystem,
1031            path: *const core::ffi::c_char,
1032        ) -> RawBytes;
1033        pub fn whiteout_mpq_MpqFileSystem_writeFile(
1034            self_: *mut whiteout_MpqFileSystem,
1035            path: *const core::ffi::c_char,
1036            data: *const u8,
1037            data_size: usize,
1038        ) -> i32;
1039        pub fn whiteout_mpq_MpqFileSystem_fileExists(
1040            self_: *mut whiteout_MpqFileSystem,
1041            path: *const core::ffi::c_char,
1042        ) -> i32;
1043    }
1044}