1use codoseo_core::change::{Change, ChangeKind};
4use codoseo_core::check::Severity;
5use codoseo_core::crawl::RobotsFile;
6use xxhash_rust::xxh3::xxh3_64;
7
8pub fn robots_fingerprint(body: &str) -> u64 {
12 let mut rules = String::new();
13 for line in body.lines() {
14 let line = line.split('#').next().unwrap_or("").trim();
15 if line.is_empty() {
16 continue;
17 }
18 let (field, value) = match line.split_once(':') {
19 Some((field, value)) => (field.trim().to_ascii_lowercase(), value.trim()),
20 None => (line.to_ascii_lowercase(), ""),
21 };
22 if field == "sitemap" {
23 continue;
24 }
25 if !rules.is_empty() {
26 rules.push('\n');
27 }
28 rules.push_str(&field);
29 rules.push(':');
30 rules.push_str(value);
31 }
32 xxh3_64(rules.as_bytes())
33}
34
35pub(crate) fn robots_change(
41 prev: Option<&RobotsFile>,
42 curr: Option<&RobotsFile>,
43 newly_blocked: bool,
44) -> Option<Change> {
45 let status =
46 |f: Option<&RobotsFile>| f.map_or_else(|| "none".to_owned(), |f| f.status.to_string());
47 let (before, after, rules_changed) = match (prev, curr) {
48 (None, None) => (status(prev), status(curr), false),
49 (Some(p), Some(c)) => {
50 let rules_differ = robots_fingerprint(&p.body) != robots_fingerprint(&c.body);
51 (
52 status(prev),
53 status(curr),
54 p.status == c.status && rules_differ,
55 )
56 }
57 _ => (status(prev), status(curr), false),
58 };
59 let differs = before != after || rules_changed;
60 if !differs && !newly_blocked {
61 return None;
62 }
63 let notes: Vec<&str> = [
64 rules_changed.then_some("rules changed"),
65 newly_blocked.then_some("blocks the crawl"),
66 ]
67 .into_iter()
68 .flatten()
69 .collect();
70 let after = if notes.is_empty() {
71 after
72 } else {
73 format!("{after} ({})", notes.join(", "))
74 };
75 Some(Change {
76 kind: ChangeKind::RobotsTxtChanged,
77 severity: if newly_blocked {
78 Severity::Critical
79 } else {
80 Severity::Warning
81 },
82 url: None,
83 before,
84 after,
85 })
86}