boreal/module/
string.rs

1use std::collections::HashMap;
2
3use super::{EvalContext, Module, StaticValue, Type, Value};
4
5/// `string` module.
6#[derive(Debug)]
7pub struct String_;
8
9impl Module for String_ {
10    fn get_name(&self) -> &'static str {
11        "string"
12    }
13
14    fn get_static_values(&self) -> HashMap<&'static str, StaticValue> {
15        [
16            (
17                "to_int",
18                StaticValue::function(
19                    Self::to_int,
20                    vec![vec![Type::Bytes], vec![Type::Bytes, Type::Integer]],
21                    Type::Integer,
22                ),
23            ),
24            (
25                "length",
26                StaticValue::function(Self::length, vec![vec![Type::Bytes]], Type::Integer),
27            ),
28        ]
29        .into()
30    }
31}
32
33impl String_ {
34    fn to_int(_ctx: &mut EvalContext, args: Vec<Value>) -> Option<Value> {
35        let mut args = args.into_iter();
36
37        let s: Vec<u8> = args.next()?.try_into().ok()?;
38
39        // Ideally, we would just use i64::from_str_radix. However, that does not
40        // exhibit the same behavior as strtol used in libyara, so we need to
41        // do the parsing by hand.
42
43        let mut s = s.as_slice();
44        while !s.is_empty() && s[0].is_ascii_whitespace() {
45            s = &s[1..];
46        }
47
48        let mut base: u32 = match args.next() {
49            Some(Value::Integer(i)) => {
50                let i = i.try_into().ok()?;
51                if i == 0 || (2..=36).contains(&i) {
52                    i
53                } else {
54                    return None;
55                }
56            }
57            Some(_) => return None,
58            None => 0,
59        };
60
61        let is_negative = if s.starts_with(b"-") {
62            s = &s[1..];
63            true
64        } else if s.starts_with(b"+") {
65            s = &s[1..];
66            false
67        } else {
68            false
69        };
70
71        if base == 0 {
72            if s.starts_with(b"0x") || s.starts_with(b"0X") {
73                base = 16;
74                s = &s[2..];
75            } else if s.starts_with(b"0") {
76                base = 8;
77                // Do not advance s to s[1..]. Why? This is to ensure that "0" is properly
78                // parsed, and not considered as an empty string which returns an undefined
79                // value.
80            } else {
81                base = 10;
82            }
83        }
84
85        if s.is_empty() {
86            return None;
87        }
88
89        let mut res: i64 = 0;
90        for c in s {
91            match (*c as char).to_digit(base) {
92                Some(c) => {
93                    res = res.checked_mul(i64::from(base))?;
94                    if is_negative {
95                        res = res.checked_sub(i64::from(c))?;
96                    } else {
97                        res = res.checked_add(i64::from(c))?;
98                    }
99                }
100                None => return None,
101            }
102        }
103        Some(Value::Integer(res))
104    }
105
106    fn length(_ctx: &mut EvalContext, args: Vec<Value>) -> Option<Value> {
107        let mut args = args.into_iter();
108        let s: Vec<u8> = args.next()?.try_into().ok()?;
109
110        Some(Value::Integer(s.len().try_into().ok()?))
111    }
112}