1use std::{
2 fmt::{Debug, Display, Write},
3 str::FromStr,
4};
5
6use chrono::{DateTime, Local};
7use enum_map::{Enum, EnumArray, EnumMap};
8use log::warn;
9use num_traits::FromPrimitive;
10
11use crate::{error::SFError, gamestate::ServerTime};
12
13pub const HASH_CONST: &str = "ahHoj2woo1eeChiech6ohphoB7Aithoh";
14pub const DEFAULT_CRYPTO_KEY: &str = "[_/$VV&*Qg&)r?~g";
15pub const DEFAULT_CRYPTO_ID: &str = "0-00000000000000";
16pub const DEFAULT_SESSION_ID: &str = "00000000000000000000000000000000";
17pub const CRYPTO_IV: &str = "jXT#/vz]3]5X7Jl\\";
18
19#[must_use]
20pub fn sha1_hash(val: &str) -> String {
21 use sha1::{Digest, Sha1};
22 let mut hasher = Sha1::new();
23 hasher.update(val.as_bytes());
24 let hash = hasher.finalize();
25 let mut result = String::with_capacity(hash.len() * 2);
26 for byte in &hash {
27 _ = result.write_fmt(format_args!("{byte:02x}"));
28 }
29 result
30}
31
32#[inline]
38pub(crate) fn soft_into<B: Display + Copy, T: TryFrom<B>>(
39 val: B,
40 name: &str,
41 default: T,
42) -> T {
43 val.try_into().unwrap_or_else(|_| {
44 log::warn!("Invalid value for {name} in server response: {val}");
45 default
46 })
47}
48
49#[inline]
52pub(crate) fn warning_try_into<B: Display + Copy, T: TryFrom<B>>(
53 val: B,
54 name: &str,
55) -> Option<T> {
56 val.try_into().ok().or_else(|| {
57 log::warn!("Invalid value for {name} in server response: {val}");
58 None
59 })
60}
61
62#[inline]
65pub(crate) fn warning_parse<T, F, V: Display + Copy>(
66 val: V,
67 name: &str,
68 conv: F,
69) -> Option<T>
70where
71 F: Fn(V) -> Option<T>,
72{
73 conv(val).or_else(|| {
74 log::warn!("Invalid value for {name} in server response: {val}");
75 None
76 })
77}
78
79#[inline]
80pub(crate) fn warning_from_str<T: FromStr>(val: &str, name: &str) -> Option<T> {
81 val.parse().ok().or_else(|| {
82 log::warn!("Invalid value for {name} in server response: {val}");
83 None
84 })
85}
86
87#[must_use]
90pub fn from_sf_string(val: &str) -> String {
91 let mut new = String::with_capacity(val.len());
92 let mut is_escaped = false;
93 for char in val.chars() {
94 if char == '$' {
95 is_escaped = true;
96 continue;
97 }
98 let escaped_char = match char {
99 x if !is_escaped => x,
100 'b' => '\n',
101 'c' => ':',
102 'P' => '%',
103 's' => '/',
104 'p' => '|',
105 '+' => '&',
106 'q' => '"',
107 'r' => '#',
108 'C' => ',',
109 'S' => ';',
110 'd' => '$',
111 x => {
112 warn!("Unkown escape sequence: ${x}");
113 x
114 }
115 };
116 new.push(escaped_char);
117 is_escaped = false;
118 }
119 new
120}
121
122#[must_use]
125pub fn to_sf_string(val: &str) -> String {
126 let mut new = String::with_capacity(val.len());
127 for char in val.chars() {
128 match char {
129 '\n' => new.push_str("$b"),
130 ':' => new.push_str("$c"),
131 '%' => new.push_str("$P"),
132 '/' => new.push_str("$s"),
133 '|' => new.push_str("$p"),
134 '&' => new.push_str("$+"),
135 '"' => new.push_str("$q"),
136 '#' => new.push_str("$r"),
137 ',' => new.push_str("$C"),
138 ';' => new.push_str("$S"),
139 '$' => new.push_str("$d"),
140 _ => new.push(char),
141 }
142 }
143 new
144}
145
146pub(crate) fn parse_vec<B: Display + Copy + std::fmt::Debug, T, F>(
147 data: &[B],
148 name: &'static str,
149 func: F,
150) -> Result<Vec<T>, SFError>
151where
152 F: Fn(B) -> Option<T>,
153{
154 data.iter()
155 .map(|a| {
156 func(*a)
157 .ok_or_else(|| SFError::ParsingError(name, format!("{data:?}")))
158 })
159 .collect()
160}
161
162fn raw_cget<T: Copy + std::fmt::Debug>(
163 val: &[T],
164 pos: usize,
165 name: &'static str,
166) -> Result<T, SFError> {
167 val.get(pos)
168 .copied()
169 .ok_or_else(|| SFError::TooShortResponse {
170 name,
171 pos,
172 array: format!("{val:?}"),
173 })
174}
175
176pub(crate) trait CGet<T: Copy + std::fmt::Debug> {
177 fn cget(&self, pos: usize, name: &'static str) -> Result<T, SFError>;
178}
179
180impl<T: Copy + std::fmt::Debug + Display> CGet<T> for [T] {
181 fn cget(&self, pos: usize, name: &'static str) -> Result<T, SFError> {
182 raw_cget(self, pos, name)
183 }
184}
185
186#[allow(unused)]
187pub(crate) trait CCGet<T: Copy + std::fmt::Debug + Display, I: TryFrom<T>> {
188 fn csiget(
189 &self,
190 pos: usize,
191 name: &'static str,
192 def: I,
193 ) -> Result<I, SFError>;
194 fn csimget(
195 &self,
196 pos: usize,
197 name: &'static str,
198 def: I,
199 fun: fn(T) -> T,
200 ) -> Result<I, SFError>;
201 fn cwiget(
202 &self,
203 pos: usize,
204 name: &'static str,
205 ) -> Result<Option<I>, SFError>;
206 fn ciget(&self, pos: usize, name: &'static str) -> Result<I, SFError>;
207 fn cimget(
208 &self,
209 pos: usize,
210 name: &'static str,
211 fun: fn(T) -> T,
212 ) -> Result<I, SFError>;
213}
214
215impl<T: Copy + std::fmt::Debug + Display, I: TryFrom<T>> CCGet<T, I> for [T] {
216 fn csiget(
217 &self,
218 pos: usize,
219 name: &'static str,
220 def: I,
221 ) -> Result<I, SFError> {
222 let raw = raw_cget(self, pos, name)?;
223 Ok(soft_into(raw, name, def))
224 }
225
226 fn cwiget(
227 &self,
228 pos: usize,
229 name: &'static str,
230 ) -> Result<Option<I>, SFError> {
231 let raw = raw_cget(self, pos, name)?;
232 Ok(warning_try_into(raw, name))
233 }
234
235 fn csimget(
236 &self,
237 pos: usize,
238 name: &'static str,
239 def: I,
240 fun: fn(T) -> T,
241 ) -> Result<I, SFError> {
242 let raw = raw_cget(self, pos, name)?;
243 let raw = fun(raw);
244 Ok(soft_into(raw, name, def))
245 }
246
247 fn ciget(&self, pos: usize, name: &'static str) -> Result<I, SFError> {
248 let raw = raw_cget(self, pos, name)?;
249 raw.try_into()
250 .map_err(|_| SFError::ParsingError(name, raw.to_string()))
251 }
252
253 fn cimget(
254 &self,
255 pos: usize,
256 name: &'static str,
257 fun: fn(T) -> T,
258 ) -> Result<I, SFError> {
259 let raw = raw_cget(self, pos, name)?;
260 let raw = fun(raw);
261 raw.try_into()
262 .map_err(|_| SFError::ParsingError(name, raw.to_string()))
263 }
264}
265
266pub(crate) trait CSGet<T: FromStr> {
267 fn cfsget(
268 &self,
269 pos: usize,
270 name: &'static str,
271 ) -> Result<Option<T>, SFError>;
272 fn cfsuget(&self, pos: usize, name: &'static str) -> Result<T, SFError>;
273}
274
275impl<T: FromStr> CSGet<T> for [&str] {
276 fn cfsget(
277 &self,
278 pos: usize,
279 name: &'static str,
280 ) -> Result<Option<T>, SFError> {
281 let raw = raw_cget(self, pos, name)?;
282 Ok(warning_from_str(raw, name))
283 }
284
285 fn cfsuget(&self, pos: usize, name: &'static str) -> Result<T, SFError> {
286 let raw = raw_cget(self, pos, name)?;
287 let Some(val) = warning_from_str(raw, name) else {
288 return Err(SFError::ParsingError(name, raw.to_string()));
289 };
290 Ok(val)
291 }
292}
293
294pub(crate) fn update_enum_map<
295 B: Default + TryFrom<i64>,
296 A: enum_map::Enum + enum_map::EnumArray<B>,
297>(
298 map: &mut enum_map::EnumMap<A, B>,
299 vals: &[i64],
300) {
301 for (map_val, val) in map.as_mut_slice().iter_mut().zip(vals) {
302 *map_val = soft_into(*val, "attribute val", B::default());
303 }
304}
305
306pub trait EnumMapGet<K, V> {
310 fn get(&self, key: K) -> &V;
312 fn get_mut(&mut self, key: K) -> &mut V;
314}
315
316impl<K: Enum + EnumArray<V>, V> EnumMapGet<K, V> for EnumMap<K, V> {
317 fn get(&self, key: K) -> &V {
318 #[allow(clippy::indexing_slicing)]
319 &self[key]
320 }
321
322 fn get_mut(&mut self, key: K) -> &mut V {
323 #[allow(clippy::indexing_slicing)]
324 &mut self[key]
325 }
326}
327
328pub(crate) trait ArrSkip<T: Debug> {
329 fn skip(&self, pos: usize, name: &'static str) -> Result<&[T], SFError>;
332}
333
334impl<T: Debug> ArrSkip<T> for [T] {
335 fn skip(&self, pos: usize, name: &'static str) -> Result<&[T], SFError> {
336 if pos > self.len() {
337 return Err(SFError::TooShortResponse {
338 name,
339 pos,
340 array: format!("{self:?}"),
341 });
342 }
343 Ok(self.split_at(pos).1)
344 }
345}
346
347pub(crate) trait CFPGet<T: Into<i64> + Copy + std::fmt::Debug, R: FromPrimitive>
348{
349 fn cfpget(
350 &self,
351 pos: usize,
352 name: &'static str,
353 fun: fn(T) -> T,
354 ) -> Result<Option<R>, SFError>;
355
356 fn cfpuget(
357 &self,
358 pos: usize,
359 name: &'static str,
360 fun: fn(T) -> T,
361 ) -> Result<R, SFError>;
362}
363
364impl<T: Into<i64> + Copy + std::fmt::Debug, R: FromPrimitive> CFPGet<T, R>
365 for [T]
366{
367 fn cfpget(
368 &self,
369 pos: usize,
370 name: &'static str,
371 fun: fn(T) -> T,
372 ) -> Result<Option<R>, SFError> {
373 let raw = raw_cget(self, pos, name)?;
374 let raw = fun(raw);
375 let t: i64 = raw.into();
376 let res = FromPrimitive::from_i64(t);
377 if res.is_none() && t != 0 && t != -1 {
378 warn!("There might be a new {name} -> {t}");
379 }
380 Ok(res)
381 }
382
383 fn cfpuget(
384 &self,
385 pos: usize,
386 name: &'static str,
387 fun: fn(T) -> T,
388 ) -> Result<R, SFError> {
389 let raw = raw_cget(self, pos, name)?;
390 let raw = fun(raw);
391 let t: i64 = raw.into();
392 FromPrimitive::from_i64(t)
393 .ok_or_else(|| SFError::ParsingError(name, t.to_string()))
394 }
395}
396
397pub(crate) trait CSTGet<T: Copy + Debug + Into<i64>> {
398 fn cstget(
399 &self,
400 pos: usize,
401 name: &'static str,
402 server_time: ServerTime,
403 ) -> Result<Option<DateTime<Local>>, SFError>;
404}
405
406impl<T: Copy + Debug + Into<i64>> CSTGet<T> for [T] {
407 fn cstget(
408 &self,
409 pos: usize,
410 name: &'static str,
411 server_time: ServerTime,
412 ) -> Result<Option<DateTime<Local>>, SFError> {
413 let val = raw_cget(self, pos, name)?;
414 let val = val.into();
415 Ok(server_time.convert_to_local(val, name))
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
424 fn test_from_sf_string() {
425 let input = "$bHello$cWorld$PThis$sis a test!";
426 let expected_output = "\nHello:World%This/is a test!";
427 let result = from_sf_string(input);
428 assert_eq!(result, expected_output);
429
430 let input = "$$$$$$$$$$$";
431 let expected_output = "";
432 let result = from_sf_string(input);
433 assert_eq!(result, expected_output);
434
435 let input = "$$b$c$P$s$p$+$q$r$C$S$d";
436 let expected_output = "\n:%/|&\"#,;$";
437 let result = from_sf_string(input);
438 assert_eq!(result, expected_output);
439 }
440 #[test]
441 fn test_to_sf_string() {
442 let input = "\nHello:World%This/is a test!";
443 let expected_output = "$bHello$cWorld$PThis$sis a test!";
444 let result = to_sf_string(input);
445 assert_eq!(result, expected_output);
446
447 let input = "\n:%/|&\"#,;$";
448 let expected_output = "$b$c$P$s$p$+$q$r$C$S$d";
449 let result = to_sf_string(input);
450 assert_eq!(result, expected_output);
451 }
452}