donet_core/
lib.rs

1/*
2    This file is part of Donet.
3
4    Copyright © 2024 Max Rodriguez <[email protected]>
5
6    Donet is free software; you can redistribute it and/or modify
7    it under the terms of the GNU Affero General Public License,
8    as published by the Free Software Foundation, either version 3
9    of the License, or (at your option) any later version.
10
11    Donet is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14    GNU Affero General Public License for more details.
15
16    You should have received a copy of the GNU Affero General Public
17    License along with Donet. If not, see <https://www.gnu.org/licenses/>.
18*/
19
20//! # donet-core
21//! Provides the necessary utilities and definitions for using the Donet networking protocol.
22//!
23//! These utilities include a lexer, parser, and high-level representation of the parsed DC
24//! file, as well as creating datagrams, iterating through datagrams, and the definition of
25//! every message type in the Donet protocol.
26//!
27//! ### Getting Started
28//! The recommended way to get started is to enable all features.
29//! Do this by enabling the `full` feature flag:
30//! ```toml
31//! donet-core = { version = "0.1.0", features = ["full"] }
32//! ```
33//!
34//! ### Feature Flags
35//! The crate provides a set of feature flags to reduce the amount of compiled code.
36//! It is possible to just enable certain features over others.
37//! Below is a list of the available feature flags.
38//!
39//! - **`full`**: Enables all feature flags available for donet-core.
40//! - **`datagram`**: Includes Datagram / Datagram Iterator source for writing network packets.
41//! - **`dcfile`**: Includes the DC file lexer, parser, and DC element structures.
42//!
43//! You can return to the Donet manual at [`docs.donet-server.org`].
44//!
45//! [`docs.donet-server.org`]: https://docs.donet-server.org/
46
47#![doc(
48    html_logo_url = "https://gitlab.com/donet-server/donet/-/raw/master/logo/donet_logo_v3.png?ref_type=heads"
49)]
50#![allow(clippy::module_inception)]
51//#![warn(missing_docs)]
52#![deny(unused_extern_crates)]
53
54pub mod globals;
55pub mod protocol;
56
57// Re-export of protocol module
58pub use protocol::*;
59
60#[macro_use]
61extern crate cfg_if;
62
63#[cfg(feature = "datagram")]
64pub mod datagram;
65
66cfg_if! {
67    if #[cfg(feature = "dcfile")] {
68        mod parser;
69        pub mod dcarray;
70        pub mod dcatomic;
71        pub mod dcfield;
72        pub mod dcfile;
73        pub mod dckeyword;
74        pub mod dclass;
75        pub mod dcmolecular;
76        pub mod dcnumeric;
77        pub mod dconfig;
78        pub mod dcparameter;
79        pub mod dcstruct;
80        pub mod dcswitch;
81        pub mod dctype;
82        mod hashgen;
83
84        use anyhow::Result;
85        use dcfile::DCFile;
86        use parser::error::DCReadError;
87    }
88}
89
90/// Returns false if a [`log`] logger is not initialized.
91///
92/// [`log`]: https://docs.rs/log/latest/log/
93///
94#[cfg(feature = "dcfile")]
95fn logger_initialized() -> bool {
96    use log::Level::*;
97
98    let levels: &[log::Level] = &[Error, Warn, Info, Debug, Trace];
99
100    for level in levels {
101        if log::log_enabled!(*level) {
102            return true;
103        }
104    }
105    false
106}
107
108/// Creates a [`pretty_env_logger`] logger if no [`log`]
109/// logger is found to be initialized in this process.
110///
111/// [`pretty_env_logger`]: https://docs.rs/pretty_env_logger/latest/pretty_env_logger/
112/// [`log`]: https://docs.rs/log/latest/log/
113///
114#[cfg(feature = "dcfile")]
115fn init_logger() {
116    if logger_initialized() {
117        return;
118    }
119    pretty_env_logger::init();
120}
121
122/// Easy to use interface for the DC file parser. Handles reading
123/// the DC files, instantiating the DC parsing pipeline, and either
124/// returns the DCFile object or a Parse/File error.
125#[cfg(feature = "dcfile")]
126pub fn read_dc_files<'a>(
127    config: dconfig::DCFileConfig,
128    file_paths: Vec<String>,
129) -> Result<DCFile<'a>, DCReadError> {
130    use log::{info, warn};
131    use parser::InputFile;
132    use std::fs::File;
133    use std::io::{Error, ErrorKind, Read};
134    use std::path::Path;
135
136    init_logger();
137    info!("DC read of {:?}", file_paths);
138
139    let mut filenames: Vec<String> = vec![];
140    let mut file_results: Vec<Result<File, std::io::Error>> = vec![];
141    let mut pipeline_input: Vec<parser::InputFile> = vec![];
142
143    if file_paths.is_empty() {
144        warn!("No DC files given! Using empty DC file.");
145        return read_dc(config, String::default());
146    }
147
148    for file_path in &file_paths {
149        // Get filename from given path
150        match Path::new(file_path).file_name() {
151            Some(filename_osstr) => {
152                // Convert OsStr to String and store filename
153                filenames.push(filename_osstr.to_string_lossy().into_owned());
154            }
155            None => {
156                // std::path::Path.file_name() **only** returns `None`
157                // if the path terminates in '..'.
158                let filename_err: Error = Error::new(
159                    ErrorKind::InvalidInput,
160                    "Failed to get filename from path because\
161                    path terminates in '..'.",
162                );
163                return Err(DCReadError::IO(filename_err));
164            }
165        }
166
167        // Open file using path and store result
168        file_results.push(File::open(file_path));
169    }
170
171    for (index, io_result) in file_results.into_iter().enumerate() {
172        if let Ok(mut dcf) = io_result {
173            // Prepare `InputFile` tuple for the pipeline function.
174            let filename: String = filenames.get(index).unwrap().to_owned();
175            let mut in_file: InputFile = (filename, String::default());
176
177            let res: std::io::Result<usize> = dcf.read_to_string(&mut in_file.1);
178
179            if let Err(res_err) = res {
180                // DC file content may not be in proper UTF-8 encoding.
181                return Err(DCReadError::IO(res_err));
182            }
183            pipeline_input.push(in_file);
184        } else {
185            // Failed to open one of the DC files. (most likely permission error)
186            return Err(DCReadError::IO(io_result.unwrap_err()));
187        }
188    }
189
190    parser::dcparse_pipeline(config, pipeline_input)
191}
192
193/// Front end to the donet-core DC parser pipeline.
194///
195/// ## Example Usage
196/// The following is an example of parsing a simple DC file string,
197/// printing its DC hash in hexadecimal notation, and accessing
198/// the elements of a defined Distributed Class:
199/// ```rust
200/// use donet_core::dcfile::DCFile;
201/// use donet_core::dclass::DClass;
202/// use donet_core::dconfig::*;
203/// use donet_core::read_dc;
204///
205/// let dc_file = "
206///
207/// from game.ai import AnonymousContact/UD
208/// from game.ai import LoginManager/AI
209/// from game.world import DistributedWorld/AI
210/// from game.avatar import DistributedAvatar/AI/OV
211///
212/// typedef uint32 doId;
213/// typedef uint32 zoneId;
214/// typedef uint64 channel;
215///
216/// dclass AnonymousContact {
217///   login(string username, string password) clsend airecv;
218/// };
219///
220/// dclass LoginManager {
221///   login(channel client, string username, string password) airecv;
222/// };
223///
224/// dclass DistributedWorld {
225///   create_avatar(channel client) airecv;
226/// };
227///
228/// dclass DistributedAvatar {
229///   set_xyzh(int16 x, int16 y, int16 z, int16 h) broadcast required;
230///   indicate_intent(int16 / 10, int16 / 10) ownsend airecv;
231/// };
232///
233/// ";
234///
235/// let dc_conf = DCFileConfig::default();
236/// let dc_read = read_dc(dc_conf, dc_file.into());
237///
238/// if let Ok(dc_file) = dc_read {
239///     // Print the DC File's 32-bit hash in hexadecimal format.
240///     println!("{}", dc_file.get_pretty_hash());
241///
242///     // TODO: Retrieve the `DistributedAvatar` dclass by ID.
243///     //let class: &DClass = dc_file.get_dclass_by_id(3);
244///
245///     // TODO: Print the identifier of the dclass.
246///     //println!("{}", class.get_name());
247/// }
248/// ```
249///
250/// The output of the program would be the following:
251/// ```txt
252/// 0x9c737148
253/// DistributedAvatar
254/// ```
255/// <br><img src="https://c.tenor.com/myQHgyWQQ9sAAAAd/tenor.gif">
256///
257#[cfg(feature = "dcfile")]
258pub fn read_dc<'a>(config: dconfig::DCFileConfig, input: String) -> Result<DCFile<'a>, DCReadError> {
259    let dcparse_input: Vec<parser::InputFile> = vec![("input.dc".to_string(), input)];
260
261    parser::dcparse_pipeline(config, dcparse_input)
262}