sf_api/
misc.rs

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/// Converts a raw value into the appropriate type. If that is not possible,
33/// a warning will be emitted and the given default returned. This is useful
34/// for stuff, that should not crash everything, when there is a weird value and
35/// the silent failure of `as T`, or `unwrap_or_default()` would yield worse
36/// results and/or no warning
37#[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/// Tries to convert val to T. If that fails a warning is emitted and none is
50/// returned
51#[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/// Converts the value using the function. If that fails, a warning is emitted
63/// and None is returned
64#[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/// Converts a S&F string from the server to their original unescaped
88/// representation
89#[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/// Makes a user controlled string, like a new character description safe to use
123/// in a request
124#[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
306/// This is a workaround for clippy index warnings for safe index ops. It
307/// also is more convenient in some cases to use these fundtions if you want
308/// to make sure something is &mut, or &
309pub trait EnumMapGet<K, V> {
310    /// Gets a normal reference to the value
311    fn get(&self, key: K) -> &V;
312    /// Gets a mutable reference to the value
313    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    /// Basically does the equivalent of [pos..], but bounds checked with
330    /// correct errors
331    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}