// Defensive RFC 9309 robots.txt parser.
///|
pub fn parse_robots(input : String) -> Result[RobotsFile, RobotsError] {
parse_robots_with_limits(input, Limits::default())
}
///|
pub fn parse_robots_with_limits(
input : String,
limits : Limits,
) -> Result[RobotsFile, RobotsError] {
Ok(parse_robots_inner(input, limits)) catch {
e => Err(unwrap_robots_error(e))
}
}
///|
fn limit_error(line : Int, offset : Int, context : String) -> RobotsError {
robots_error(Limit, LimitExceeded, line, offset, context)
}
///|
fn finish_group(
groups : Array[RobotsGroup],
agents : Array[String],
rules : Array[RobotsRule],
start_line : Int,
end_line : Int,
limits : Limits,
offset : Int,
) -> Unit raise {
if agents.length() == 0 {
return
}
if groups.length() >= limits.max_groups {
raise limit_error(start_line, offset, "too many user-agent groups")
}
groups.push(robots_group(agents, rules, start_line, end_line))
}
///|
fn parse_robots_inner(input : String, limits : Limits) -> RobotsFile raise {
let input_bytes = byte_len(input)
if input_bytes > limits.max_input_bytes {
raise limit_error(
1,
limits.max_input_bytes,
"input exceeds max_input_bytes",
)
}
if contains_illegal_control(input) {
raise robots_error(
Input,
ControlCharacter,
1,
0,
"input contains an illegal control character",
)
}
let groups : Array[RobotsGroup] = []
let other_records : Array[OtherRecord] = []
let mut agents : Array[String] = []
let mut rules : Array[RobotsRule] = []
let mut group_start = 0
let mut group_end = 0
let mut comments = 0
let mut blanks = 0
let lines = input.split("\n").to_array()
let logical_lines = if input.has_suffix("\n") && lines.length() > 0 {
lines.length() - 1
} else {
lines.length()
}
if logical_lines > limits.max_lines {
raise limit_error(limits.max_lines + 1, 0, "input exceeds max_lines")
}
let mut offset = 0
for index = 0; index < logical_lines; index = index + 1 {
let line_no = index + 1
let raw0 = if index == 0 {
strip_utf8_bom(strip_one_trailing_cr(lines[index].to_owned()))
} else {
strip_one_trailing_cr(lines[index].to_owned())
}
let raw_bytes = byte_len(raw0)
if raw_bytes > limits.max_record_bytes {
raise limit_error(line_no, offset, "record exceeds max_record_bytes")
}
comments = comments + inline_comment_count(raw0)
let clean = strip_comment(raw0).trim().to_owned()
if clean.length() == 0 {
blanks = blanks + 1
offset = offset + byte_len(lines[index].to_owned()) + 1
continue
}
match clean.split_once(":") {
None => raise robots_error(Line, MissingColon, line_no, offset, clean)
Some(parts) => {
let raw_name = parts.0.trim().to_owned()
let value = parts.1.trim().to_owned()
if !valid_record_name(raw_name) {
raise robots_error(Line, InvalidRecordName, line_no, offset, raw_name)
}
match record_kind(raw_name) {
UserAgentRecord => {
if rules.length() > 0 {
finish_group(
groups, agents, rules, group_start, group_end, limits, offset,
)
agents = Array::new()
rules = Array::new()
group_start = 0
group_end = 0
}
if value.length() == 0 {
raise robots_error(
UserAgent,
EmptyUserAgent,
line_no,
offset,
clean,
)
}
let token = lower_ascii(value)
if !valid_product_token(token) {
raise robots_error(
UserAgent,
InvalidUserAgent,
line_no,
offset,
value,
)
}
if agents.length() >= limits.max_user_agents_per_group {
raise limit_error(
line_no, offset, "too many user-agents in one group",
)
}
agents.push(token)
if group_start == 0 {
group_start = line_no
}
group_end = line_no
}
AllowRecord | DisallowRecord => {
if agents.length() == 0 {
raise robots_error(
Rule,
RuleBeforeFirstGroup,
line_no,
offset,
clean,
)
}
if byte_len(value) > limits.max_pattern_bytes {
raise limit_error(
line_no, offset, "pattern exceeds max_pattern_bytes",
)
}
if has_malformed_percent_encoding(value) {
raise robots_error(
Pattern,
InvalidPercentEncoding,
line_no,
offset,
value,
)
}
if rules.length() >= limits.max_rules_per_group {
raise limit_error(line_no, offset, "too many rules in one group")
}
let kind = if record_kind(raw_name) == AllowRecord {
Allow
} else {
Disallow
}
rules.push(robots_rule(kind, value, line_no, offset))
group_end = line_no
}
SitemapRecord | OtherRecordKind =>
other_records.push(other_record(raw_name, value, line_no, offset))
}
}
}
offset = offset + byte_len(lines[index].to_owned()) + 1
}
finish_group(groups, agents, rules, group_start, group_end, limits, offset)
robots_file(groups, other_records, comments, blanks, logical_lines)
}