use codoseo_core::change::{Change, ChangeKind};
use codoseo_core::check::Severity;
use codoseo_core::crawl::RobotsFile;
use xxhash_rust::xxh3::xxh3_64;
pub fn robots_fingerprint(body: &str) -> u64 {
let mut rules = String::new();
for line in body.lines() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let (field, value) = match line.split_once(':') {
Some((field, value)) => (field.trim().to_ascii_lowercase(), value.trim()),
None => (line.to_ascii_lowercase(), ""),
};
if field == "sitemap" {
continue;
}
if !rules.is_empty() {
rules.push('\n');
}
rules.push_str(&field);
rules.push(':');
rules.push_str(value);
}
xxh3_64(rules.as_bytes())
}
pub(crate) fn robots_change(
prev: Option<&RobotsFile>,
curr: Option<&RobotsFile>,
newly_blocked: bool,
) -> Option<Change> {
let status =
|f: Option<&RobotsFile>| f.map_or_else(|| "none".to_owned(), |f| f.status.to_string());
let (before, after, rules_changed) = match (prev, curr) {
(None, None) => (status(prev), status(curr), false),
(Some(p), Some(c)) => {
let rules_differ = robots_fingerprint(&p.body) != robots_fingerprint(&c.body);
(
status(prev),
status(curr),
p.status == c.status && rules_differ,
)
}
_ => (status(prev), status(curr), false),
};
let differs = before != after || rules_changed;
if !differs && !newly_blocked {
return None;
}
let notes: Vec<&str> = [
rules_changed.then_some("rules changed"),
newly_blocked.then_some("blocks the crawl"),
]
.into_iter()
.flatten()
.collect();
let after = if notes.is_empty() {
after
} else {
format!("{after} ({})", notes.join(", "))
};
Some(Change {
kind: ChangeKind::RobotsTxtChanged,
severity: if newly_blocked {
Severity::Critical
} else {
Severity::Warning
},
url: None,
before,
after,
})
}