libmudtelnet_rs/
compatibility.rs

1//! Negotiation support and state table.
2//!
3//! The parser uses a compact 256‑entry table to record, for each Telnet
4//! option, whether it is supported locally (us→them), supported remotely
5//! (them→us), and whether either direction is currently enabled.
6//!
7//! Typical usage
8//! ```
9//! use libmudtelnet_rs::{Parser, compatibility::{CompatibilityEntry, CompatibilityTable}};
10//! use libmudtelnet_rs::telnet::op_option::GMCP;
11//! use libmudtelnet_rs::telnet::op_command::WILL;
12//!
13//! // Declare that we support GMCP locally and remotely, initially disabled.
14//! let mut table = CompatibilityTable::new();
15//! table.set_option(GMCP, CompatibilityEntry::new(true, true, false, false));
16//!
17//! let mut parser = Parser::with_support(table);
18//! // Ask to enable GMCP locally (IAC WILL GMCP)
19//! if let Some(send) = parser._will(GMCP) { let _bytes_to_socket = send.to_bytes(); }
20//! // When the server responds (e.g., DO GMCP), the parser tracks state for you.
21//! let _ = WILL; // avoid unused import in doctest
22//! ```
23//!
24//! Tip: call [`CompatibilityTable::reset_states`] when reconnecting, to clear any
25//! enabled flags while preserving which options you support.
26
27/// An expansion of a bitmask contained in `CompatibilityTable`.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct CompatibilityEntry {
30  /// Whether we support this option from us -> them.
31  pub local: bool,
32  /// Whether we support this option from them -> us.
33  pub remote: bool,
34  /// Whether this option is locally enabled.
35  pub local_state: bool,
36  /// Whether this option is remotely enabled.
37  pub remote_state: bool,
38}
39
40impl CompatibilityEntry {
41  #[must_use]
42  pub fn new(local: bool, remote: bool, local_state: bool, remote_state: bool) -> Self {
43    Self {
44      local,
45      remote,
46      local_state,
47      remote_state,
48    }
49  }
50
51  /// Creates a u8 bitmask from this entry.
52  #[must_use]
53  pub fn into_u8(self) -> u8 {
54    let mut res = 0;
55    if self.local {
56      res |= CompatibilityTable::ENABLED_LOCAL;
57    }
58    if self.remote {
59      res |= CompatibilityTable::ENABLED_REMOTE;
60    }
61    if self.local_state {
62      res |= CompatibilityTable::LOCAL_STATE;
63    }
64    if self.remote_state {
65      res |= CompatibilityTable::REMOTE_STATE;
66    }
67    res
68  }
69
70  /// Expands a u8 bitmask into a `CompatibilityEntry`.
71  #[must_use]
72  pub fn from(value: u8) -> Self {
73    Self {
74      local: value & CompatibilityTable::ENABLED_LOCAL == CompatibilityTable::ENABLED_LOCAL,
75      remote: value & CompatibilityTable::ENABLED_REMOTE == CompatibilityTable::ENABLED_REMOTE,
76      local_state: value & CompatibilityTable::LOCAL_STATE == CompatibilityTable::LOCAL_STATE,
77      remote_state: value & CompatibilityTable::REMOTE_STATE == CompatibilityTable::REMOTE_STATE,
78    }
79  }
80}
81
82/// A table of options that are supported locally or remotely, and their current state.
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub struct CompatibilityTable {
85  options: [u8; 256],
86}
87
88impl Default for CompatibilityTable {
89  fn default() -> Self {
90    Self { options: [0; 256] }
91  }
92}
93
94impl CompatibilityTable {
95  /// Option is locally supported.
96  pub const ENABLED_LOCAL: u8 = 1;
97  /// Option is remotely supported.
98  pub const ENABLED_REMOTE: u8 = 1 << 1;
99  /// Option is currently enabled locally.
100  pub const LOCAL_STATE: u8 = 1 << 2;
101  /// Option is currently enabled remotely.
102  pub const REMOTE_STATE: u8 = 1 << 3;
103
104  #[must_use]
105  pub fn new() -> Self {
106    Self::default()
107  }
108
109  /// Create a table with some option values set.
110  ///
111  /// # Arguments
112  ///
113  /// `values` - A slice of `(u8, u8)` tuples. The first value is the option code, and the second is the bitmask value for that option.
114  ///
115  /// # Notes
116  ///
117  /// An option bitmask can be generated using the `CompatibilityEntry` struct, using `entry.into_u8()`.
118  #[must_use]
119  pub fn from_options(values: &[(u8, u8)]) -> Self {
120    let mut options = [0; 256];
121    for (opt, val) in values {
122      options[*opt as usize] = *val;
123    }
124    Self { options }
125  }
126
127  /// Enable local support for an option.
128  pub fn support_local(&mut self, option: u8) {
129    let mut opt = CompatibilityEntry::from(self.options[option as usize]);
130    opt.local = true;
131    self.set_option(option, opt);
132  }
133
134  /// Enable remote support for an option.
135  pub fn support_remote(&mut self, option: u8) {
136    let mut opt = CompatibilityEntry::from(self.options[option as usize]);
137    opt.remote = true;
138    self.set_option(option, opt);
139  }
140
141  /// Enable both remote and local support for an option.
142  pub fn support(&mut self, option: u8) {
143    let mut opt = CompatibilityEntry::from(self.options[option as usize]);
144    opt.local = true;
145    opt.remote = true;
146    self.set_option(option, opt);
147  }
148
149  /// Retrieve a `CompatbilityEntry` generated from the current state of the option value.
150  #[must_use]
151  pub fn get_option(&self, option: u8) -> CompatibilityEntry {
152    CompatibilityEntry::from(self.options[option as usize])
153  }
154
155  /// Set an option value by getting the bitmask from a `CompatibilityEntry`.
156  pub fn set_option(&mut self, option: u8, entry: CompatibilityEntry) {
157    self.options[option as usize] = entry.into_u8();
158  }
159
160  /// Reset all negotiated states
161  pub fn reset_states(&mut self) {
162    for opt in &mut self.options {
163      let mut entry = CompatibilityEntry::from(*opt);
164      entry.local_state = false;
165      entry.remote_state = false;
166      *opt = entry.into_u8();
167    }
168  }
169}
170
171#[cfg(test)]
172mod test_compat {
173  use super::*;
174  use crate::telnet::op_option::GMCP;
175
176  #[test]
177  fn test_reset() {
178    let mut table = CompatibilityTable::default();
179    let entry = CompatibilityEntry::new(true, true, true, true);
180    assert!(entry.remote);
181    assert!(entry.local);
182    assert!(entry.remote_state);
183    assert!(entry.local_state);
184    table.set_option(GMCP, entry);
185    table.reset_states();
186    let entry = table.get_option(GMCP);
187    assert!(entry.remote);
188    assert!(entry.local);
189    assert!(!entry.remote_state);
190    assert!(!entry.local_state);
191  }
192}