// Grammar adapted from RFC 9112 ยง3 (request-line) with RFC 3986 path/query
// rules simplified to origin-form targets. Code Components in IETF RFCs are
// under the IETF Trust BSD license; see licenses/rfc9110-BSD and SOURCES.md.
// The surrounding example implementation is MoonABNF MIT.
///|
/// ABNF text for an HTTP/1.1 request line, as caller-supplied grammar bytes.
/// Scope: origin-form targets only (no absolute-form, authority-form or `*`).
pub fn grammar() -> Bytes {
let lines = [
"request-line = method SP request-target SP http-version CRLF", "method = 1*tchar",
"tchar = \"!\" / \"#\" / \"$\" / \"%\" / \"&\" / \"'\" / \"*\" / \"+\" / \"-\" / \".\" / \"^\" / \"_\" / \"`\" / \"|\" / \"~\" / DIGIT / ALPHA",
"request-target = absolute-path [ \"?\" query ]", "absolute-path = 1*( \"/\" *pchar )",
"query = *( pchar / \"/\" / \"?\" )", "pchar = unreserved / pct-encoded / sub-delims / \":\" / \"@\"",
"unreserved = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"", "pct-encoded = \"%\" HEXDIG HEXDIG",
"sub-delims = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"",
"http-version = %s\"HTTP/\" DIGIT \".\" DIGIT",
]
@utf8.encode(lines.join("\r\n") + "\r\n")
}
///|
/// Fields lifted from a successful `request-line` derivation. Every field is a
/// `[start, end)` byte span of the original input; nothing is copied or decoded.
pub(all) struct RequestLine {
verb : (Int32, Int32)
path : (Int32, Int32)
query : (Int32, Int32)?
major : Int32
minor : Int32
} derive(Eq)
///|
pub extend RequestLine with Eq::{equal, not_equal}
///|
/// Precondition: `tree` is a successful `request-line` tree for `grammar()`.
/// Looks nodes up by rule name instead of relying on incidental node ids.
pub fn extract(tree : @abnf.InputTree, input : Bytes) -> RequestLine {
let mut meth = (0, 0)
let mut path = (0, 0)
let mut query = None
let mut digits : Array[Int32] = []
for node in tree.nodes {
match node.kind {
@abnf.RuleNode("method") => meth = (node.start, node.end)
@abnf.RuleNode("absolute-path") => path = (node.start, node.end)
@abnf.RuleNode("query") => query = Some((node.start, node.end))
@abnf.RuleNode("http-version") =>
// "HTTP/" DIGIT "." DIGIT: the two digits are fixed offsets from start.
digits = [
input[node.start + 5].to_int() - 48,
input[node.start + 7].to_int() - 48,
]
_ => ()
}
}
{ verb: meth, path, query, major: digits[0], minor: digits[1], }
}
///|
/// Parse a complete request line (including its CRLF) into spans.
/// `Err` carries the farthest byte offset reached, or a budget notice.
pub fn parse(
program : @abnf.Program,
input : Bytes,
) -> Result[RequestLine, String] {
let report = program.parse_input("request-line", input)
match report.status {
@abnf.Matched => Ok(extract(report.tree.unwrap(), input))
@abnf.NoMatch => Err("no-match at byte \{report.offset}")
@abnf.BudgetExceeded(reason) => Err("undecided: budget \{reason}")
@abnf.UnknownRule => Err("unknown entry rule")
@abnf.InvalidLimits => Err("invalid limits")
}
}
///|
fn text(input : Bytes, start : Int32, end : Int32) -> String {
let shown = input[start:end].to_owned()
let s = @utf8.decode_lossy(shown)
s.replace_all(old="\r", new="\\r").replace_all(old="\n", new="\\n")
}
///|
fn dump_node(
tree : @abnf.InputTree,
input : Bytes,
id : Int32,
depth : Int32,
hidden : Array[String],
leaf : Array[String],
out : Array[String],
) -> Unit {
let node = tree.nodes[id]
let mut next_depth = depth
let mut descend = true
if node.kind is @abnf.RuleNode(name) {
// Rule names are stored lower-case: ABNF rule names are case-insensitive.
if hidden.contains(name) {
return
}
let pad = " ".repeat(depth)
out.push(
"\{pad}\{name} [\{node.start},\{node.end}) \"\{text(input, node.start, node.end)}\"",
)
next_depth = depth + 1
descend = !leaf.contains(name)
}
if descend {
for child in node.children {
dump_node(tree, input, child, next_depth, hidden, leaf, out)
}
}
}
///|
/// Indented rule-level view of one derivation, e.g.
/// `request-line [0,24) "GET /a?x=1 HTTP/1.1\r\n"`. Nodes that are only
/// sequence/choice/repeat structure are skipped; per-byte character rules are hidden.
pub fn dump(tree : @abnf.InputTree, input : Bytes) -> String {
let out : Array[String] = []
// Per-byte character rules would add one line per byte: they stay visible
// through their parent's text. sp/crlf/digit are shown but not expanded.
let hidden = ["tchar", "pchar"]
let leaf = ["sp", "crlf", "digit"]
dump_node(tree, input, tree.root, 0, hidden, leaf, out)
out.join("\n")
}