1use crate::config::{ClientConfig, MaskedString, Platform};
2use anyhow::{bail, Context, Result};
3use std::str::FromStr;
4
5macro_rules! config_options {
6 ($(
7 $variant:ident => $key:literal,
8 get: $getter:expr,
9 set: $setter:expr
10 ),* $(,)?) => {
11 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12 pub enum ConfigOption {
13 $($variant,)*
14 }
15
16 impl ConfigOption {
17 pub fn all() -> &'static [Self] {
18 &[$(Self::$variant,)*]
19 }
20
21 pub fn as_str(&self) -> &'static str {
22 match self {
23 $(Self::$variant => $key,)*
24 }
25 }
26
27
28 pub fn get_value(&self, config: &ClientConfig) -> Result<String> {
29 match self {
30 $(Self::$variant => $getter(config),)*
31 }
32 }
33
34 pub fn set_value(&self, config: &mut ClientConfig, value: &str) -> Result<()> {
35 match self {
36 $(Self::$variant => $setter(config, value)?,)*
37 }
38 Ok(())
39 }
40 }
41
42 impl FromStr for ConfigOption {
43 type Err = anyhow::Error;
44
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 match s {
47 $($key => Ok(Self::$variant),)*
48 _ => bail!("Unknown config option: {}", s),
49 }
50 }
51 }
52 };
53}
54
55config_options! {
56 Server => "server",
57 get: |config: &ClientConfig| Ok(config.server.to_string()),
58 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
59 config.server = value.parse().context("Invalid server URL")?;
60 Ok(())
61 },
62
63 Token => "token",
64 get: |config: &ClientConfig| Ok(config.token.as_ref().map_or("".to_string(), |t| t.to_string())),
65 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
66 if let Some((secret, server)) = value.rsplit_once('@') {
70 let server = if server.contains("://") {
71 server.to_string()
72 } else {
73 format!("https://{}", server)
74 };
75 config.server = server.parse().context("Invalid server address in token")?;
76 config.token = Some(MaskedString::from(secret));
77 } else {
78 config.token = Some(MaskedString::from(value));
79 }
80 Ok(())
81 },
82
83 Hwid => "hwid",
84 get: |config: &ClientConfig| Ok(config.get_hwid()),
85 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
86 config.hwid = Some(value.to_string());
87 Ok(())
88 },
89
90 HeartbeatTimeout => "heartbeat_timeout",
91 get: |config: &ClientConfig| Ok(config.heartbeat_timeout.to_string()),
92 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
93 config.heartbeat_timeout = value.parse().context("Invalid heartbeat_timeout")?;
94 Ok(())
95 },
96
97 OneCHome => "1c_home",
98 get: |config: &ClientConfig| Ok(config.one_c_home.clone().unwrap_or_default()),
99 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
100 config.one_c_home = if value.is_empty() { None } else { Some(value.to_string()) };
101 Ok(())
102 },
103
104 OneCPlatform => "1c_platform",
105 get: |config: &ClientConfig| Ok(config.one_c_platform.as_ref().map(|p| p.to_string()).unwrap_or_default()),
106 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
107 config.one_c_platform = if value.is_empty() {
108 None
109 } else {
110 Some(value.parse::<Platform>().context("Invalid platform")?)
111 };
112 Ok(())
113 },
114
115 OneCPublishDir => "1c_publish_dir",
116 get: |config: &ClientConfig| Ok(config.one_c_publish_dir.clone().unwrap_or_default()),
117 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
118 config.one_c_publish_dir = if value.is_empty() { None } else { Some(value.to_string()) };
119 Ok(())
120 },
121
122 MinecraftServer => "minecraft_server",
123 get: |config: &ClientConfig| Ok(config.minecraft_server.clone().unwrap_or_default()),
124 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
125 config.minecraft_server = if value.is_empty() { None } else { Some(value.to_string()) };
126 Ok(())
127 },
128
129 MinecraftJavaOpts => "minecraft_java_opts",
130 get: |config: &ClientConfig| Ok(config.minecraft_java_opts.clone().unwrap_or_default()),
131 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
132 config.minecraft_java_opts = if value.is_empty() { None } else { Some(value.to_string()) };
133 Ok(())
134 },
135
136 MinimizeToTrayOnClose => "minimize_to_tray_on_close",
137 get: |config: &ClientConfig| Ok(config.minimize_to_tray_on_close.map_or("false".to_string(), |v| v.to_string())),
138 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
139 config.minimize_to_tray_on_close = Some(value.parse().context("Invalid boolean value")?);
140 Ok(())
141 },
142
143 MinimizeToTrayOnStart => "minimize_to_tray_on_start",
144 get: |config: &ClientConfig| Ok(config.minimize_to_tray_on_start.map_or("false".to_string(), |v| v.to_string())),
145 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
146 config.minimize_to_tray_on_start = Some(value.parse().context("Invalid boolean value")?);
147 Ok(())
148 },
149
150 SignedOut => "signed_out",
151 get: |config: &ClientConfig| Ok(config.signed_out.map_or("false".to_string(), |v| v.to_string())),
152 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
153 config.signed_out = Some(value.parse().context("Invalid boolean value")?);
154 Ok(())
155 },
156
157 UnsafeTls => "unsafe_tls",
158 get: |config: &ClientConfig| Ok(config.transport.tls.as_ref().map_or("".to_string(), |tls| {
159 tls.danger_ignore_certificate_verification.map_or("".to_string(), |v| v.to_string())
160 })),
161 set: |config: &mut ClientConfig, value: &str| -> Result<(), anyhow::Error> {
162 let allow = value.parse().context("Invalid boolean value")?;
163 if config.transport.tls.is_none() {
164 config.transport.tls = Some(Default::default());
165 }
166 if let Some(tls) = config.transport.tls.as_mut() {
167 tls.danger_ignore_certificate_verification = Some(allow);
168 }
169 Ok(())
170 },
171}
172
173impl ClientConfig {
174 pub fn get_option(&self, option: ConfigOption) -> Result<String> {
175 option.get_value(self)
176 }
177
178 pub fn set_option(&mut self, option: ConfigOption, value: &str) -> Result<()> {
179 option.set_value(self, value)?;
180 self.save()
181 }
182
183 pub fn get_all_config_options(&self) -> Vec<(String, String)> {
184 ConfigOption::all()
185 .iter()
186 .filter_map(|&option| {
187 option
188 .get_value(self)
189 .ok()
190 .map(|value| (option.as_str().to_string(), value))
191 })
192 .collect()
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_set_token_plain() {
202 let mut config = ClientConfig::default();
203 let server_before = config.server.clone();
204 ConfigOption::Token
205 .set_value(&mut config, "secret123")
206 .unwrap();
207 assert_eq!(config.token.as_deref(), Some("secret123"));
208 assert_eq!(config.server, server_before);
209 }
210
211 #[test]
212 fn test_set_token_with_server() {
213 let mut config = ClientConfig::default();
214 ConfigOption::Token
215 .set_value(&mut config, "secret123@my.server.ru:8443")
216 .unwrap();
217 assert_eq!(config.token.as_deref(), Some("secret123"));
218 assert_eq!(config.server.to_string(), "https://my.server.ru:8443/");
219 }
220
221 #[test]
222 fn test_set_token_with_server_url() {
223 let mut config = ClientConfig::default();
224 ConfigOption::Token
225 .set_value(&mut config, "secret123@https://my.server.ru")
226 .unwrap();
227 assert_eq!(config.token.as_deref(), Some("secret123"));
228 assert_eq!(config.server.to_string(), "https://my.server.ru/");
229 }
230
231 #[test]
232 fn test_set_token_with_invalid_server() {
233 let mut config = ClientConfig::default();
234 assert!(ConfigOption::Token
235 .set_value(&mut config, "secret123@ not a server ")
236 .is_err());
237 }
238}