1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
//! Support of Riot PROP files
//!
//! # Overview
//!
//! PROP files, more commonly called *bin* files, contain nested data structures.
//! All structures are typed and usually abide to the same type, but type is provided in the data
//! itself, so there is no need to have a schema to decode it.
//!
//! Each PROP File contains a list of [entries](BinEntry) which itself contains a nested list of
//! [fields](BinField) of various [data types](BinType).
//!
//! # Dynamic types
//!
//! Container types store data whose type is defined dynamically.
//! Since Rust types are defined statically, they cannot be used directly.
//! Moreover, even if the embedded bin type is known by the container at run-time, the Rust type
//! system requires the user to explicitely request a given type, one way or the other.
//!
//! The [`binget!()`] macro makes it easier to chain casts and should be enough when the names and
//! types to get are known in advance.
//!
//! Data can also be casted explicitely, using the `downcast()` method provided by
//! all types wrapping dynamically typed data:
//! ```no_run
//! # use cdragon_prop::data::*;
//! # fn test(field: BinField) {
//! field.downcast::<BinString>();
//! # }
//! // => `Some(BinString)` if field contains a string, `None` otherwise
//! ```
//! Containers with fields provide a `getv()` helper to follow a `get()` with a `downcast()`.
//!
//! If all possible types have to be handled, the [`binvalue_map_type!()`] can be used to provide a
//! single generic expression to handle all possible types.
//!
//! Map keys support only a subset or types. [`binvalue_map_keytype!()`] can be used to map only
//! key types. It can be combined with another [`binvalue_map_type!()`] to handle both keys and
//! values.
//!
//! **Note:** those macros are expanded to a `match` handling all possible cases; resulting code can be
//! large.
//!
//! ## Examples
//! ```
//! # use cdragon_prop::{binvalue_map_keytype, binvalue_map_type, data::*};
//! # fn test(field: BinField, map: BinMap) {
//! binvalue_map_type!(field.vtype, T, {
//!     let value: &T = field.downcast::<T>().unwrap();
//! });
//!
//! binvalue_map_keytype!(map.ktype, K,
//!     binvalue_map_type!(map.vtype, V, {
//!         let entries: &Vec<(K, V)> = map.downcast::<K, V>().unwrap();
//!     })
//! );
//! # }
//! ```
//!
//! # Bin hashes
//!
//! Bin files use 32-bit FNV-1a hashes for several identifier names:
//!
//! - [entry paths](BinEntryPath)
//! - [class names](BinClassName)
//! - [field names](BinFieldName)
//! - ["bin hash" values](BinHashValue).
//!
//! Hash values can be computed at compile-time with [`cdragon_hashes::binh!()`], or
//! using other methods from [`cdragon_hashes::bin`].
//!
//! A [`BinHashMappers`] gather all hash-to-string conversion needed by bin data.

mod macros;
mod parser;
mod serializer;
mod text_tree;
mod json;
pub mod visitor;
pub mod data;

use std::io;
use std::fs;
use std::path::Path;
use thiserror::Error;
use cdragon_hashes::{HashKind, HashMapper, HashError};
use cdragon_utils::parsing::ParseError;
pub use cdragon_hashes::bin::{BinHashKind, BinHashMapper};

pub use serializer::{BinSerializer, BinEntriesSerializer};
pub use data::*;
pub use parser::{BinEntryScanner, BinEntryScannerItem};
pub use text_tree::TextTreeSerializer;
pub use json::JsonSerializer;
pub use visitor::{BinVisitor, BinTraversal};


/// Result type for PROP file errors
type Result<T, E = PropError> = std::result::Result<T, E>;


/// Generic type to associate each kind of hash from bin files to a value
#[allow(missing_docs)]
#[derive(Debug)]
pub struct BinHashKindMapping<T, U> {
    pub entry_path: T,
    pub class_name: T,
    pub field_name: T,
    pub hash_value: T,
    pub path_value: U,
}

impl<T, U> BinHashKindMapping<T, U> {
    /// Give access to a specific field from its kind
    #[inline]
    pub fn get(&self, kind: BinHashKind) -> &T {
        match kind {
            BinHashKind::EntryPath => &self.entry_path,
            BinHashKind::ClassName => &self.class_name,
            BinHashKind::FieldName => &self.field_name,
            BinHashKind::HashValue => &self.hash_value,
        }
    }

    /// Give mutable access to a specific mapper from its kind
    #[inline]
    pub fn get_mut(&mut self, kind: BinHashKind) -> &mut T {
        match kind {
            BinHashKind::EntryPath => &mut self.entry_path,
            BinHashKind::ClassName => &mut self.class_name,
            BinHashKind::FieldName => &mut self.field_name,
            BinHashKind::HashValue => &mut self.hash_value,
        }
    }
}

impl<T: Default, U: Default> Default for BinHashKindMapping<T, U> {
    fn default() -> Self {
        Self {
            entry_path: T::default(),
            class_name: T::default(),
            field_name: T::default(),
            hash_value: T::default(),
            path_value: U::default(),
        }
    }
}


/// Hash mappers for all kinds of bin hashes
///
/// Each individual mapper can be accessed either directly through its field, or from a
/// `BinHashKind` value.
pub type BinHashMappers = BinHashKindMapping<BinHashMapper, HashMapper<u64, 64>>;

