1use std::{
2 fmt::{Debug, Display, Write},
3 str::FromStr,
4};
5
6use base64::Engine;
7use chrono::{DateTime, Local};
8use enum_map::{Enum, EnumArray, EnumMap};
9use log::warn;
10use num_traits::FromPrimitive;
11
12use crate::{error::SFError, gamestate::ServerTime};
13
14pub const HASH_CONST: &str = "ahHoj2woo1eeChiech6ohphoB7Aithoh";
15pub const DEFAULT_CRYPTO_KEY: &str = "[_/$VV&*Qg&)r?~g";
16pub const DEFAULT_CRYPTO_ID: &str = "0-00000000000000";
17pub const DEFAULT_SESSION_ID: &str = "00000000000000000000000000000000";
18pub const CRYPTO_IV: &str = "jXT#/vz]3]5X7Jl\\";
19
20#[must_use]
21pub fn sha1_hash(val: &str) -> String {
22 use sha1::{Digest, Sha1};
23 let mut hasher = Sha1::new();
24 hasher.update(val.as_bytes());
25 let hash = hasher.finalize();
26 let mut result = String::with_capacity(hash.len() * 2);
27 for byte in &hash {
28 _ = result.write_fmt(format_args!("{byte:02x}"));
29 }
30 result
31}
32
33#[inline]
39pub(crate) fn soft_into<B: Display + Copy, T: TryFrom<B>>(
40 val: B,
41 name: &str,
42 default: T,
43) -> T {
44 val.try_into().unwrap_or_else(|_| {
45 log::warn!("Invalid value for {name} in server response: {val}");
46 default
47 })
48}
49
50#[inline]
53pub(crate) fn warning_try_into<B: Display + Copy, T: TryFrom<B>>(
54 val: B,
55 name: &str,
56) -> Option<T> {
57 val.try_into().ok().or_else(|| {
58 log::warn!("Invalid value for {name} in server response: {val}");
59 None
60 })
61}
62
63#[inline]
66pub(crate) fn warning_parse<T, F, V: Display + Copy>(
67 val: V,
68 name: &str,
69 conv: F,
70) -> Option<T>
71where
72 F: Fn(V) -> Option<T>,
73{
74 conv(val).or_else(|| {
75 log::warn!("Invalid value for {name} in server response: {val}");
76 None
77 })
78}
79
80#[inline]
81pub(crate) fn warning_from_str<T: FromStr>(val: &str, name: &str) -> Option<T> {
82 val.parse().ok().or_else(|| {
83 log::warn!("Invalid value for {name} in server response: {val}");
84 None
85 })
86}
87
88#[must_use]
91pub fn from_sf_string(val: &str) -> String {
92 let mut new = String::with_capacity(val.len());
93 let mut is_escaped = false;
94 for char in val.chars() {
95 if char == '$' {
96 is_escaped = true;
97 continue;
98 }
99 let escaped_char = match char {
100 x if !is_escaped => x,
101 'b' => '\n',
102 'c' => ':',
103 'P' => '%',
104 's' => '/',
105 'p' => '|',
106 '+' => '&',
107 'q' => '"',
108 'r' => '#',
109 'C' => ',',
110 'S' => ';',
111 'd' => '$',
112 x => {
113 warn!("Unkown escape sequence: ${x}");
114 x
115 }
116 };
117 new.push(escaped_char);
118 is_escaped = false;
119 }
120 new
121}
122
123#[must_use]
126pub fn to_sf_string(val: &str) -> String {
127 let mut new = String::with_capacity(val.len());
128 for char in val.chars() {
129 match char {
130 '\n' => new.push_str("$b"),
131 ':' => new.push_str("$c"),
132 '%' => new.push_str("$P"),
133 '/' => new.push_str("$s"),
134 '|' => new.push_str("$p"),
135 '&' => new.push_str("$+"),
136 '"' => new.push_str("$q"),
137 '#' => new.push_str("$r"),
138 ',' => new.push_str("$C"),
139 ';' => new.push_str("$S"),
140 '$' => new.push_str("$d"),
141 _ => new.push(char),
142 }
143 }
144 new
145}
146
147#[allow(clippy::missing_errors_doc, deprecated)]
163#[deprecated(
164 since = "0.2.2",
165 note = "S&F requests are no longer encrypted, so this function will be \
166 removed in a future update. If you need to decode the arguments \
167 to a command yourself, you can just base64 decode them"
168)]
169pub fn decrypt_url(
170 encrypted_url: &str,
171 login_resp: Option<&str>,
172) -> Result<crate::command::Command, SFError> {
173 let crypto_key = if let Some(login_resp) = login_resp {
174 login_resp
175 .split('&')
176 .filter_map(|a| a.split_once(':'))
177 .find(|a| a.0 == "cryptokey")
178 .ok_or(SFError::InvalidRequest("No crypto key in login resp"))?
179 .1
180 } else {
181 DEFAULT_CRYPTO_KEY
182 };
183
184 let encrypted = encrypted_url
185 .split_once("req=")
186 .ok_or(SFError::InvalidRequest("url does not contain request"))?
187 .1
188 .rsplit_once("&rnd=")
189 .ok_or(SFError::InvalidRequest("url does not contain rnd"))?
190 .0;
191
192 let resp = encrypted.get(DEFAULT_CRYPTO_ID.len()..).ok_or(
193 SFError::InvalidRequest("encrypted command does not contain crypto id"),
194 )?;
195 let full_resp = decrypt_server_request(resp, crypto_key)?;
196
197 let (_session_id, command) = full_resp.split_once('|').ok_or(
198 SFError::InvalidRequest("decrypted command has no session id"),
199 )?;
200
201 let (cmd_name, args) = command
202 .split_once(':')
203 .ok_or(SFError::InvalidRequest("decrypted command has no name"))?;
204 let args: Vec<_> = args
205 .trim_end_matches('|')
206 .split('/')
207 .map(std::string::ToString::to_string)
208 .collect();
209
210 Ok(crate::command::Command::Custom {
211 cmd_name: cmd_name.to_string(),
212 arguments: args,
213 })
214}
215
216#[allow(clippy::missing_errors_doc)]
217#[deprecated(
218 since = "0.2.2",
219 note = "S&F requests are no longer encrypted, so this function will be \
220 removed in a future update. If you need to decode the arguments \
221 to a command yourself, you can just base64 decode them"
222)]
223pub fn decrypt_server_request(
224 to_decrypt: &str,
225 key: &str,
226) -> Result<String, SFError> {
227 let text = base64::engine::general_purpose::URL_SAFE
228 .decode(to_decrypt)
229 .map_err(|_| {
230 SFError::InvalidRequest("Value to decode is not base64")
231 })?;
232
233 let mut my_key = [0; 16];
234 my_key.copy_from_slice(
235 key.as_bytes()
236 .get(..16)
237 .ok_or(SFError::InvalidRequest("Key is not 16 bytes long"))?,
238 );
239
240 let mut cipher = libaes::Cipher::new_128(&my_key);
241 cipher.set_auto_padding(false);
242 let decrypted = cipher.cbc_decrypt(CRYPTO_IV.as_bytes(), &text);
243
244 String::from_utf8(decrypted)
245 .map_err(|_| SFError::InvalidRequest("Decrypted value is not UTF8"))
246}
247
248pub(crate) fn parse_vec<B: Display + Copy + std::fmt::Debug, T, F>(
249 data: &[B],
250 name: &'static str,
251 func: F,
252) -> Result<Vec<T>, SFError>
253where
254 F: Fn(B) -> Option<T>,
255{
256 data.iter()
257 .map(|a| {
258 func(*a)
259 .ok_or_else(|| SFError::ParsingError(name, format!("{data:?}")))
260 })
261 .collect()
262}
263
264fn raw_cget<T: Copy + std::fmt::Debug>(
265 val: &[T],
266 pos: usize,
267 name: &'static str,
268) -> Result<T, SFError> {
269 val.get(pos)
270 .copied()
271 .ok_or_else(|| SFError::TooShortResponse {
272 name,
273 pos,
274 array: format!("{val:?}"),
275 })
276}
277
278pub(crate) trait CGet<T: Copy + std::fmt::Debug> {
279 fn cget(&self, pos: usize, name: &'static str) -> Result<T, SFError>;
280}
281
282impl<T: Copy + std::fmt::Debug + Display> CGet<T> for [T] {
283 fn cget(&self, pos: usize, name: &'static str) -> Result<T, SFError> {
284 raw_cget(self, pos, name)
285 }
286}
287
288#[allow(unused)]
289pub(crate) trait CCGet<T: Copy + std::fmt::Debug + Display, I: TryFrom<T>> {
290 fn csiget(
291 &self,
292 pos: usize,
293 name: &'static str,
294 def: I,
295 ) -> Result<I, SFError>;
296 fn csimget(
297 &self,
298 pos: usize,
299 name: &'static str,
300 def: I,
301 fun: fn(T) -> T,
302 ) -> Result<I, SFError>;
303 fn cwiget(
304 &self,
305 pos: usize,
306 name: &'static str,
307 ) -> Result<Option<I>, SFError>;
308 fn ciget(&self, pos: usize, name: &'static str) -> Result<I, SFError>;
309 fn cimget(
310 &self,
311 pos: usize,
312 name: &'static str,
313 fun: fn(T) -> T,
314 ) -> Result<I, SFError>;
315}
316
317impl<T: Copy + std::fmt::Debug + Display, I: TryFrom<T>> CCGet<T, I> for [T] {
318 fn csiget(
319 &self,
320 pos: usize,
321 name: &'static str,
322 def: I,
323 ) -> Result<I, SFError> {
324 let raw = raw_cget(self, pos, name)?;
325 Ok(soft_into(raw, name, def))
326 }
327
328 fn cwiget(
329 &self,
330 pos: usize,
331 name: &'static str,
332 ) -> Result<Option<I>, SFError> {
333 let raw = raw_cget(self, pos, name)?;
334 Ok(warning_try_into(raw, name))
335 }
336
337 fn csimget(
338 &self,
339 pos: usize,
340 name: &'static str,
341 def: I,
342 fun: fn(T) -> T,
343 ) -> Result<I, SFError> {
344 let raw = raw_cget(self, pos, name)?;
345 let raw = fun(raw);
346 Ok(soft_into(raw, name, def))
347 }
348
349 fn ciget(&self, pos: usize, name: &'static str) -> Result<I, SFError> {
350 let raw = raw_cget(self, pos, name)?;
351 raw.try_into()
352 .map_err(|_| SFError::ParsingError(name, raw.to_string()))
353 }
354
355 fn cimget(
356 &self,
357 pos: usize,
358 name: &'static str,
359 fun: fn(T) -> T,
360 ) -> Result<I, SFError> {
361 let raw = raw_cget(self, pos, name)?;
362 let raw = fun(raw);
363 raw.try_into()
364 .map_err(|_| SFError::ParsingError(name, raw.to_string()))
365 }
366}
367
368pub(crate) trait CSGet<T: FromStr> {
369 fn cfsget(
370 &self,
371 pos: usize,
372 name: &'static str,
373 ) -> Result<Option<T>, SFError>;
374 fn cfsuget(&self, pos: usize, name: &'static str) -> Result<T, SFError>;
375}
376
377impl<T: FromStr> CSGet<T> for [&str] {
378 fn cfsget(
379 &self,
380 pos: usize,
381 name: &'static str,
382 ) -> Result<Option<T>, SFError> {
383 let raw = raw_cget(self, pos, name)?;
384 Ok(warning_from_str(raw, name))
385 }
386
387 fn cfsuget(&self, pos: usize, name: &'static str) -> Result<T, SFError> {
388 let raw = raw_cget(self, pos, name)?;
389 let Some(val) = warning_from_str(raw, name) else {
390 return Err(SFError::ParsingError(name, raw.to_string()));
391 };
392 Ok(val)
393 }
394}
395
396pub(crate) fn update_enum_map<
397 B: Default + TryFrom<i64>,
398 A: enum_map::Enum + enum_map::EnumArray<B>,
399>(
400 map: &mut enum_map::EnumMap<A, B>,
401 vals: &[i64],
402) {
403 for (map_val, val) in map.as_mut_slice().iter_mut().zip(vals) {
404 *map_val = soft_into(*val, "attribute val", B::default());
405 }
406}
407
408pub trait EnumMapGet<K, V> {
412 fn get(&self, key: K) -> &V;
414 fn get_mut(&mut self, key: K) -> &mut V;
416}
417
418impl<K: Enum + EnumArray<V>, V> EnumMapGet<K, V> for EnumMap<K, V> {
419 fn get(&self, key: K) -> &V {
420 #[allow(clippy::indexing_slicing)]
421 &self[key]
422 }
423
424 fn get_mut(&mut self, key: K) -> &mut V {
425 #[allow(clippy::indexing_slicing)]
426 &mut self[key]
427 }
428}
429
430pub(crate) trait ArrSkip<T: Debug> {
431 fn skip(&self, pos: usize, name: &'static str) -> Result<&[T], SFError>;
434}
435
436impl<T: Debug> ArrSkip<T> for [T] {
437 fn skip(&self, pos: usize, name: &'static str) -> Result<&[T], SFError> {
438 if pos > self.len() {
439 return Err(SFError::TooShortResponse {
440 name,
441 pos,
442 array: format!("{self:?}"),
443 });
444 }
445 Ok(self.split_at(pos).1)
446 }
447}
448
449pub(crate) trait CFPGet<T: Into<i64> + Copy + std::fmt::Debug, R: FromPrimitive>
450{
451 fn cfpget(
452 &self,
453 pos: usize,
454 name: &'static str,
455 fun: fn(T) -> T,
456 ) -> Result<Option<R>, SFError>;
457
458 fn cfpuget(
459 &self,
460 pos: usize,
461 name: &'static str,
462 fun: fn(T) -> T,
463 ) -> Result<R, SFError>;
464}
465
466impl<T: Into<i64> + Copy + std::fmt::Debug, R: FromPrimitive> CFPGet<T, R>
467 for [T]
468{
469 fn cfpget(
470 &self,
471 pos: usize,
472 name: &'static str,
473 fun: fn(T) -> T,
474 ) -> Result<Option<R>, SFError> {
475 let raw = raw_cget(self, pos, name)?;
476 let raw = fun(raw);
477 let t: i64 = raw.into();
478 let res = FromPrimitive::from_i64(t);
479 if res.is_none() && t != 0 && t != -1 {
480 warn!("There might be a new {name} -> {t}");
481 }
482 Ok(res)
483 }
484
485 fn cfpuget(
486 &self,
487 pos: usize,
488 name: &'static str,
489 fun: fn(T) -> T,
490 ) -> Result<R, SFError> {
491 let raw = raw_cget(self, pos, name)?;
492 let raw = fun(raw);
493 let t: i64 = raw.into();
494 FromPrimitive::from_i64(t)
495 .ok_or_else(|| SFError::ParsingError(name, t.to_string()))
496 }
497}
498
499pub(crate) trait CSTGet<T: Copy + Debug + Into<i64>> {
500 fn cstget(
501 &self,
502 pos: usize,
503 name: &'static str,
504 server_time: ServerTime,
505 ) -> Result<Option<DateTime<Local>>, SFError>;
506}
507
508impl<T: Copy + Debug + Into<i64>> CSTGet<T> for [T] {
509 fn cstget(
510 &self,
511 pos: usize,
512 name: &'static str,
513 server_time: ServerTime,
514 ) -> Result<Option<DateTime<Local>>, SFError> {
515 let val = raw_cget(self, pos, name)?;
516 let val = val.into();
517 Ok(server_time.convert_to_local(val, name))
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 #[test]
526 fn test_from_sf_string() {
527 let input = "$bHello$cWorld$PThis$sis a test!";
528 let expected_output = "\nHello:World%This/is a test!";
529 let result = from_sf_string(input);
530 assert_eq!(result, expected_output);
531
532 let input = "$$$$$$$$$$$";
533 let expected_output = "";
534 let result = from_sf_string(input);
535 assert_eq!(result, expected_output);
536
537 let input = "$$b$c$P$s$p$+$q$r$C$S$d";
538 let expected_output = "\n:%/|&\"#,;$";
539 let result = from_sf_string(input);
540 assert_eq!(result, expected_output);
541 }
542 #[test]
543 fn test_to_sf_string() {
544 let input = "\nHello:World%This/is a test!";
545 let expected_output = "$bHello$cWorld$PThis$sis a test!";
546 let result = to_sf_string(input);
547 assert_eq!(result, expected_output);
548
549 let input = "\n:%/|&\"#,;$";
550 let expected_output = "$b$c$P$s$p$+$q$r$C$S$d";
551 let result = to_sf_string(input);
552 assert_eq!(result, expected_output);
553 }
554}