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