---
title: "Introduction to robotstxtr"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Introduction to robotstxtr}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup}
library(robotstxtr)
```

## What robotstxtr does

`robotstxtr` decides whether a URL may be crawled according to a site's
`robots.txt` rules. It answers one question — *"is this URL allowed for this
crawler?"* — and answers it exactly the way Google's own crawler would, because
the matching engine is a vendored, upstream-test-validated snapshot of Google's
open-source C++ `robots.txt` parser and matcher.

The core value is **fidelity**. `robots.txt` matching has many quirks — longest
rule wins, `Allow` beats `Disallow` on an equal-length tie, `*` and `$`
wildcards, case-sensitive paths but case-insensitive user-agent groups,
percent-escape canonicalization. Re-implementing those rules by hand is a common
source of subtle bugs. `robotstxtr` sidesteps that by running the real Google
algorithm and returning a stable, machine-readable decision for every URL.

This is a faithful, upstream-test-validated replica of the pinned open-source
matcher. It is not a claim of universal behavioral identity with Googlebot,
which has crawl policy beyond this matcher.

## Crawl-decision entry points

There are two ways to get a decision, depending on whether you already have the
`robots.txt` text:

- `allowed_by_robots_text()` — you supply the `robots.txt` body yourself. No
  network access is performed. This is the pure matching path.
- `allowed_by_robots_url()` — you supply only the target URL(s). The package
  works out the governing `robots.txt` origin, fetches it under a deterministic
  and conservative HTTP policy, and matches.

Both return an S3 `robots_decisions` object with one row per input URL.

Document validation is a separate operation. `robots_validate_text()` checks a
supplied document, while `robots_validate_url()` runs the same deterministic
fetch stage as `robots_fetch()` and validates each distinct stored raw body.

## Matching against a supplied body

`allowed_by_robots_text()` needs a `robots.txt` body, one or more URLs, and a
crawler user-agent token. Everything below runs offline, so it is safe to
evaluate in this vignette.

```{r}
robots <- "user-agent: *
disallow: /private
allow: /private/public
"

allowed_by_robots_text(robots, "https://example.com/private/report", "mybot")
```

The result is a data-frame-backed object. Pull out the pieces you need from its
`results` table:

```{r}
decision <- allowed_by_robots_text(
  robots,
  c(
    "https://example.com/index.html",     # not disallowed, allowed
    "https://example.com/private/report", # under /private, disallowed
    "https://example.com/private/public"  # re-allowed by the Allow rule
  ),
  "mybot"
)

decision$results[c("url", "allowed", "decision_source")]
```

`allowed` is the headline `TRUE`/`FALSE`/`NA` answer. `decision_source` explains
*why*: `rule_disallow`, `rule_allow`, `default_allow` (matching ran and nothing
matched), `missing_allow` (the file was absent), or one of the `*_unknown`
values when no safe decision was possible.

### Vectorized, with per-URL user agents

A scalar user agent expands across every URL, or you can pass one user agent per
URL. Invalid rows (a missing URL, an empty user agent) never abort the call;
they come back as a per-row `input_unknown` decision in their original position.

```{r}
allowed_by_robots_text(
  "user-agent: newsbot\ndisallow: /\nuser-agent: *\nallow: /",
  c("https://example.com/a", "https://example.com/a"),
  c("newsbot", "otherbot")
)$results[c("url", "user_agent", "allowed")]
```

### Rich match metadata

For a matched row the object also records which rule won: the one-based
`matched_line`, the `matched_rule_type` (`allow`/`disallow`), and the canonical
`matched_rule_value` — the exact directive value the matcher used, after
upstream escaping canonicalization.

```{r}
m <- allowed_by_robots_text(
  "user-agent: *\ndisallow: /*.pdf$",
  "https://example.com/manual.pdf",
  "mybot"
)
cols <- c("allowed", "matched_line", "matched_rule_type", "matched_rule_value")
m$results[cols]
```

### Inspecting the stored body

The supplied (or fetched) `robots.txt` body is stored once per source. Preview
or extract it with `robots_body()`:

```{r}
robots_body(decision, n = 40)
```

## Fetching robots.txt for a URL

When you do not already have the `robots.txt` text, `allowed_by_robots_url()`
does the whole job: it derives the `scheme://host[:port]/robots.txt` origin for
each URL (handling IDN hosts, ports, IPv6 literals, and userinfo stripping via
`rurl`), fetches each distinct origin exactly once under a conservative `httr2`
policy (10-second timeout, at most five redirects, no HTTPS-to-HTTP downgrades,
no cookies or credentials forwarded), and matches every URL against the body it
retrieved.

