Skip to content

Commit

Permalink
feat: add docs + last changes
Browse files Browse the repository at this point in the history
  • Loading branch information
zakuciael committed Sep 17, 2023
1 parent c6a679d commit 4281961
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 27 deletions.
9 changes: 4 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/nosapi_data/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 3 additions & 0 deletions crates/nosapi_data/src/client/error.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down
38 changes: 38 additions & 0 deletions crates/nosapi_data/src/client/mod.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Error>> {
/// 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<X: AsRef<[u8]> + ?Sized, GL: AsRef<[u8]> + ?Sized>(
client_x: &X,
client_gl: &GL,
Expand Down
18 changes: 18 additions & 0 deletions crates/nosapi_data/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 4 additions & 0 deletions crates/nosapi_data/src/nos/error.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down
2 changes: 2 additions & 0 deletions crates/nosapi_data/src/nos/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Contains parsers for different types of `.NOS` files

pub use r#type::*;
pub use text::*;

Expand Down
85 changes: 68 additions & 17 deletions crates/nosapi_data/src/nos/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Error>> {
/// 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<NOSTextFileEntry>,
pub ole_time: Option<DateTime<Utc>>,
/// The last file modification timestamp, if available.
pub last_modified_date: Option<DateTime<Utc>>,
}

/// 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<u8>,
}

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<T: AsRef<[u8]> + ?Sized>(bytes: &T) -> Result<Self, NOSFileError> {
Self::from_reader(&mut Cursor::new(bytes.as_ref()))
}

fn from_reader<T: Read + Seek>(reader: &mut T) -> Result<Self, NOSFileError> {
/// 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<T: Read + Seek>(reader: &mut T) -> Result<Self, NOSFileError> {
let file_type = NOSFileType::from_reader(reader)?;

if file_type != NOSFileType::Text {
Expand All @@ -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<T: AsRef<[u8]> + ?Sized>(bytes: &T) -> Result<Self, NOSFileError> {
Self::from_reader(&mut Cursor::new(bytes.as_ref()))
}

pub fn from_reader<T: Read + Seek>(reader: &mut T) -> Result<Self, NOSFileError> {
/// 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<T: Read>(reader: &mut T) -> Result<Self, NOSFileError> {
let file_number = reader.read_i32::<LittleEndian>()?;
let name_size = reader.read_i32::<LittleEndian>()? as usize;
let name = reader.read_to_utf8_sized(name_size)?;
Expand Down
17 changes: 17 additions & 0 deletions crates/nosapi_data/src/nos/type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NOSDataType>),
Expand All @@ -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<T: AsRef<[u8]> + ?Sized>(bytes: &T) -> Result<Self, NOSFileHeaderError> {
let bytes = bytes.as_ref();

Expand All @@ -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<T: Read + Seek>(reader: &mut T) -> Result<Self, NOSFileHeaderError> {
let mut buf = [0u8; 0x0B];
reader
Expand All @@ -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 {
Expand Down Expand Up @@ -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<Option<Self>, NOSFileHeaderError> {
match header {
_ if &header[0..7] == DATA_FILE_HEADER[0] => {
Expand All @@ -90,6 +106,7 @@ impl NOSDataType {
}
}

/// Resolves a data type from a [usize] integer
pub fn from_raw(raw: usize) -> Option<Self> {
match raw {
_ if raw == Self::PCHPKG as usize => Some(Self::PCHPKG),
Expand Down
3 changes: 3 additions & 0 deletions crates/nosapi_data/src/prelude.rs
Original file line number Diff line number Diff line change
@@ -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::*;
2 changes: 2 additions & 0 deletions crates/nosapi_data/src/traits.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Contains traits used inside this crate

use std::io;
use std::io::Read;

Expand Down
8 changes: 4 additions & 4 deletions crates/nosapi_data/tests/nos/text.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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"
)
}
Expand All @@ -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![],
}
)
}
Expand Down

0 comments on commit 4281961

Please sign in to comment.