cal_core/
utils.rs

1use crate::device::device::{DeviceTag, TagType};
2use ip_in_subnet::iface_in_subnet;
3use lazy_static::lazy_static;
4use log::{debug, error, trace};
5use phonenumber::country;
6use phonenumber::country::Id;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11lazy_static! {
12    // Compile regex patterns once at program start
13    static ref PHONE_CLEANER: Regex = Regex::new(r"(?:\+|<sip:)?([^>:]+)(?::.*|>.*)?").unwrap();
14    static ref REQUEST_PLUS: Regex = Regex::new(r"\+\[request\.(\w+)\]").unwrap();
15    static ref APP_PLUS: Regex = Regex::new(r"\+\[app\.(\w+)\]").unwrap();
16    static ref REQUEST: Regex = Regex::new(r"\[request\.(\w+)\]").unwrap();
17    static ref APP: Regex = Regex::new(r"\[app\.(\w+)\]").unwrap();
18}
19
20#[derive(Serialize, Deserialize, Clone, PartialEq)]
21#[serde(rename_all = "lowercase")]
22pub enum NumberFormat {
23    #[serde(rename = "national")]
24    National,
25    #[serde(rename = "e164")]
26    E164,
27    #[serde(rename = "e164p")]
28    E164Plus,
29}
30
31/// Clean a phone number string and parse it based on country code
32///
33/// # Arguments
34/// * `country_code` - Country code as string (e.g., "1" for US)
35/// * `str` - Input phone number to clean
36pub fn clean_str_parse_country(country_code: &str, str: &str) -> String {
37    trace!("Cleaning phone number '{}' with country code '{}'", str, country_code);
38
39    let id = match country_code.parse() {
40        Ok(id) => id,
41        Err(e) => {
42            debug!("Invalid country code '{}', defaulting to GB: {}", country_code, e);
43            country::GB
44        }
45    };
46
47    clean_str(id, str)
48}
49
50/// Clean a phone number string for a specific country using regex
51///
52/// Removes SIP formatting, plus signs, and extracts the main number part
53pub fn clean_str(country: Id, str: &str) -> String {
54    trace!("Cleaning phone number '{}' for country {:?}", str, country);
55
56    // Extract the phone number using regex
57    let clean = match PHONE_CLEANER.captures(str) {
58        Some(caps) => {
59            // Use the captured group which contains just the number
60            caps.get(1).map_or(str, |m| m.as_str())
61        },
62        None => {
63            debug!("Regex pattern didn't match for '{}', using as-is", str);
64            str
65        }
66    };
67
68    trace!("Extracted number part: '{}'", clean);
69    parse_number(country, clean.to_string())
70}
71
72/// Format a phone number according to the specified format
73///
74/// # Arguments
75/// * `str` - Input phone number
76/// * `country_code` - Country code as string
77/// * `format` - Desired output format
78pub fn format_number(str: &str, country_code: &str, format: &NumberFormat) -> String {
79    trace!("Formatting '{}' with country code '{}' using format {:?}", str, country_code, format);
80
81    let clean_str = clean_str_parse_country(country_code, str);
82
83    match format {
84        NumberFormat::National => {
85            let result = national_number(&clean_str, country_code);
86            debug!("Formatted '{}' to national format: '{}'", str, result);
87            result
88        },
89        NumberFormat::E164 => {
90            debug!("Formatted '{}' to E164 format: '{}'", str, clean_str);
91            clean_str
92        },
93        NumberFormat::E164Plus => {
94            let result = format!("+{}", clean_str);
95            debug!("Formatted '{}' to E164Plus format: '{}'", str, result);
96            result
97        },
98    }
99}
100
101/// Format a phone number in national format
102fn national_number(str: &str, country_code: &str) -> String {
103    trace!("Converting '{}' to national format for country '{}'", str, country_code);
104
105    let id = match country_code.parse() {
106        Ok(id) => id,
107        Err(e) => {
108            debug!("Invalid country code '{}', defaulting to GB: {}", country_code, e);
109            country::GB
110        }
111    };
112
113    parse_number(id, str.to_string())
114}
115
116/// Parse a phone number using the phonenumber library
117fn parse_number(country: Id, clean: String) -> String {
118    trace!("Parsing '{}' for country {:?}", clean, country);
119
120    match phonenumber::parse(Some(country), &clean) {
121        Ok(number) => {
122            let valid = phonenumber::is_valid(&number);
123            if valid {
124                let result = format!("{}{}", number.code().value(), number.national().value());
125                trace!("Successfully parsed number: '{}'", result);
126                result
127            } else {
128                debug!("Number '{}' parsed but not valid, returning as-is", clean);
129                clean
130            }
131        }
132        Err(e) => {
133            debug!("Failed to parse '{}': {}", clean, e);
134            clean
135        }
136    }
137}
138
139/// Replaces placeholders in a string with values from device tags
140/// and applies number formatting when needed
141///
142/// # Arguments
143/// * `input` - Input string with placeholders
144/// * `device_tags` - Map of device tags
145/// * `country_code` - Country code for number formatting
146/// * `format` - Number format to apply to tag values
147pub fn replace_placeholders_with_formatting(
148    input: &str,
149    device_tags: &HashMap<String, DeviceTag>,
150    country_code: &str,
151    format: &NumberFormat
152) -> String {
153    debug!("Replacing placeholders in '{}' with country_code '{}', format {:?}",
154           input, country_code, format);
155
156    let mut result = input.to_string();
157
158    // Process all plus-prefixed placeholders first
159    let replacements = find_tag_replacements(
160        &result,
161        device_tags,
162        country_code,
163        format,
164        true
165    );
166
167    for (placeholder, value) in replacements {
168        result = result.replace(&placeholder, &value);
169    }
170
171    // Then process regular placeholders
172    let replacements = find_tag_replacements(
173        &result,
174        device_tags,
175        country_code,
176        format,
177        false
178    );
179
180    for (placeholder, value) in replacements {
181        result = result.replace(&placeholder, &value);
182    }
183
184    debug!("Replacement result: '{}'", result);
185    result
186}
187
188/// Find all placeholder replacements in a string
189fn find_tag_replacements(
190    input: &str,
191    device_tags: &HashMap<String, DeviceTag>,
192    country_code: &str,
193    format: &NumberFormat,
194    with_plus: bool
195) -> Vec<(String, String)> {
196    let placeholder_type = if with_plus { "plus-prefixed" } else { "standard" };
197    trace!("Finding {} placeholders in '{}'", placeholder_type, input);
198
199    let mut replacements = Vec::new();
200
201    // Process request.* placeholders
202    if with_plus {
203        for cap in REQUEST_PLUS.captures_iter(input) {
204            let placeholder = cap[0].to_string();
205            let tag_name = &cap[1];
206
207            let key = format!("request.{}", tag_name);
208            if let Some(tag) = device_tags.get(&key) {
209                if tag.tag_type == TagType::Session {
210                    let formatted_value = format_number(&tag.value, country_code, format);
211                    trace!("Found request+ tag: '{}' → '{}'", placeholder, formatted_value);
212                    replacements.push((placeholder, formatted_value));
213                }
214            }
215        }
216    } else {
217        for cap in REQUEST.captures_iter(input) {
218            let placeholder = cap[0].to_string();
219            let tag_name = &cap[1];
220
221            let key = format!("request.{}", tag_name);
222            if let Some(tag) = device_tags.get(&key) {
223                if tag.tag_type == TagType::Session {
224                    let formatted_value = format_number(&tag.value, country_code, format);
225                    trace!("Found request tag: '{}' → '{}'", placeholder, formatted_value);
226                    replacements.push((placeholder, formatted_value));
227                }
228            }
229        }
230    }
231
232    // Process app.* placeholders
233    if with_plus {
234        for cap in APP_PLUS.captures_iter(input) {
235            let placeholder = cap[0].to_string();
236            let tag_name = &cap[1];
237
238            let key = format!("app.{}", tag_name);
239            if let Some(tag) = device_tags.get(&key) {
240                if tag.tag_type == TagType::Global {
241                    let formatted_value = format_number(&tag.value, country_code, format);
242                    trace!("Found app+ tag: '{}' → '{}'", placeholder, formatted_value);
243                    replacements.push((placeholder, formatted_value));
244                }
245            }
246        }
247    } else {
248        for cap in APP.captures_iter(input) {
249            let placeholder = cap[0].to_string();
250            let tag_name = &cap[1];
251
252            let key = format!("app.{}", tag_name);
253            if let Some(tag) = device_tags.get(&key) {
254                if tag.tag_type == TagType::Global {
255                    let formatted_value = format_number(&tag.value, country_code, format);
256                    trace!("Found app tag: '{}' → '{}'", placeholder, formatted_value);
257                    replacements.push((placeholder, formatted_value));
258                }
259            }
260        }
261    }
262
263    replacements
264}
265
266/// Check if an IP address is within a subnet
267///
268/// # Arguments
269/// * `ip` - IP address to check
270/// * `subnet` - Subnet in CIDR notation (e.g., "192.168.1.0/24")
271///
272/// # Returns
273/// `true` if the IP is in the subnet, `false` otherwise
274pub fn ip_in_subnet(ip: &str, subnet: &str) -> bool {
275    trace!("Checking if IP '{}' is in subnet '{}'", ip, subnet);
276
277    // Direct match for identical values
278    if ip == subnet {
279        debug!("IP '{}' matches subnet '{}' directly", ip, subnet);
280        return true;
281    }
282
283    // Check using the iface_in_subnet function
284    match iface_in_subnet(ip, subnet) {
285        Ok(result) => {
286            debug!("IP '{}' in subnet '{}': {}", ip, subnet, result);
287            result
288        },
289        Err(e) => {
290            error!("Failed to check if IP '{}' is in subnet '{}': {}", ip, subnet, e);
291            false
292        }
293    }
294}