use std::cmp::min;
use std::time::Duration;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use url::Url;
use crate::parse::lexer::Lexer;
use crate::parse::parser::Parser;
use crate::parse::rule::Rule;
use crate::paths::normalize_path;
use crate::BYTE_LIMIT;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Rules {
Rules(Vec<Rule>),
Always(bool),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct RobotsInner {
user_agent: String,
#[cfg_attr(feature = "serde", serde(flatten))]
rules: Rules,
crawl_delay: Option<Duration>,
sitemaps: Vec<Url>,
}
impl RobotsInner {
pub fn from_bytes(robots: &[u8], user_agent: &str) -> Self {
let limit = min(robots.len(), BYTE_LIMIT);
let robots = &robots[0..limit];
let robots: Vec<_> = robots
.iter()
.map(|u| match u {
b'\x00' => b'\n',
v => *v,
})
.collect();
let directives = Lexer::parse_tokens(&robots);
let state = Parser::parse_rules(&directives, user_agent);
Self {
user_agent: state.longest_match,
rules: Self::optimize(state.rules),
crawl_delay: state.crawl_delay,
sitemaps: state.sitemaps,
}
}
fn optimize(rules: Vec<Rule>) -> Rules {
#[cfg(feature = "optimal")]
if rules.is_empty() || rules.iter().all(|r| r.is_allowed()) {
return Rules::Always(true);
} else if rules.iter().all(|r| !r.is_allowed())
&& rules.iter().rev().any(|r| r.is_universal())
{
return Rules::Always(false);
}
Rules::Rules(rules)
}
pub fn from_always(always: bool, crawl_delay: Option<Duration>, user_agent: &str) -> Self {
Self {
user_agent: user_agent.to_string(),
rules: Rules::Always(always),
crawl_delay,
sitemaps: Vec::default(),
}
}
pub fn try_is_allowed(&self, path: &str) -> Option<bool> {
match self.rules {
Rules::Always(always) => Some(always),
Rules::Rules(ref rules) => match normalize_path(path).as_str() {
"/robots.txt" => Some(true),
path => rules
.iter()
.find(|r| r.is_match(path))
.map(|rule| rule.is_allowed()),
},
}
}
pub fn is_allowed(&self, path: &str) -> bool {
self.try_is_allowed(path).unwrap_or(true)
}
pub fn is_always(&self) -> Option<bool> {
match &self.rules {
Rules::Rules(_) => None,
Rules::Always(always) => Some(*always),
}
}
pub fn user_agent(&self) -> &str {
self.user_agent.as_ref()
}
pub fn crawl_delay(&self) -> Option<Duration> {
self.crawl_delay
}
pub fn sitemaps(&self) -> &[Url] {
self.sitemaps.as_slice()
}
pub fn len(&self) -> Option<usize> {
match &self.rules {
Rules::Rules(vec) => Some(vec.len()),
Rules::Always(_) => None,
}
}
pub fn is_empty(&self) -> Option<bool> {
self.len().map(|len| len == 0)
}
}
#[cfg(test)]
#[cfg(feature = "optimal")]
mod optimal_output {
use super::*;
use crate::ALL_UAS;
#[test]
fn from() {
let r = RobotsInner::from_always(true, None, "foo");
assert_eq!(r.is_always(), Some(true));
let r = RobotsInner::from_always(false, None, "foo");
assert_eq!(r.is_always(), Some(false));
}
#[test]
fn empty() {
let r = RobotsInner::from_bytes(b"", ALL_UAS);
assert_eq!(r.is_always(), Some(true));
}
#[test]
fn allow() {
let t = b"Allow: / \n Allow: /foo";
let r = RobotsInner::from_bytes(t, ALL_UAS);
assert_eq!(r.is_always(), Some(true));
}
#[test]
fn disallow_all() {
let t = b"Disallow: /* \n Disallow: /foo";
let r = RobotsInner::from_bytes(t, ALL_UAS);
assert_eq!(r.is_always(), Some(false));
}
#[test]
fn disallow_exc() {
let t = b"Disallow: /* \n Allow: /foo";
let r = RobotsInner::from_bytes(t, ALL_UAS);
assert_eq!(r.is_always(), None);
}
}
#[cfg(test)]
mod precedence_rules {
use super::*;
use crate::ALL_UAS;
#[test]
fn simple() {
let t = b"Allow: /p \n Disallow: /";
let r = RobotsInner::from_bytes(t, ALL_UAS);
assert!(r.is_allowed("/page"));
}
#[test]
fn restrictive() {
let t = b"Allow: /folder \n Disallow: /folder";
let r = RobotsInner::from_bytes(t, ALL_UAS);
assert!(r.is_allowed("/folder/page"));
}
#[test]
fn restrictive2() {
let t = b"Allow: /page \n Disallow: /*.ph";
let r = RobotsInner::from_bytes(t, ALL_UAS);
assert!(r.is_allowed("/page.php5"));
}
#[test]
fn longer() {
let t = b"Allow: /page \n Disallow: /*.htm";
let r = RobotsInner::from_bytes(t, ALL_UAS);
assert!(!r.is_allowed("/page.htm"));
}
#[test]
fn specific() {
let t = b"Allow: /$ \n Disallow: /";
let r = RobotsInner::from_bytes(t, ALL_UAS);
assert!(r.is_allowed("/"));
}
#[test]
fn specific2() {
let t = b"Allow: /$ \n Disallow: /";
let r = RobotsInner::from_bytes(t, ALL_UAS);
assert!(!r.is_allowed("/page.htm"));
}
}
#[cfg(test)]
mod precedence_agents {
use super::*;
static TXT: &[u8] = br#"""
User-Agent: bot-robotxt
Allow: /1
Disallow: /
User-Agent: *
Allow: /2
Disallow: /
User-Agent: bot
Allow: /3
Disallow: /
"""#;
#[test]
fn specific() {
let r = RobotsInner::from_bytes(TXT, "bot-robotxt");
assert!(r.is_allowed("/1"));
assert!(!r.is_allowed("/2"));
assert!(!r.is_allowed("/3"));
}
#[test]
fn strict() {
let r = RobotsInner::from_bytes(TXT, "bot");
assert!(r.is_allowed("/3"));
assert!(!r.is_allowed("/1"));
assert!(!r.is_allowed("/2"));
}
#[test]
fn missing() {
let r = RobotsInner::from_bytes(TXT, "super-bot");
assert!(r.is_allowed("/2"));
assert!(!r.is_allowed("/1"));
assert!(!r.is_allowed("/3"));
}
#[test]
fn partial() {
let r = RobotsInner::from_bytes(TXT, "bot-super");
assert!(r.is_allowed("/3"));
assert!(!r.is_allowed("/1"));
assert!(!r.is_allowed("/2"));
}
}