// test_truncation.mbt — Truncation safety.
//
// The parser front doors must accept a stream that ends at any byte offset:
// a truncated header, a truncated JSON document, a truncated URI or extended
// value. Every byte-prefix of the complex inputs below is fed to the parsers;
// none may panic and every outcome is either `Ok` or a structured `Err`.
// All inputs in this file are ASCII, so a byte-prefix is a character boundary.
//
// The two tests together exercise more than 2000 (prefix, parser) cases.
///|
/// The first `n` bytes of `s` (safe because the inputs here are ASCII).
fn byte_prefix(s : String, n : Int) -> String {
let bytes = @utf8.encode(s)
decode_utf8(bytes.view(start=0, end=n))
}
///|
/// Evaluates a result, discarding it (used to force parser execution).
fn[T] force(r : Result[T, LinkError]) -> Unit {
match r {
Ok(_) => ()
Err(_) => ()
}
}
///|
test "every byte-prefix of a complex Link header and linkset text never panics" {
let dflt = Limits::default()
let header = "; rel=\"next prev https://rel.example/x\"; anchor=\"https://example.com/\"; hreflang=\"en-US\"; hreflang=\"fr\"; media=\"print\"; title=\"A title with \\\"quotes\\\" and \\\\backslashes\"; title*=UTF-8'en'h%C3%A9llo; type=\"text/html; charset=utf-8\"; x-note=value1; x-flag; hreflang=\"de\"; x-extra=abcdefghij"
let text = "; rel=\"next\",\n; rel=\"prev\"; media=\"screen\",\n; rel=\"canonical\""
let len1 = @utf8.encode(header).length()
let len2 = @utf8.encode(text).length()
assert_true(len1 + len2 >= 400)
let mut cases = 0
for i = 0; i <= len1; i = i + 1 {
let p = byte_prefix(header, i)
force(parse_link_header(p, dflt))
force(parse_linkset_text(p, dflt))
force(canonicalize_link_header(p))
force(parse_linkset_json(p, dflt))
force(parse_uri_reference(p))
cases = cases + 5
}
for i = 0; i <= len2; i = i + 1 {
let p = byte_prefix(text, i)
force(parse_linkset_text(p, dflt))
force(parse_link_header(p, dflt))
cases = cases + 2
}
assert_true(cases >= 1500)
}
///|
test "every byte-prefix of complex JSON, URI and ext-value inputs never panics" {
let dflt = Limits::default()
let json = "{\"linkset\":[{\"anchor\":\"https://example.com/\",\"next\":[{\"href\":\"https://example.com/next\",\"hreflang\":[\"en\",\"fr\",\"de\"],\"media\":\"print\",\"title\":\"T\",\"type\":\"text/html\",\"x-note\":[\"v1\",\"v2\"]}],\"prev\":[{\"href\":\"https://example.com/prev\",\"media\":\"screen\",\"hreflang\":[\"en\"],\"title*\":[{\"value\":\"salut\",\"language\":\"fr\"}]}],\"https://rel.example/x\":[{\"href\":\"/other\"}]}]}"
let uri = "https://user%20name@example.com:8443/a%2Fb/c/d/e/f/g;p?q=%20x%20y&r=z&s=w&t=v#frag"
let ext = "UTF-8'en'hello%20world%2C%20with%20%C3%A9%20accented%20and%20more%20text%20here%20and%20there%20everywhere"
let lj = @utf8.encode(json).length()
let lu = @utf8.encode(uri).length()
let le = @utf8.encode(ext).length()
assert_true(lj + lu + le >= 500)
let mut cases = 0
for i = 0; i <= lj; i = i + 1 {
force(parse_linkset_json(byte_prefix(json, i), dflt))
cases = cases + 1
}
for i = 0; i <= lu; i = i + 1 {
let p = byte_prefix(uri, i)
force(parse_uri_reference(p))
force(parse_link_header("<\{p}>; rel=\"next\"", dflt))
cases = cases + 2
}
for i = 0; i <= le; i = i + 1 {
let p = byte_prefix(ext, i)
force(parse_extended_value_string(p))
force(parse_link_header("; title*=\{p}", dflt))
cases = cases + 2
}
assert_true(cases >= 400)
}