```{r, eval = FALSE}
# Not evaluated here: this call performs a network request, which vignettes
# must never require. Run it in an interactive session against a real site.
allowed_by_robots_url("https://example.com/some/page", "mybot")
```

The returned object has exactly the same `robots_decisions` shape as
`allowed_by_robots_text()`, with additional fetch metadata populated per row:
`fetch_outcome` (for example `fetched`, `missing`, `redirect_error`,
`timeout`), `http_status`, and `robots_url`. A `404`/`410` origin yields
`missing_allow` (`TRUE`, because an absent file allows everything); any fetch
failure yields `fetch_unknown` (`NA`), so a transport problem never silently
looks like an allow. A URL that structurally resolves to a private, loopback,
link-local, or cloud-metadata address is refused before any request as
`ssrf_blocked`, which also yields `fetch_unknown` (`NA`).

If you only want the fetch stage without matching, `robots_fetch()` returns the
grouped fetch results on their own.

## Validating the document itself

Use document validation when the question is *"is this robots.txt file
well-formed for the pinned parser profile?"*, rather than *"may this crawler
fetch this URL?"*.

```{r}
validation <- robots_validate_text(
  "user-agent: *\ndisallow: /private\nunknown-field: value\n"
)
validation$documents
validation$diagnostics
```

The `documents` table contains byte size, line and directive counts, encoding,
BOM/NUL and final-newline evidence, and a validation status. The `diagnostics`
table has stable line, severity, code, directive, raw-text, and message columns.
Raw vectors are accepted so malformed UTF-8 and NUL bytes can be reported
without first converting or losing the acquisition bytes.

```{r, eval = FALSE}
# Performs live HTTP, so it is not evaluated in the vignette.
robots_validate_url("https://example.com/some/page")
```

URL validation reuses `robots_fetch()` and never parses a truncated body. For
example, a decoded body over `max_bytes` is reported as an acquisition limit
failure with `validation_status = "not_validated"`.

The validation profile is explicitly `google-parser-compatible`: it reflects
the package's pinned parser (including accepted typo and missing-colon forms)
plus conservative group and byte checks. It is not universal crawler or full
RFC 9309 validation. It also does not infer webmaster intent or fetch declared
sitemap resources to verify their availability.

## The URL you pass is the URL that is matched

`robots.txt` matching is sensitive to the exact URL bytes: path case, query
strings, fragments, and percent escapes all matter. `robotstxtr` passes your
original URL string to the matcher unchanged — it never cleans, decodes, or
reserializes it. The upstream matcher expects an appropriately encoded, RFC 3986
`%`-escaped full URL, so supplying the URL in that form is the caller's
responsibility.

```{r}
# Path case is significant: /Private is disallowed, /private is not.
allowed_by_robots_text(
  "user-agent: *\ndisallow: /Private",
  c("https://example.com/Private", "https://example.com/private"),
  "mybot"
)$results[c("url", "allowed")]
```

## Summary

- `allowed_by_robots_text()` matches against a body you supply, fully offline.
- `allowed_by_robots_url()` fetches and matches under a deterministic policy.
- `robots_validate_text()` and `robots_validate_url()` diagnose document syntax,
  structure, byte hazards, and acquisition evidence under an explicit profile.
- Both return a stable, vectorized `robots_decisions` object with `allowed`,
  `decision_source`, fetch metadata, and per-rule match metadata.
- Decisions come from a vendored snapshot of Google's own matcher, so the quirky
  parts of `robots.txt` semantics are handled faithfully.
