gvas/
lib.rs

1#![warn(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
2#![warn(missing_docs)]
3
4//! Gvas
5//!
6//! UE4 Save File parsing library
7//!
8//! # Examples
9//!
10//! ```no_run
11//! use gvas::{error::Error, GvasFile};
12//! use std::{
13//!     fs::File,
14//! };
15//! use gvas::game_version::GameVersion;
16//!
17//! let mut file = File::open("save.sav")?;
18//! let gvas_file = GvasFile::read(&mut file, GameVersion::Default);
19//!
20//! println!("{:#?}", gvas_file);
21//! # Ok::<(), Error>(())
22//! ```
23//!
24//! ## Hints
25//!
26//! If your file fails while parsing with a [`DeserializeError::MissingHint`] error you need hints.
27//! When a struct is stored inside ArrayProperty/SetProperty/MapProperty in GvasFile it does not contain type annotations.
28//! This means that a library parsing the file must know the type beforehand. That's why you need hints.
29//!
30//! The error usually looks like this:
31//! ```no_run,ignore
32//! MissingHint(
33//!         "StructProperty" /* property type */,
34//!         "UnLockedMissionParameters.MapProperty.Key.StructProperty" /* property path */,
35//!         120550 /* position */)
36//! ```
37//! To get a hint type you need to look at the position of [`DeserializeError::MissingHint`] error.
38//! Then you go to that position in the file and try to determine which type the struct has.
39//! Afterwards you parse the file like this:
40//!
41//!
42//!  [`DeserializeError::MissingHint`]: error/enum.DeserializeError.html#variant.MissingHint
43//!
44//! ```no_run
45//! use gvas::{error::Error, GvasFile};
46//! use std::{
47//!     collections::HashMap,
48//!     fs::File,
49//! };
50//! use gvas::game_version::GameVersion;
51//!
52//! let mut file = File::open("save.sav")?;
53//!
54//! let mut hints = HashMap::new();
55//! hints.insert("UnLockedMissionParameters.MapProperty.Key.StructProperty".to_string(), "Guid".to_string());
56//!
57//! let gvas_file = GvasFile::read_with_hints(&mut file, GameVersion::Default, &hints);
58//!
59//! println!("{:#?}", gvas_file);
60//! # Ok::<(), Error>(())
61//! ```
62
63/// Extensions for `Cursor`.
64pub mod cursor_ext;
65/// Custom version information.
66pub mod custom_version;
67/// Engine version information.
68pub mod engine_version;
69/// Error types.
70pub mod error;
71/// Game version enumeration.
72pub mod game_version;
73/// Object version information.
74pub mod object_version;
75/// Extensions for `Ord`.
76mod ord_ext;
77/// Property types.
78pub mod properties;
79/// Savegame version information.
80pub mod savegame_version;
81pub(crate) mod scoped_stack_entry;
82/// Various types.
83pub mod types;
84
85use std::io::{Cursor, SeekFrom};
86use std::{
87    collections::HashMap,
88    fmt::Debug,
89    io::{Read, Seek, Write},
90};
91
92use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
93use flate2::read::ZlibDecoder;
94use flate2::write::ZlibEncoder;
95use flate2::Compression;
96
97use crate::{
98    cursor_ext::{ReadExt, WriteExt},
99    custom_version::FCustomVersion,
100    engine_version::FEngineVersion,
101    error::{DeserializeError, Error},
102    game_version::{DeserializedGameVersion, GameVersion, PalworldCompressionType, PLZ_MAGIC},
103    object_version::EUnrealEngineObjectUE5Version,
104    ord_ext::OrdExt,
105    properties::{Property, PropertyOptions, PropertyTrait},
106    savegame_version::SaveGameVersion,
107    types::{map::HashableIndexMap, Guid},
108};
109
110/// The four bytes 'GVAS' appear at the beginning of every GVAS file.
111pub const FILE_TYPE_GVAS: u32 = u32::from_le_bytes([b'G', b'V', b'A', b'S']);
112
113/// Stores information about GVAS file, engine version, etc.
114#[derive(Debug, Clone, PartialEq, Eq)]
115#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
116#[cfg_attr(feature = "serde", serde(tag = "type"))]
117pub enum GvasHeader {
118    /// Version 2
119    Version2 {
120        /// File format version.
121        package_file_version: u32,
122        /// Unreal Engine version.
123        engine_version: FEngineVersion,
124        /// Custom version format.
125        custom_version_format: u32,
126        /// Custom versions.
127        custom_versions: HashableIndexMap<Guid, u32>,
128        /// Save game class name.
129        save_game_class_name: String,
130    },
131    /// Version 3
132    Version3 {
133        /// File format version (UE4).
134        package_file_version: u32,
135        /// File format version (UE5).
136        package_file_version_ue5: u32,
137        /// Unreal Engine version.
138        engine_version: FEngineVersion,
139        /// Custom version format.
140        custom_version_format: u32,
141        /// Custom versions.
142        custom_versions: HashableIndexMap<Guid, u32>,
143        /// Save game class name.
144        save_game_class_name: String,
145    },
146}
147
148impl GvasHeader {
149    /// Read GvasHeader from a binary file
150    ///
151    /// # Errors
152    ///
153    /// If this function reads an invalid header it returns [`Error`]
154    ///
155    /// # Examples
156    ///
157    /// ```no_run
158    /// use gvas::{error::Error, GvasHeader};
159    /// use std::{
160    ///     fs::File,
161    /// };
162    ///
163    /// let mut file = File::open("save.sav")?;
164    ///
165    /// let gvas_header = GvasHeader::read(&mut file)?;
166    ///
167    /// println!("{:#?}", gvas_header);
168    /// # Ok::<(), Error>(())
169    /// ```
170    pub fn read<R: Read + Seek>(cursor: &mut R) -> Result<Self, Error> {
171        let file_type_tag = cursor.read_u32::<LittleEndian>()?;
172        if file_type_tag != FILE_TYPE_GVAS {
173            Err(DeserializeError::InvalidHeader(
174                format!("File type {file_type_tag} not recognized").into_boxed_str(),
175            ))?
176        }
177
178        let save_game_file_version = cursor.read_u32::<LittleEndian>()?;
179        if !save_game_file_version.between(
180            SaveGameVersion::AddedCustomVersions as u32,
181            SaveGameVersion::PackageFileSummaryVersionChange as u32,
182        ) {
183            Err(DeserializeError::InvalidHeader(
184                format!("GVAS version {save_game_file_version} not supported").into_boxed_str(),
185            ))?
186        }
187
188        let package_file_version = cursor.read_u32::<LittleEndian>()?;
189        if !package_file_version.between(0x205, 0x20D) {
190            Err(DeserializeError::InvalidHeader(
191                format!("Package file version {package_file_version} not supported")
192                    .into_boxed_str(),
193            ))?
194        }
195
196        // This field is only present in the v3 header
197        let package_file_version_ue5 = if save_game_file_version
198            >= SaveGameVersion::PackageFileSummaryVersionChange as u32
199        {
200            let version = cursor.read_u32::<LittleEndian>()?;
201            if !version.between(
202                EUnrealEngineObjectUE5Version::InitialVersion as u32,
203                EUnrealEngineObjectUE5Version::DataResources as u32,
204            ) {
205                Err(DeserializeError::InvalidHeader(
206                    format!("UE5 Package file version {version} is not supported").into_boxed_str(),
207                ))?
208            }
209            Some(version)
210        } else {
211            None
212        };
213
214        let engine_version = FEngineVersion::read(cursor)?;
215        let custom_version_format = cursor.read_u32::<LittleEndian>()?;
216        if custom_version_format != 3 {
217            Err(DeserializeError::InvalidHeader(
218                format!("Custom version format {custom_version_format} not supported")
219                    .into_boxed_str(),
220            ))?
221        }
222
223        let custom_versions_len = cursor.read_u32::<LittleEndian>()?;
224        let mut custom_versions = HashableIndexMap::with_capacity(custom_versions_len as usize);
225        for _ in 0..custom_versions_len {
226            let FCustomVersion { key, version } = FCustomVersion::read(cursor)?;
227            custom_versions.insert(key, version);
228        }
229
230        let save_game_class_name = cursor.read_string()?;
231
232        Ok(match package_file_version_ue5 {
233            None => GvasHeader::Version2 {
234                package_file_version,
235                engine_version,
236                custom_version_format,
237                custom_versions,
238                save_game_class_name,
239            },
240            Some(package_file_version_ue5) => GvasHeader::Version3 {
241                package_file_version,
242                package_file_version_ue5,
243                engine_version,
244                custom_version_format,
245                custom_versions,
246                save_game_class_name,
247            },
248        })
249    }
250
251    /// Write GvasHeader to a binary file
252    ///
253    /// # Examples
254    /// ```no_run
255    /// use gvas::{error::Error, GvasHeader};
256    /// use std::{
257    ///     fs::File,
258    ///     io::Cursor,
259    /// };
260    ///
261    /// let mut file = File::open("save.sav")?;
262    /// let gvas_header = GvasHeader::read(&mut file)?;
263    ///
264    /// let mut writer = Cursor::new(Vec::new());
265    /// gvas_header.write(&mut writer)?;
266    /// println!("{:#?}", writer.get_ref());
267    /// # Ok::<(), Error>(())
268    /// ```
269    pub fn write<W: Write>(&self, cursor: &mut W) -> Result<usize, Error> {
270        cursor.write_u32::<LittleEndian>(FILE_TYPE_GVAS)?;
271        match self {
272            GvasHeader::Version2 {
273                package_file_version,
274                engine_version,
275                custom_version_format,
276                custom_versions,
277                save_game_class_name,
278            } => {
279                let mut len = 20;
280                cursor.write_u32::<LittleEndian>(2)?;
281                cursor.write_u32::<LittleEndian>(*package_file_version)?;
282                len += engine_version.write(cursor)?;
283                cursor.write_u32::<LittleEndian>(*custom_version_format)?;
284                cursor.write_u32::<LittleEndian>(custom_versions.len() as u32)?;
285                for (&key, &version) in custom_versions {
286                    len += FCustomVersion::new(key, version).write(cursor)?;
287                }
288                len += cursor.write_string(save_game_class_name)?;
289                Ok(len)
290            }
291
292            GvasHeader::Version3 {
293                package_file_version,
294                package_file_version_ue5,
295                engine_version,
296                custom_version_format,
297                custom_versions,
298                save_game_class_name,
299            } => {
300                let mut len = 24;
301                cursor.write_u32::<LittleEndian>(3)?;
302                cursor.write_u32::<LittleEndian>(*package_file_version)?;
303                cursor.write_u32::<LittleEndian>(*package_file_version_ue5)?;
304                len += engine_version.write(cursor)?;
305                cursor.write_u32::<LittleEndian>(*custom_version_format)?;
306                cursor.write_u32::<LittleEndian>(custom_versions.len() as u32)?;
307                for (&key, &version) in custom_versions {
308                    len += FCustomVersion::new(key, version).write(cursor)?
309                }
310                len += cursor.write_string(save_game_class_name)?;
311                Ok(len)
312            }
313        }
314    }
315
316    /// Get custom versions from this header
317    pub fn get_custom_versions(&self) -> &HashableIndexMap<Guid, u32> {
318        match self {
319            GvasHeader::Version2 {
320                custom_versions, ..
321            } => custom_versions,
322            GvasHeader::Version3 {
323                custom_versions, ..
324            } => custom_versions,
325        }
326    }
327}
328
329/// Main UE4 save file struct
330#[derive(Debug, Clone, PartialEq, Eq)]
331#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
332pub struct GvasFile {
333    /// Game version
334    #[cfg_attr(
335        feature = "serde",
336        serde(default, skip_serializing_if = "DeserializedGameVersion::is_default")
337    )]
338    pub deserialized_game_version: DeserializedGameVersion,
339    /// GVAS file header.
340    pub header: GvasHeader,
341    /// GVAS properties.
342    pub properties: HashableIndexMap<String, Property>,
343}
344
345impl GvasFile {
346    /// Read GvasFile from a binary file
347    ///
348    /// # Errors
349    ///
350    /// If this function reads an invalid file it returns [`Error`]
351    ///
352    /// If this function reads a file which needs hints it returns [`DeserializeError::MissingHint`]
353    ///
354    /// [`DeserializeError::MissingHint`]: error/enum.DeserializeError.html#variant.MissingHint
355    ///
356    /// # Examples
357    ///
358    /// ```no_run
359    /// use gvas::{error::Error, GvasFile};
360    /// use std::fs::File;
361    /// use gvas::game_version::GameVersion;
362    ///
363    /// let mut file = File::open("save.sav")?;
364    /// let gvas_file = GvasFile::read(&mut file, GameVersion::Default);
365    ///
366    /// println!("{:#?}", gvas_file);
367    /// # Ok::<(), Error>(())
368    /// ```
369    pub fn read<R: Read + Seek>(cursor: &mut R, game_version: GameVersion) -> Result<Self, Error> {
370        let hints = HashMap::new();
371        Self::read_with_hints(cursor, game_version, &hints)
372    }
373
374    /// Read GvasFile from a binary file
375    ///
376    /// # Errors
377    ///
378    /// If this function reads an invalid file it returns [`Error`]
379    ///
380    /// If this function reads a file which needs a hint that is missing it returns [`DeserializeError::MissingHint`]
381    ///
382    /// [`DeserializeError::MissingHint`]: error/enum.DeserializeError.html#variant.MissingHint
383    ///
384    /// # Examples
385    ///
386    /// ```no_run
387    /// use gvas::{error::Error, GvasFile};
388    /// use std::{collections::HashMap, fs::File};
389    /// use gvas::game_version::GameVersion;
390    ///
391    /// let mut file = File::open("save.sav")?;
392    ///
393    /// let mut hints = HashMap::new();
394    /// hints.insert(
395    ///     "SeasonSave.StructProperty.Seasons.MapProperty.Key.StructProperty".to_string(),
396    ///     "Guid".to_string(),
397    /// );
398    ///
399    /// let gvas_file = GvasFile::read_with_hints(&mut file, GameVersion::Default, &hints);
400    ///
401    /// println!("{:#?}", gvas_file);
402    /// # Ok::<(), Error>(())
403    /// ```
404    pub fn read_with_hints<R: Read + Seek>(
405        cursor: &mut R,
406        game_version: GameVersion,
407        hints: &HashMap<String, String>,
408    ) -> Result<Self, Error> {
409        let deserialized_game_version: DeserializedGameVersion;
410        let mut cursor = match game_version {
411            GameVersion::Default => {
412                deserialized_game_version = DeserializedGameVersion::Default;
413                let mut data = Vec::new();
414                cursor.read_to_end(&mut data)?;
415                Cursor::new(data)
416            }
417            GameVersion::Palworld => {
418                let decompresed_length = cursor.read_u32::<LittleEndian>()?;
419                let _compressed_length = cursor.read_u32::<LittleEndian>()?;
420
421                let mut magic = [0u8; 3];
422                cursor.read_exact(&mut magic)?;
423                if &magic != PLZ_MAGIC {
424                    Err(DeserializeError::InvalidHeader(
425                        format!("Invalid PlZ magic {magic:?}").into_boxed_str(),
426                    ))?
427                }
428
429                let compression_type = cursor.read_enum()?;
430
431                deserialized_game_version = DeserializedGameVersion::Palworld(compression_type);
432
433                match compression_type {
434                    PalworldCompressionType::None => {
435                        let mut data = vec![0u8; decompresed_length as usize];
436
437                        cursor.read_exact(&mut data)?;
438                        Cursor::new(data)
439                    }
440                    PalworldCompressionType::Zlib => {
441                        let mut zlib_data = vec![0u8; decompresed_length as usize];
442
443                        let mut decoder = ZlibDecoder::new(cursor);
444                        decoder.read_exact(&mut zlib_data)?;
445
446                        Cursor::new(zlib_data)
447                    }
448                    PalworldCompressionType::ZlibTwice => {
449                        let decoder = ZlibDecoder::new(cursor);
450                        let mut decoder = ZlibDecoder::new(decoder);
451
452                        let mut zlib_data = Vec::new();
453                        decoder.read_to_end(&mut zlib_data)?;
454
455                        Cursor::new(zlib_data)
456                    }
457                }
458            }
459        };
460
461        let header = GvasHeader::read(&mut cursor)?;
462
463        let mut options = PropertyOptions {
464            hints,
465            properties_stack: &mut vec![],
466            custom_versions: header.get_custom_versions(),
467        };
468
469        let mut properties = HashableIndexMap::new();
470        loop {
471            let property_name = cursor.read_string()?;
472            if property_name == "None" {
473                break;
474            }
475
476            let property_type = cursor.read_string()?;
477
478            options.properties_stack.push(property_name.clone());
479
480            let property = Property::new(&mut cursor, &property_type, true, &mut options, None)?;
481            properties.insert(property_name, property);
482
483            let _ = options.properties_stack.pop();
484        }
485
486        Ok(GvasFile {
487            deserialized_game_version,
488            header,
489            properties,
490        })
491    }
492
493    /// Write GvasFile to a binary file
494    ///
495    /// # Errors
496    ///
497    /// If the file was modified in a way that makes it invalid this function returns [`Error`]
498    ///
499    /// # Examples
500    ///
501    /// ```no_run
502    /// use gvas::{error::Error, GvasFile};
503    /// use std::{
504    ///     fs::File,
505    ///     io::Cursor,
506    /// };
507    /// use gvas::game_version::GameVersion;
508    ///
509    /// let mut file = File::open("save.sav")?;
510    /// let gvas_file = GvasFile::read(&mut file, GameVersion::Default)?;
511    ///
512    /// let mut writer = Cursor::new(Vec::new());
513    /// gvas_file.write(&mut writer)?;
514    /// println!("{:#?}", writer.get_ref());
515    /// # Ok::<(), Error>(())
516    /// ```
517    pub fn write<W: Write + Seek>(&self, cursor: &mut W) -> Result<(), Error> {
518        let mut writing_cursor = Cursor::new(Vec::new());
519
520        self.header.write(&mut writing_cursor)?;
521
522        let mut options = PropertyOptions {
523            hints: &HashMap::new(),
524            properties_stack: &mut vec![],
525            custom_versions: self.header.get_custom_versions(),
526        };
527
528        for (name, property) in &self.properties {
529            writing_cursor.write_string(name)?;
530            property.write(&mut writing_cursor, true, &mut options)?;
531        }
532        writing_cursor.write_string("None")?;
533        writing_cursor.write_i32::<LittleEndian>(0)?; // padding
534
535        match self.deserialized_game_version {
536            DeserializedGameVersion::Default => cursor.write_all(&writing_cursor.into_inner())?,
537            DeserializedGameVersion::Palworld(compression_type) => {
538                let decompressed = writing_cursor.into_inner();
539
540                cursor.write_u32::<LittleEndian>(decompressed.len() as u32)?;
541                let compressed_length_pos = cursor.stream_position()?;
542                cursor.write_u32::<LittleEndian>(0)?; // Compressed length placeholder, will be updated later
543                cursor.write_all(PLZ_MAGIC)?;
544                cursor.write_enum(compression_type)?;
545
546                // Compress and write data directly to the output cursor
547                match compression_type {
548                    PalworldCompressionType::None => cursor.write_all(&decompressed)?,
549                    PalworldCompressionType::Zlib => {
550                        let mut encoder = ZlibEncoder::new(cursor.by_ref(), Compression::new(6));
551                        encoder.write_all(&decompressed)?;
552                        encoder.finish()?;
553                    }
554                    PalworldCompressionType::ZlibTwice => {
555                        let encoder = ZlibEncoder::new(cursor.by_ref(), Compression::default());
556                        let mut encoder = ZlibEncoder::new(encoder, Compression::default());
557                        encoder.write_all(&decompressed)?;
558                        encoder.finish()?;
559                    }
560                }
561
562                // Update compressed length
563                let end_pos = cursor.stream_position()?;
564                cursor.seek(SeekFrom::Start(compressed_length_pos))?;
565                cursor.write_u32::<LittleEndian>((end_pos - (compressed_length_pos + 4)) as u32)?;
566                cursor.seek(SeekFrom::Start(end_pos))?;
567            }
568        }
569        Ok(())
570    }
571}