impl BinHashMappers {
    /// Create mapper, load all sub-mappers from a directory path
    pub fn from_dirpath(path: &Path) -> Result<Self, HashError> {
        let mut this = Self::default();
        this.load_dirpath(path)?;
        Ok(this)
    }

    /// Load all sub-mappers from a directory path
    pub fn load_dirpath(&mut self, path: &Path) -> Result<(), HashError> {
        self.entry_path.load_path(path.join(HashKind::BinEntryPath.mapping_path()))?;
        self.class_name.load_path(path.join(HashKind::BinClassName.mapping_path()))?;
        self.field_name.load_path(path.join(HashKind::BinFieldName.mapping_path()))?;
        self.hash_value.load_path(path.join(HashKind::BinHashValue.mapping_path()))?;
        self.path_value.load_path(path.join(HashKind::WadGame.mapping_path()))?;
        Ok(())
    }

    /// Write all sub-mappers to a directory path
    pub fn write_dirpath(&self, path: &Path) -> Result<(), HashError> {
        self.entry_path.write_path(path.join(HashKind::BinEntryPath.mapping_path()))?;
        self.class_name.write_path(path.join(HashKind::BinClassName.mapping_path()))?;
        self.field_name.write_path(path.join(HashKind::BinFieldName.mapping_path()))?;
        self.hash_value.write_path(path.join(HashKind::BinHashValue.mapping_path()))?;
        self.path_value.write_path(path.join(HashKind::WadGame.mapping_path()))?;
        Ok(())
    }
}

/// PROP file, with entries
///
/// This structure contains all the data of a PROP file, completely parsed.
/// It also provides methods to simply scan an file, without storing all the data, and possibly
/// skipping unneeded data.
#[derive(Debug)]
pub struct PropFile {
    /// PROP version
    pub version: u32,
    /// `true` for patch file
    ///
    /// Patch files are used to hot-patch data from other, regular files.
    /// They are usually much slower and a notably used by Riot to update the game without a new
    /// release (patches are then provided directly by the server when the game starts).
    pub is_patch: bool,
    /// List of paths to other PROP files
    pub linked_files: Vec<String>,
    /// List of bin entries
    pub entries: Vec<BinEntry>,
}

impl PropFile {
    /// Parse a whole `PropFile` from data
    pub fn from_slice(data: &[u8]) -> Result<PropFile> {
        Ok(parser::binparse(data)?)
    }

    /// Parse a whole `PropFile` from data
    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<PropFile> {
        Self::from_slice(&fs::read(path.as_ref())?)
    }

    /// Iterate on entry headers (path and type) from a PROP reader
    pub fn scan_entries_from_reader<R: io::Read>(reader: R) -> Result<BinEntryScanner<R>> {
        let scanner = BinEntryScanner::new(reader)?;
        Ok(scanner)
    }

    /// Iterate on entry headers (path and type) from a PROP file path
    pub fn scan_entries_from_path<P: AsRef<Path>>(path: P) -> Result<BinEntryScanner<io::BufReader<fs::File>>> {
        let file = fs::File::open(path)?;
        let reader = io::BufReader::new(file);
        let scanner = BinEntryScanner::new(reader)?;
        Ok(scanner)
    }
}

/// Entry header, used by parsers that iterate on entries
pub type BinEntryHeader = (BinEntryPath, BinClassName);

/// Entry in a PROP file
#[derive(Debug)]
pub struct BinEntry {
    /// Entry path (hashed)
    pub path: BinEntryPath,
    /// Class type of the entry
    pub ctype: BinClassName,
    /// Struct fields
    pub fields: Vec<BinField>,
}

impl BinEntry {
    /// Get a field by its name
    pub fn get(&self, name: BinFieldName) -> Option<&BinField> {
        self.fields.iter().find(|f| f.name == name)
    }

    /// Get a field by its name and downcast it
    pub fn getv<T: BinValue + 'static>(&self, name: BinFieldName) -> Option<&T> {
        self.get(name).and_then(|field| field.downcast::<T>())
    }
}

/// Files known to not be PROP files, despite their extension
pub const NON_PROP_BASENAMES: &[&str]  = &[
    "atlas_info.bin",
    "tftoutofgamecharacterdata.bin",
    "tftmapcharacterlists.bin",
    "tftactivesets.bin",
    "tftitemlist.bin",
];

/// Return `true` if a path is a bin file path
///
/// This helper is intended to be used with files extracted by CDragon.
/// It has several limitations.
///
/// - File content is not checked, only path is checkde
/// - Some PROP files don't have the `.bin` extension, they will not be detected.
///   (CDragon add the missing extension and thus does not have this problem.)
/// - Some files have a `.bin` extension but are not actually PROP files.
///   This helper return `false` for known occurrences.
pub fn is_binfile_path(path: &Path) -> bool {
    if let Some(true) = path.extension().map(|s| s == "bin") {
        if let Some(name) = path.file_name() {
            // Some files are not actual 'PROP' files
            return name.to_str()
                .map(|s| !NON_PROP_BASENAMES.contains(&s))
                .unwrap_or(false)
        }
    }
    false
}


/// Error in a PROP file
#[allow(missing_docs)]
#[derive(Error, Debug)]
pub enum PropError {
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error("parsing error")]
    Parsing(#[from] ParseError),
}