From 42819615dc7bd613ccbe78c9e7e259f1f4b3c9d6 Mon Sep 17 00:00:00 2001 From: Krzysztof Saczuk Date: Sun, 17 Sep 2023 19:39:46 +0200 Subject: [PATCH] feat: add docs + last changes --- Cargo.lock | 9 ++- crates/nosapi_data/Cargo.toml | 2 +- crates/nosapi_data/src/client/error.rs | 3 + crates/nosapi_data/src/client/mod.rs | 38 ++++++++++++ crates/nosapi_data/src/lib.rs | 18 ++++++ crates/nosapi_data/src/nos/error.rs | 4 ++ crates/nosapi_data/src/nos/mod.rs | 2 + crates/nosapi_data/src/nos/text.rs | 85 ++++++++++++++++++++------ crates/nosapi_data/src/nos/type.rs | 17 ++++++ crates/nosapi_data/src/prelude.rs | 3 + crates/nosapi_data/src/traits.rs | 2 + crates/nosapi_data/tests/nos/text.rs | 8 +-- 12 files changed, 164 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fc7b3b..27e0d8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -863,13 +863,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] -name = "memmap" -version = "0.7.0" +name = "memmap2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" dependencies = [ "libc", - "winapi", ] [[package]] @@ -962,7 +961,7 @@ dependencies = [ "derivative", "lazy_static", "md5", - "memmap", + "memmap2", "pelite", "schemars", "serde", diff --git a/crates/nosapi_data/Cargo.toml b/crates/nosapi_data/Cargo.toml index bd87eca..a88f74b 100644 --- a/crates/nosapi_data/Cargo.toml +++ b/crates/nosapi_data/Cargo.toml @@ -23,7 +23,7 @@ thiserror = "1.0.47" [dev-dependencies] color-eyre = "0.6.2" -memmap = "0.7.0" +memmap2 = "0.7.1" sha1_smol = { version = "1.0.0", features = ["std"] } [features] diff --git a/crates/nosapi_data/src/client/error.rs b/crates/nosapi_data/src/client/error.rs index 3ec0003..8533012 100644 --- a/crates/nosapi_data/src/client/error.rs +++ b/crates/nosapi_data/src/client/error.rs @@ -1,3 +1,6 @@ +//! Contains error types returned by functions in the [`client`](crate::client) module + +/// An error that occurs when extracting the [client version](crate::client::ClientVersion) #[derive(thiserror::Error, Copy, Clone, Debug, Eq, PartialEq, Hash)] pub enum ClientVersionError { #[error("Invalid file provided, expected a valid pe32 file")] diff --git a/crates/nosapi_data/src/client/mod.rs b/crates/nosapi_data/src/client/mod.rs index 94a8324..f7efbe8 100644 --- a/crates/nosapi_data/src/client/mod.rs +++ b/crates/nosapi_data/src/client/mod.rs @@ -1,26 +1,64 @@ +//! Contains functions related to the game's client EXEs + use pelite::pe32::{Pe, PeFile}; use crate::client::error::ClientVersionError; pub mod error; +/// Stores MD5 hashes of the game's client EXEs #[derive(Clone, Eq, PartialEq, Debug)] #[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct ClientHashes { + /// MD5 hash of `NostaleClientX.exe` pub client_x: String, + /// MD5 hash of `NostaleClient.exe` pub client_gl: String, } +/// Represents the game's client version and hashes of its EXEs #[derive(Clone, Eq, PartialEq, Debug)] #[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ClientVersion { + /// Game's client version pub version: String, + /// MD5 hashes of game's client EXEs pub hashes: ClientHashes, } +/// Resolves a client version and computes MD5 hashes of game's client EXEs +/// +/// # Examples +/// ``` +/// use nosapi_data::client::get_client_version; +/// use std::error::Error; +/// use std::fs::File; +/// use std::io::Read; +/// +/// fn main() -> Result<(), Box> { +/// let mut client_x = vec![]; +/// let mut client_gl = vec![]; +/// +/// File::open("./tests/fixtures/NostaleClientX.exe")? +/// .read_to_end(&mut client_x)?; +/// File::open("./tests/fixtures/NostaleClient.exe")? +/// .read_to_end(&mut client_gl)?; +/// +/// if let Some(result) = get_client_version(&client_x, &client_gl)? { +/// println!("{:?}", result.version); // 0.9.3.3202 +/// } +/// +/// Ok(()) +/// } +/// ``` +/// +/// # Errors +/// - [`InvalidFile`](ClientVersionError::InvalidFile) - Specified file is not a valid [PE32 file](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format) +/// - [`NoResources`](ClientVersionError::NoResources) - Specified file doesn't contain the [`resources`](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-rsrc-section) section +/// - [`UnableToFindVersionInfo`](ClientVersionError::UnableToFindVersionInfo) - The version information was not found in the EXE pub fn get_client_version + ?Sized, GL: AsRef<[u8]> + ?Sized>( client_x: &X, client_gl: &GL, diff --git a/crates/nosapi_data/src/lib.rs b/crates/nosapi_data/src/lib.rs index eeeda50..c032efc 100644 --- a/crates/nosapi_data/src/lib.rs +++ b/crates/nosapi_data/src/lib.rs @@ -1,3 +1,21 @@ +//! Library for working with [NosTale's](https://gameforge.com/pl-PL/play/nostale) client files. +//! +//! # Create features +//! This library uses a set of [feature flags](https://doc.rust-lang.org/cargo/reference/features.html#the-features-section) to reduce the amount of compiled code. +//! It is possible to just enable certain features over other. +//! By default, +//! this crate does not enable any features but allows one to enable a subset for their use case. +//! Below is a list of the available feature flags. +//! +//! | Feature Name | Description | +//! |-|-| +//! | `serde` | Adds support for serialization using the [`serde`](https://crates.io/crates/serde) crate | +//! | `json_schema` | Adds support for generating JSON schemas using the [`schemars`](https://crates.io/crates/schemars) crate | +#![doc( + html_logo_url = "https://raw.githubusercontent.com/zakuciael/nosapi/main/assets/logo.png", + html_favicon_url = "https://raw.githubusercontent.com/zakuciael/nosapi/main/assets/logo.png" +)] + pub mod client; pub mod nos; pub mod prelude; diff --git a/crates/nosapi_data/src/nos/error.rs b/crates/nosapi_data/src/nos/error.rs index 280ac5b..87ea2cc 100644 --- a/crates/nosapi_data/src/nos/error.rs +++ b/crates/nosapi_data/src/nos/error.rs @@ -1,9 +1,13 @@ +//! Contains error types returned by functions in the [`nos`](crate::nos) module + use crate::nos::NOSFileType; +/// An error that occurs when parsing the [file type](NOSFileType) #[derive(thiserror::Error, Debug)] #[error("{0}")] pub struct NOSFileHeaderError(pub String); +/// An error that occurs when parsing the `.NOS` file #[derive(thiserror::Error, Debug)] pub enum NOSFileError { #[error("Invalid file header detected")] diff --git a/crates/nosapi_data/src/nos/mod.rs b/crates/nosapi_data/src/nos/mod.rs index bc24782..1e078ee 100644 --- a/crates/nosapi_data/src/nos/mod.rs +++ b/crates/nosapi_data/src/nos/mod.rs @@ -1,3 +1,5 @@ +//! Contains parsers for different types of `.NOS` files + pub use r#type::*; pub use text::*; diff --git a/crates/nosapi_data/src/nos/text.rs b/crates/nosapi_data/src/nos/text.rs index 045dfef..3845de4 100644 --- a/crates/nosapi_data/src/nos/text.rs +++ b/crates/nosapi_data/src/nos/text.rs @@ -11,36 +11,78 @@ use crate::traits::ReadExt; static OLE_TIME_CHECK: [u8; 4] = [0xEE, 0x3E, 0x32, 0x01]; +/// Represents a parsed `.NOS` text file. +/// +/// # Examples +/// Here is an example of loading the `NSgtdData.NOS` +/// using the [`memmap2`](https://crates.io/crates/memmap2) crate +/// ``` +/// use std::error::Error; +/// use std::fs::File; +/// use std::io::Read; +/// use nosapi_data::nos::NOSTextFile; +/// use memmap2::Mmap; +/// +/// fn main() -> Result<(), Box> { +/// let file = File::open("./tests/fixtures/NSgtdData.NOS")?; +/// let mmap = unsafe { Mmap::map(&file)? }; +/// let result = NOSTextFile::from_bytes(&mmap)?; +/// +/// println!("{:?}", result); +/// Ok(()) +/// } +/// ``` #[derive(Clone, Eq, Debug, PartialEq, Hash)] #[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct NOSTextFile { + /// The number of file entries contained in the `.NOS` file. pub file_count: i32, + /// A vector of [`NOSTextFileEntry`] structs, representing the file entries within the `.NOS` file. pub files: Vec, - pub ole_time: Option>, + /// The last file modification timestamp, if available. + pub last_modified_date: Option>, } +/// Represents an file entry within a `.NOS` text file. #[derive(Clone, Eq, Derivative, PartialEq, Hash)] #[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] #[derivative(Debug)] pub struct NOSTextFileEntry { + /// Stores the file number, that indicates the position of this file inside the file list. pub file_number: i32, + /// Represents the name of this file. pub name: String, + /// Indicates whether the file is encrypted as `.dat` file or not. pub is_dat: bool, + /// Stores the size of the file in bytes. pub size: i32, + /// Decrypted raw content of the file #[derivative(Debug = "ignore")] pub raw_content: Vec, } impl NOSTextFile { + /// Creates a [`NOSTextFile`] instance from a byte slice + /// + /// # Errors + /// - [`InvalidFileHeader`](NOSFileError::InvalidFileHeader) - An invalid file header was detected. + /// - [`InvalidFileType`](NOSFileError::InvalidFileType) - The file is of a wrong file type. + /// - [`IO`](NOSFileError::IO) - An error was encountered while reading the file. pub fn from_bytes + ?Sized>(bytes: &T) -> Result { Self::from_reader(&mut Cursor::new(bytes.as_ref())) } - fn from_reader(reader: &mut T) -> Result { + /// Creates a [`NOSTextFile`] instance from a reader implementing [`Read`] and [`Seek`] traits. + /// + /// # Errors + /// - [`InvalidFileHeader`](NOSFileError::InvalidFileHeader) - An invalid file header was detected. + /// - [`InvalidFileType`](NOSFileError::InvalidFileType) - The file is of a wrong file type. + /// - [`IO`](NOSFileError::IO) - An error was encountered while reading the file. + pub fn from_reader(reader: &mut T) -> Result { let file_type = NOSFileType::from_reader(reader)?; if file_type != NOSFileType::Text { @@ -58,34 +100,43 @@ impl NOSTextFile { files.push(NOSTextFileEntry::from_reader(reader)?); } - let mut ole_time = [0u8; 12]; - let ole_time = if reader.read(&mut ole_time)? == 12 && ole_time[8..12] == OLE_TIME_CHECK { - let variant = &ole_time[0..8].try_into().map(f64::from_le_bytes).ok(); - - variant - .map(|variant| { - let unix_timestamp = (-2208988800f64 + ((variant - 2.00001) * 86400f64)) as i64; - Utc.timestamp_opt(unix_timestamp, 0).single() - }) - .unwrap_or(None) - } else { - None - }; + let mut raw_ole_time = [0u8; 12]; + let last_modified_date = + if reader.read(&mut raw_ole_time)? == 12 && raw_ole_time[8..12] == OLE_TIME_CHECK { + let variant = &raw_ole_time[0..8].try_into().map(f64::from_le_bytes).ok(); + + variant + .map(|variant| { + let unix_timestamp = (-2208988800f64 + ((variant - 2.00001) * 86400f64)) as i64; + Utc.timestamp_opt(unix_timestamp, 0).single() + }) + .unwrap_or(None) + } else { + None + }; Ok(Self { file_count, files, - ole_time, + last_modified_date, }) } } impl NOSTextFileEntry { + /// Creates a [`NOSTextFileEntry`] instance from a byte slice. + /// + /// # Errors + /// This method can return an [`IO`](NOSFileError::IO) error if it fails to read the file. pub fn from_bytes + ?Sized>(bytes: &T) -> Result { Self::from_reader(&mut Cursor::new(bytes.as_ref())) } - pub fn from_reader(reader: &mut T) -> Result { + /// Creates a [`NOSTextFileEntry`] instance from a reader implementing [`Read`] trait. + /// + /// # Errors + /// This method can return an [`IO`](NOSFileError::IO) error if it fails to read the file. + pub fn from_reader(reader: &mut T) -> Result { let file_number = reader.read_i32::()?; let name_size = reader.read_i32::()? as usize; let name = reader.read_to_utf8_sized(name_size)?; diff --git a/crates/nosapi_data/src/nos/type.rs b/crates/nosapi_data/src/nos/type.rs index 2beb61a..fcd9a37 100644 --- a/crates/nosapi_data/src/nos/type.rs +++ b/crates/nosapi_data/src/nos/type.rs @@ -5,6 +5,7 @@ use crate::nos::error::NOSFileHeaderError; static DATA_FILE_HEADER: [&str; 3] = ["NT Data", "32GBS V1.0", "ITEMS V1.0"]; static CCINF_FILE_HEADER: &str = "CCINF V1.20"; +/// Represents a file type of the `.NOS` file #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] pub enum NOSFileType { Data(Option), @@ -13,6 +14,10 @@ pub enum NOSFileType { } impl NOSFileType { + /// Resolves a file type from a byte slice + /// + /// # Errors + /// This method can return an error if the specified byte slice has an invalid length pub fn from_bytes + ?Sized>(bytes: &T) -> Result { let bytes = bytes.as_ref(); @@ -36,6 +41,11 @@ impl NOSFileType { }) } + /// Resolves a file type from a reader + /// + /// # Errors + /// This method can return an error + /// if it failed to read the bytes required to resolve the file type pub fn from_reader(reader: &mut T) -> Result { let mut buf = [0u8; 0x0B]; reader @@ -46,6 +56,7 @@ impl NOSFileType { } } +/// Represents a type of data file (available only if the file type is [NOSFileType::Data]) #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] #[repr(usize)] pub enum NOSDataType { @@ -75,6 +86,11 @@ pub enum NOSDataType { } impl NOSDataType { + /// Resolves a data type from a file header string + /// + /// # Errors + /// This method can return an error + /// if the header string doesn't contain a valid [usize] integer at the end of it pub fn from_header(header: &str) -> Result, NOSFileHeaderError> { match header { _ if &header[0..7] == DATA_FILE_HEADER[0] => { @@ -90,6 +106,7 @@ impl NOSDataType { } } + /// Resolves a data type from a [usize] integer pub fn from_raw(raw: usize) -> Option { match raw { _ if raw == Self::PCHPKG as usize => Some(Self::PCHPKG), diff --git a/crates/nosapi_data/src/prelude.rs b/crates/nosapi_data/src/prelude.rs index 0520546..cbc9683 100644 --- a/crates/nosapi_data/src/prelude.rs +++ b/crates/nosapi_data/src/prelude.rs @@ -1,2 +1,5 @@ +//! Contains a collection of names +//! that should be automatically brought into the scope for an ease of use + pub use crate::client::error::*; pub use crate::nos::error::*; diff --git a/crates/nosapi_data/src/traits.rs b/crates/nosapi_data/src/traits.rs index 7d2a0c7..9658b9a 100644 --- a/crates/nosapi_data/src/traits.rs +++ b/crates/nosapi_data/src/traits.rs @@ -1,3 +1,5 @@ +//! Contains traits used inside this crate + use std::io; use std::io::Read; diff --git a/crates/nosapi_data/tests/nos/text.rs b/crates/nosapi_data/tests/nos/text.rs index 6197d08..8534508 100644 --- a/crates/nosapi_data/tests/nos/text.rs +++ b/crates/nosapi_data/tests/nos/text.rs @@ -1,7 +1,7 @@ use std::fs::File; use color_eyre::eyre::WrapErr; -use memmap::Mmap; +use memmap2::Mmap; use sha1_smol::Sha1; use nosapi_data::nos::error::NOSFileError; @@ -55,9 +55,9 @@ fn correctly_parses_ole_time() { .wrap_err_with(|| format!("Failed to parse file {}", &FILE_PATH)) .unwrap(); - assert!(file.ole_time.is_some()); + assert!(file.last_modified_date.is_some()); assert_eq!( - file.ole_time.unwrap().to_string(), + file.last_modified_date.unwrap().to_string(), "2023-08-23 15:27:04 UTC" ) } @@ -79,7 +79,7 @@ fn correctly_parses_entry_metadata() { name: "kr_abuse.lst".to_owned(), is_dat: false, size: 0, - raw_content: vec![] + raw_content: vec![], } ) }