use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
static ref PROTOCOL_REGEX: Regex = Regex::new(r"^https?://").unwrap();
static ref DIGIT_REGEX: Regex = Regex::new(r"\d").unwrap();
static ref IP_REGEX: Regex = Regex::new(r"^\d+\.\d+\.\d+\.\d+").unwrap();
}
pub fn extract_features(url: &str, n_features: usize, n_manual_features: usize) -> Vec<f32> {
let cleaned = clean_url(url);
let chars: Vec<char> = cleaned.chars().collect();
let mut features = vec![0.0; n_features + n_manual_features];
for n in 2..=3 {
for i in 0..=(chars.len() as i32 - n) {
let gram: String = chars[i as usize..(i + n) as usize].iter().collect();
let hash = murmurhash3::murmurhash3_x86_32(gram.as_bytes(), 0) as usize;
let idx = hash % n_features;
features[idx] += 1.0;
}
}
let manual_features = extract_manual_features(url);
for (i, &val) in manual_features.iter().enumerate() {
if i < n_manual_features {
features[n_features + i] = val;
}
}
features
}
pub fn extract_manual_features(url: &str) -> Vec<f32> {
let url_lower = url.to_lowercase();
let has_http = if url_lower.starts_with("http://") {
1.0
} else {
0.0
};
let has_https = if url_lower.starts_with("https://") {
1.0
} else {
0.0
};
let url_clean = PROTOCOL_REGEX.replace_all(&url_lower, "");
let url_clean_str = url_clean.as_ref();
let url_len = url_clean_str.len();
let url_len_bucket = if url_len < 30 {
1.0
} else if url_len < 60 {
2.0
} else if url_len < 100 {
3.0
} else if url_len < 150 {
4.0
} else {
5.0
};
let (domain, path) = match url_clean_str.find('/') {
Some(pos) => (&url_clean_str[..pos], &url_clean_str[pos..]),
None => (url_clean_str, ""),
};
let domain_len = domain.len();
let domain_len_bucket = if domain_len < 8 {
1.0
} else if domain_len < 15 {
2.0
} else if domain_len < 25 {
3.0
} else {
4.0
};
let has_ip = if IP_REGEX.is_match(domain) { 1.0 } else { 0.0 };
let num_subdomains = domain.chars().filter(|&c| c == '.').count();
let num_subdomains = std::cmp::min(num_subdomains, 5) as f32;
let num_dots = url_clean_str.chars().filter(|&c| c == '.').count() as f32;
let num_hyphens = url_clean_str.chars().filter(|&c| c == '-').count() as f32;
let num_underscores = url_clean_str.chars().filter(|&c| c == '_').count() as f32;
let num_at = url_clean_str.chars().filter(|&c| c == '@').count() as f32;
let num_percent = url_clean_str.chars().filter(|&c| c == '%').count() as f32;
let num_equals = url_clean_str.chars().filter(|&c| c == '=').count() as f32;
let num_qmark = url_clean_str.chars().filter(|&c| c == '?').count() as f32;
let num_and = url_clean_str.chars().filter(|&c| c == '&').count() as f32;
let num_digits = url_clean_str
.chars()
.filter(|&c| c.is_ascii_digit())
.count() as f32;
let digit_ratio = num_digits / url_clean_str.len().max(1) as f32;
let path_len = path.len();
let path_len_bucket = if path_len == 0 {
1.0
} else if path_len < 20 {
2.0
} else if path_len < 50 {
3.0
} else {
4.0
};
let query_len = match path.find('?') {
Some(pos) => path.len() - pos,
None => 0,
};
let query_len_bucket = if query_len == 0 {
1.0
} else if query_len < 20 {
2.0
} else if query_len < 50 {
3.0
} else {
4.0
};
let tld = domain.split('.').next_back().unwrap_or("");
let popular_tlds = [
"com", "org", "net", "edu", "gov", "io", "co", "me", "uk", "us", "cn", "jp", "de", "fr",
"au", "ca",
];
let tld_code = popular_tlds
.iter()
.position(|&s| s == tld)
.map_or(0, |i| i + 1) as f32;
let sensitive_words = [
"login",
"signin",
"verify",
"account",
"password",
"secure",
"update",
"bank",
"paypal",
"facebook",
"google",
"apple",
"amazon",
"ebay",
"microsoft",
"yahoo",
"linkedin",
];
let has_sensitive_word = if sensitive_words.iter().any(|&w| url_clean_str.contains(w)) {
1.0
} else {
0.0
};
vec![
has_http,
has_https,
url_len_bucket,
domain_len_bucket,
has_ip,
num_subdomains,
num_dots,
num_hyphens,
num_underscores,
num_at,
num_percent,
num_equals,
num_qmark,
num_and,
digit_ratio,
path_len_bucket,
query_len_bucket,
tld_code,
has_sensitive_word,
]
}
pub fn clean_url(url: &str) -> String {
let s = PROTOCOL_REGEX.replace_all(url, "");
let s = s.to_lowercase();
let s = DIGIT_REGEX.replace_all(&s, "0");
s.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clean_url() {
assert_eq!(
clean_url("https://Example123.com/path?query=123"),
"example000.com/path?query=000"
);
assert_eq!(
clean_url("http://192.168.1.1/index.html"),
"000.000.0.0/index.html"
);
assert_eq!(clean_url("example.com"), "example.com");
}
#[test]
fn test_extract_features_basic() {
let features = extract_features("example.com", 100, 19);
assert_eq!(features.len(), 119);
let sum: f32 = features.iter().sum();
assert!(sum > 0.0);
}
#[test]
fn test_extract_manual_features() {
let features = extract_manual_features("https://example.com/login");
assert_eq!(features.len(), 19);
assert_eq!(features[0], 0.0); assert_eq!(features[1], 1.0); assert_eq!(features[18], 1.0); }
}