From 8982ae35b89f975fe947feadb1a598b534a21e23 Mon Sep 17 00:00:00 2001 From: stadust <43299462+stadust@users.noreply.github.com> Date: Sat, 21 Oct 2023 21:52:56 +0100 Subject: [PATCH 1/2] Implement rudimentary plist deserializer RobTop uses the plist (a variant of XML) for storing local game data. Supporting this format in dash-rs will allows us to successfully parse and manipulate the game's save files. --- Cargo.toml | 1 + src/serde/de/mod.rs | 8 ++ src/serde/de/plist.rs | 317 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 326 insertions(+) create mode 100644 src/serde/de/plist.rs diff --git a/Cargo.toml b/Cargo.toml index 0397f77..e061b08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ flate2 = {version = "1.0.14", features = ["zlib"], default-features=false} variant_partial_eq = { git = "https://github.com/stadust/variant-partial-eq" } thiserror = "1.0.51" dash-rs-derive = { path = "dash-rs-derive" } +quick-xml = "0.28.2" [dev-dependencies] # benchmark diff --git a/src/serde/de/mod.rs b/src/serde/de/mod.rs index fc7ea50..cdaf3ab 100644 --- a/src/serde/de/mod.rs +++ b/src/serde/de/mod.rs @@ -2,5 +2,13 @@ //! //! All of these deserializers have the goal to use zero-allocations for maximum efficiency +use crate::serde::IndexedDeserializer; +use serde::{ + de::{MapAccess, Visitor}, + Deserializer, +}; +use std::{collections::HashMap, fmt::Formatter}; + pub mod error; pub mod indexed; +pub mod plist; diff --git a/src/serde/de/plist.rs b/src/serde/de/plist.rs new file mode 100644 index 0000000..8c4db04 --- /dev/null +++ b/src/serde/de/plist.rs @@ -0,0 +1,317 @@ +use std::fmt::{Debug, Display, Formatter, write}; +use std::ops::Deref; +use quick_xml::events::Event; +use quick_xml::Reader; +use serde::{de::Visitor, Deserializer}; +use std::borrow::Cow; + +#[derive(Debug)] +pub enum Error<'de> { + Unexpected { + got: Event<'de>, + expected: String + }, + Custom(String), + Xml(quick_xml::Error) +} + +impl<'de> std::error::Error for Error<'de> {} + +impl<'de> Display for Error<'de> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Error::Unexpected { got, expected } => write!(f, "expected {}, got {:?}", expected, got), + Error::Custom(msg) => write!(f, "{}", msg), + Error::Xml(inner) => write!(f, "{}", inner) + } + } +} + +impl<'de> serde::de::Error for Error<'de> { + fn custom(msg: T) -> Self where T: Display { + Error::Custom(msg.to_string()) + } +} + +impl From for Error<'_> { + fn from(value: quick_xml::Error) -> Self { + Error::Xml(value) + } +} + +pub struct PlistDeserializer<'de> { + reader: Reader<&'de [u8]>, +} + +impl<'de> PlistDeserializer<'de> { + pub fn new(input: &'de str) -> Result> { + let mut deserializer = PlistDeserializer { + reader: Reader::from_str(input) + }; + + match deserializer.reader.read_event()? { + Event::Decl(_) => (), + event => return Err(Error::Unexpected {got: event, expected: String::from("")}) + } + + deserializer.expect_tag_start("plist")?; + deserializer.expect_tag_start("dict")?; + + Ok(deserializer) + } + + fn expect_tag_start(&mut self, tag: &str) -> Result<(), Error<'de>> { + match self.reader.read_event()? { + Event::Start(bytes) if bytes.name().0 == tag.as_bytes() => Ok(()), + event => Err(Error::Unexpected { got: event, expected: format!("<{} ...>", tag) }) + } + } + + fn expect_text(&mut self) -> Result, Error<'de>> { + match self.reader.read_event()? { + Event::Text(text) => Ok(text.into_inner()), + event => Err(Error::Unexpected {got: event, expected: String::from("text content")}) + } + } + + fn expect_tag_end(&mut self, tag: &str) -> Result<(), Error<'de>> { + match self.reader.read_event()? { + Event::End(bytes) if bytes.name().0 == tag.as_bytes() => Ok(()), + event => Err(Error::Unexpected { got: event, expected: format!("", tag) }) + } + } + + fn expect_simple_tag(&mut self, tag: &str) -> Result, Error<'de>> { + self.expect_tag_start(tag)?; + let content = self.expect_text()?; + self.expect_tag_end(tag)?; + Ok(content) + } + + fn expect_key(&mut self) -> Result, Error<'de>> { + self.expect_simple_tag("k") + } +} + +impl<'a, 'de> Deserializer<'de> for &'a mut PlistDeserializer<'de> { + type Error = Error<'de>; + + fn deserialize_any(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_bool(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_i8(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_i16(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_i32(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_i64(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_u8(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_u16(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_u32(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_u64(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_f32(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_f64(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_char(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_str(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_string(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_bytes(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_byte_buf(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_option(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_unit(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_unit_struct(self, name: &'static str, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_newtype_struct(self, name: &'static str, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_seq(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_tuple(self, len: usize, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_tuple_struct(self, name: &'static str, len: usize, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_map(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_struct( + self, name: &'static str, fields: &'static [&'static str], visitor: V, + ) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_enum( + self, name: &'static str, variants: &'static [&'static str], visitor: V, + ) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_identifier(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } + + fn deserialize_ignored_any(self, visitor: V) -> Result<>::Value, Self::Error> + where + V: Visitor<'de>, + { + todo!() + } +} + +#[cfg(test)] +mod tests { + use crate::serde::de::plist::PlistDeserializer; + + const INPUT: &str = r#"134"#; + + #[test] + fn test_deserialize() { + let mut deserializer = PlistDeserializer::new(INPUT).unwrap(); + } +} \ No newline at end of file From 312c4fa70c1374e46b338e05e655294472fda194 Mon Sep 17 00:00:00 2001 From: stadust <43299462+stadust@users.noreply.github.com> Date: Sat, 21 Oct 2023 22:01:21 +0100 Subject: [PATCH 2/2] Rudimentary module for interacting with local savefiles So far only contains a single function for decoding CCGameManager.dat. --- src/lib.rs | 1 + src/savefile.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/savefile.rs diff --git a/src/lib.rs b/src/lib.rs index abff521..736a80d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,5 +5,6 @@ pub mod request; pub mod response; pub(crate) mod serde; pub mod util; +pub mod savefile; pub use crate::serde::{Dash, DeError, GJFormat, IndexedDeserializer, IndexedSerializer, ProcessError, SerError, Thunk, ThunkProcessor}; diff --git a/src/savefile.rs b/src/savefile.rs new file mode 100644 index 0000000..a736a7c --- /dev/null +++ b/src/savefile.rs @@ -0,0 +1,38 @@ +//! Module containing utilities for interacting with Geometry Dash's local save files + +use std::{path::Path, io::Read}; + +use base64::{DecodeError, engine::general_purpose::URL_SAFE, Engine}; +use flate2::read::GzDecoder; + +use crate::util; + +pub enum SavefileError { + Io(std::io::Error), + Base64(DecodeError) +} + +/// Reads the given path to a CCGameManager.dat file and decodes it. +pub fn load_cc_game_manager(p: impl AsRef) -> Result { + let mut cc_game_manager_bytes = std::fs::read_to_string(p) + .map_err(SavefileError::Io)? + .into_bytes(); + + util::cyclic_xor(&mut cc_game_manager_bytes, &[0xB]); + + // Spurious nul-terminators at end of string, kinda scary + while cc_game_manager_bytes.last() == Some(&0) { + cc_game_manager_bytes.pop(); + } + + let decoded = URL_SAFE.decode(&cc_game_manager_bytes) + .map_err(SavefileError::Base64)?; + let mut decoder = GzDecoder::new(&decoded[..]); + + let mut result = String::new(); + + decoder.read_to_string(&mut result) + .map_err(SavefileError::Io)?; + + Ok(result) +}