use super::*;
const TRACKERS: &[&str] = &[
"fbclid",
"gclid",
"gclsrc",
"dclid",
"gbraid",
"wbraid",
"msclkid",
"yclid",
"ysclid",
"twclid",
"ttclid",
"li_fat_id",
"igshid",
"igsh",
"mc_cid",
"mc_eid",
"_ga",
"_gl",
"_hsenc",
"_hsmi",
"__hstc",
"__hssc",
"__hsfp",
"hsctatracking",
"mkt_tok",
"oly_anon_id",
"oly_enc_id",
"vero_id",
"vero_conv",
"rb_clickid",
"s_cid",
"_openstat",
"srsltid",
"wt_mc",
"pk_campaign",
"pk_kwd",
"pk_source",
"pk_medium",
"pk_content",
"spm",
"scm",
];
const TRACKER_PREFIXES: &[&str] = &["utm_", "mtm_", "itm_", "internal_", "elq"];
const SESSIONS: &[&str] = &[
"jsessionid",
"phpsessid",
"sessionid",
"session_id",
"cfid",
"cftoken",
"zenid",
"oscsid",
];
const BINARY_EXT: &[&str] = &[
"jpg", "jpeg", "png", "gif", "webp", "avif", "bmp", "ico", "svg", "tif", "tiff", "heic", "psd",
"mp3", "mp4", "m4a", "m4v", "mov", "avi", "wmv", "flv", "mkv", "webm", "wav", "ogg", "oga",
"ogv", "flac", "aac", "mid", "midi", "zip", "gz", "tgz", "bz2", "xz", "7z", "rar", "tar",
"zst", "iso", "dmg", "exe", "msi", "apk", "ipa", "deb", "rpm", "jar", "woff", "woff2", "ttf",
"otf", "eot", "css", "js", "mjs", "map", "wasm", "swf",
];
const MAX_SEGMENTS: usize = 20;
const MAX_URL_LEN: usize = 2048;
const MAX_REPEATS: usize = 2;
fn is_session(name: &str, value: &str) -> bool {
if SESSIONS.contains(&name) || name.starts_with("aspsessionid") {
return true;
}
name == "sid" && value.len() >= 16 && value.bytes().all(|b| b.is_ascii_alphanumeric())
}
fn is_tracker(name: &str) -> bool {
TRACKERS.contains(&name) || TRACKER_PREFIXES.iter().any(|p| name.starts_with(p))
}
pub fn clean(u: &mut Url) {
u.set_fragment(None);
let path = u.path();
let lower = path.to_ascii_lowercase();
if lower.contains(";jsessionid=") || lower.contains(";phpsessid=") || path.contains("//") {
let raw: Vec<&str> = path.split('/').skip(1).collect();
let mut trailing = path.ends_with('/');
let mut parts: Vec<&str> = Vec::with_capacity(raw.len());
for (i, seg) in raw.iter().enumerate() {
let low = seg.to_ascii_lowercase();
let seg = match low.find(";jsessionid=").or_else(|| low.find(";phpsessid=")) {
Some(c) => &seg[..c],
None => seg,
};
if seg.is_empty() {
trailing |= i + 1 == raw.len();
continue;
}
parts.push(seg);
}
let mut out = format!("/{}", parts.join("/"));
if trailing && !parts.is_empty() {
out.push('/');
}
u.set_path(&out);
}
if u.query().is_some() {
let pairs: Vec<(String, String)> = u
.query_pairs()
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect();
let kept: Vec<&(String, String)> = pairs
.iter()
.filter(|(k, v)| {
let k = k.to_ascii_lowercase();
!is_tracker(&k) && !is_session(&k, v)
})
.collect();
if kept.len() != pairs.len() {
if kept.is_empty() {
u.set_query(None);
} else {
u.query_pairs_mut()
.clear()
.extend_pairs(kept.iter().map(|(k, v)| (k, v)));
}
}
}
}
pub fn skip(u: &Url) -> bool {
if u.as_str().len() > MAX_URL_LEN {
return true;
}
let path = u.path();
let last = path.rsplit('/').next().unwrap_or_default();
if let Some((_, ext)) = last.rsplit_once('.')
&& ext.len() <= 5
&& BINARY_EXT.contains(&ext.to_ascii_lowercase().as_str())
{
return true;
}
let segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
segs.len() > MAX_SEGMENTS || repeats(&segs)
}
fn repeats(segs: &[&str]) -> bool {
for len in 1..=3 {
let need = len * (MAX_REPEATS + 1);
if segs.len() < need {
break;
}
for start in 0..=segs.len() - need {
let unit = &segs[start..start + len];
if (1..=MAX_REPEATS).all(|r| &segs[start + r * len..start + (r + 1) * len] == unit) {
return true;
}
}
}
false
}
pub fn shape(u: &Url) -> String {
let mut s = String::with_capacity(u.as_str().len());
s.push_str(u.host_str().unwrap_or_default());
for seg in u.path().split('/') {
s.push('/');
if seg.len() >= 16 && seg.bytes().any(|b| b.is_ascii_digit()) {
s.push('*');
continue;
}
let mut in_digits = false;
for c in seg.chars() {
if c.is_ascii_digit() {
if !in_digits {
s.push('#');
}
in_digits = true;
} else {
in_digits = false;
s.push(c);
}
}
}
if u.query().is_some() {
let mut keys: Vec<String> = u.query_pairs().map(|(k, _)| k.into_owned()).collect();
keys.sort();
keys.dedup();
s.push('?');
s.push_str(&keys.join("&"));
}
s
}
pub fn shape_key(u: &Url) -> u64 {
fnv(shape(u).bytes())
}