donet_core/
lib.rs

1/*
2    This file is part of Donet.
3
4    Copyright © 2024-2025 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 dcparameter;
78        pub mod dcstruct;
79        pub mod dcswitch;
80        pub mod dctype;
81        mod hashgen;
82
83        use anyhow::Result;
84        use dcfile::DCFile;
85        use parser::error::DCReadError;
86    }
87}
88
89/// Returns false if a [`log`] logger is not initialized.
90///
91/// [`log`]: https://docs.rs/log/latest/log/
92///
93#[cfg(feature = "dcfile")]
94fn logger_initialized() -> bool {
95    use log::Level::*;
96
97    let levels: &[log::Level] = &[Error, Warn, Info, Debug, Trace];
98
99    for level in levels {
100        if log::log_enabled!(*level) {
101            return true;
102        }
103    }
104    false
105}
106
107/// Creates a [`pretty_env_logger`] logger if no [`log`]
108/// logger is found to be initialized in this process.
109///
110/// [`pretty_env_logger`]: https://docs.rs/pretty_env_logger/latest/pretty_env_logger/
111/// [`log`]: https://docs.rs/log/latest/log/
112///
113#[cfg(feature = "dcfile")]
114fn init_logger() {
115    if logger_initialized() {
116        return;
117    }
118    pretty_env_logger::init();
119}
120
121/// Easy to use interface for the DC file parser. Handles reading
122/// the DC files, instantiating the DC parsing pipeline, and either
123/// returns the DCFile object or a Parse/File error.
124#[cfg(feature = "dcfile")]
125pub fn read_dc_files(file_paths: Vec<String>) -> Result<DCFile, DCReadError> {
126    use log::{info, warn};
127    use parser::InputFile;
128    use std::fs::File;
129    use std::io::{Error, ErrorKind, Read};
130    use std::path::Path;
131
132    init_logger();
133    info!("DC read of {:?}", file_paths);
134
135    let mut filenames: Vec<String> = vec![];
136    let mut file_results: Vec<Result<File, std::io::Error>> = vec![];
137    let mut pipeline_input: Vec<parser::InputFile> = vec![];
138
139    if file_paths.is_empty() {
140        warn!("No DC files given! Using empty DC file.");
141        return read_dc(String::default());
142    }
143
144    for file_path in &file_paths {
145        // Get filename from given path
146        match Path::new(file_path).file_name() {
147            Some(filename_osstr) => {
148                // Convert OsStr to String and store filename
149                filenames.push(filename_osstr.to_string_lossy().into_owned());
150            }
151            None => {
152                // std::path::Path.file_name() **only** returns `None`
153                // if the path terminates in '..'.
154                let filename_err: Error = Error::new(
155                    ErrorKind::InvalidInput,
156                    "Failed to get filename from path because\
157                    path terminates in '..'.",
158                );
159                return Err(DCReadError::IO(filename_err));
160            }
161        }
162
163        // Open file using path and store result
164        file_results.push(File::open(file_path));
165    }
166
167    for (index, io_result) in file_results.into_iter().enumerate() {
168        if let Ok(mut dcf) = io_result {
169            // Prepare `InputFile` tuple for the pipeline function.
170            let filename: String = filenames.get(index).unwrap().to_owned();
171            let mut in_file: InputFile = (filename, String::default());
172
173            let res: std::io::Result<usize> = dcf.read_to_string(&mut in_file.1);
174
175            if let Err(res_err) = res {
176                // DC file content may not be in proper UTF-8 encoding.
177                return Err(DCReadError::IO(res_err));
178            }
179            pipeline_input.push(in_file);
180        } else {
181            // Failed to open one of the DC files. (most likely permission error)
182            return Err(DCReadError::IO(io_result.unwrap_err()));
183        }
184    }
185
186    parser::dcparse_pipeline(pipeline_input)
187}
188
189/// Front end to the donet-core DC parser pipeline.
190///
191/// ## Example Usage
192/// The following is an example of parsing a simple DC file string,
193/// printing its DC hash in hexadecimal notation, and accessing
194/// the elements of a defined Distributed Class:
195/// ```rust
196/// use donet_core::dcfile::DCFile;
197/// use donet_core::dclass::DClass;
198/// use donet_core::read_dc;
199///
200/// let dc_file = "
201///
202/// from game.ai import AnonymousContact/UD
203/// from game.ai import LoginManager/AI
204/// from game.world import DistributedWorld/AI
205/// from game.avatar import DistributedAvatar/AI/OV
206///
207/// typedef uint32 doId;
208/// typedef uint32 zoneId;
209/// typedef uint64 channel;
210///
211/// dclass AnonymousContact {
212///   login(string username, string password) clsend airecv;
213/// };
214///
215/// dclass LoginManager {
216///   login(channel client, string username, string password) airecv;
217/// };
218///
219/// dclass DistributedWorld {
220///   create_avatar(channel client) airecv;
221/// };
222///
223/// dclass DistributedAvatar {
224///   set_xyzh(int16 x, int16 y, int16 z, int16 h) broadcast required;
225///   indicate_intent(int16 / 10, int16 / 10) ownsend airecv;
226/// };
227///
228/// ";
229///
230/// let dc_read = read_dc(dc_file.into());
231///
232/// if let Ok(dc_file) = dc_read {
233///     // Print the DC File's 32-bit hash in hexadecimal format.
234///     println!("{}", dc_file.get_pretty_hash());
235///
236///     // TODO: Retrieve the `DistributedAvatar` dclass by ID.
237///     //let class: &DClass = dc_file.get_dclass_by_id(3);
238///
239///     // TODO: Print the identifier of the dclass.
240///     //println!("{}", class.get_name());
241/// }
242/// ```
243///
244/// The output of the program would be the following:
245/// ```txt
246/// 0x0a93c1b8
247/// DistributedAvatar
248/// ```
249/// <br><img src="https://c.tenor.com/myQHgyWQQ9sAAAAd/tenor.gif">
250///
251#[cfg(feature = "dcfile")]
252pub fn read_dc(input: String) -> Result<DCFile, DCReadError> {
253    let dcparse_input: Vec<parser::InputFile> = vec![("input.dc".to_string(), input)];
254
255    parser::dcparse_pipeline(dcparse_input)
256}