diff --git a/LAWS.bend b/LAWS.bend index 30acb7f..5db5a00 100644 --- a/LAWS.bend +++ b/LAWS.bend @@ -1,5560 +1,306 @@ -# ezhttp: the laws. Closed equalities for URL parsing (RFC 3986), HTTP/1.1 -# message framing (RFC 9112), HTTP semantics (RFC 9110), TLS selection -# (RFC 2818), Basic and Bearer credentials (RFC 7617, RFC 4648, RFC 6750), -# cookies (RFC 6265), caching directives (RFC 9111), Fetch CORS, query strings -# (WHATWG URL §5) and HTML character references (HTML §13.2.5). -# PROOF.bend fills them. `bend PROOF.bend` is the gate. +# ezhttp's laws. Each guarantee is one file under laws/, its prose on top and +# its quantified laws under it, proved by the file of the same name under +# proofs/; LAWS.bend and PROOF.bend only import them. The gate: the first line +# `bend PROOF.bend` prints is exactly `ALL PROOFS CHECK`. Tests and fixtures +# are never evidence for a guarantee; what ezhttp cannot prove is said where +# it is assumed, on the code (`# Trusted, not proved:`). A guarantee whose +# prose says it is pending is proved only in part, and is not guaranteed +# until the rest is. +# +# The guarantees cover the server, percent-encoding, the client's requests, +# timeout and redirects, bodies as octets, multipart form bodies, HTML +# escaping, percent-decoding and query strings, HTML character references, +# and the Cookie header sent back after Set-Cookie. The first file of each +# family opens with what the family is about: laws/BadRequestAnswers400.bend +# (the server), laws/QuoteKeepsUnreserved.bend (percent-encoding), +# laws/RequestLineAndHost.bend (the client), laws/OneOctetPerChar.bend +# (bodies as octets), laws/RedirectsFollowLocation.bend (redirects), +# laws/FormRefusesBadRequests.bend (multipart forms), +# laws/HtmlEscapeCharByChar.bend (HTML escaping), +# laws/UnquoteReadsEscapes.bend (percent-decoding and query strings), +# laws/UnescapeFindsReferences.bend (character references) and +# laws/CookieHeaderEchoesStore.bend (cookies sent back). +# +# ezhttp's other laws are closed equalities for URL parsing (RFC 3986), +# HTTP/1.1 message framing (RFC 9112), HTTP semantics (RFC 9110), TLS +# selection (RFC 2818), Basic and Bearer credentials (RFC 7617, RFC 4648, +# RFC 6750), the cookie jar in src/cookie.bend (RFC 6265), caching +# directives (RFC 9111) and Fetch CORS. They are not inventoried as +# guarantees yet: each is a file of its own holding one law, named for what +# the law states and with no prose (laws/UrlPathIsAbsolute.bend, +# laws/CacheReadsKnownDirectives.bend), and promises nothing beyond the +# equality it states. +# +# Commits before these files name guarantees by the requirement IDs of +# docs/SPEC.md, the table these files replaced. import Base -import ./src/http.bend as Http -import ./src/url.bend as Url -import ./src/body.bend as Body -import ./src/auth.bend as Auth -import ./src/wirespec.bend as Spec -import ./src/b64.bend as B64 -import ./src/cookie.bend as Cookie -import ./src/cache.bend as Cache -import ./src/cors.bend as Cors -import ./src/json.bend as Json -import ./src/step.bend as Step -import ./src/pct.bend as Pct -import ./src/html.bend as Html -import ./src/redirect.bend as Redirect -import ./src/form.bend as Form -import ./src/qs.bend as Qs -import ./src/unescape.bend as Ref -import ./src/entities.bend as Ents -import ./src/echo.bend as Echo -import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes -import 0xa81e161008ecc2f297c0a3065cb9b188/main.bend as Ezjson - -# LAW (RFC 3986 §3.3 path-absolute / path-abempty with authority): the path a -# URL comes apart into is always absolute. The request-target is an -# absolute-path (RFC 9112 §3.2.1). -law path_absolute: - for ps: List<&2, String> - {String.starts_with(Url.path.of(ps), "/") == True{} : Bool} - -# LAW: take and drop partition a string at the same UTF-8 byte offset -# (Content-Length octets, RFC 9110 §8.6). -law utf8_split: - for s: String - for +n: Nat - {String.append(Http.octs.take(s, n), Http.octs.drop(s, n)) == s : String} - -# LAW (RFC 9110 §8.6 / RFC 9112 §6.3): a body whose Content-Length is its own -# octet count comes back exactly. -law clen_exact: - for st: U32 - for hs: List<&2, Http.Header> - for +body: String - {Http.parse.clen(st, hs, Http.octs.len(body), body) - == Http.Reply{st, hs, body} : Http.Reply} - -# LAW (RFC 9112 §6.3): a body shorter than its Content-Length is torn — a cut -# connection is not a short file. -# HTTP-BODY-2 -law clen_short: - for st: U32 - for hs: List<&2, Http.Header> - for +n: Nat - for +body: String - for e: {True{} == Nat.is_lt(Http.octs.len(body), n) : Bool} - {Http.parse.clen(st, hs, n, body) - == Http.Torn{"the response body stopped before its Content-Length"} - : Http.Reply} - -# whether a string holds a character nowhere -def avoids(s: String, +c: Char) -> Bool: - match s: - case SNil{}: - True{} - case SCon{+h, t}: - Bool.and(Bool.not(Char.is_eq(h, c)), avoids(t, c)) - -# LAW: a string that holds the character nowhere splits into itself -# HTTP-QS-6 -# HTTP-CK-1 -law split_whole: - for +s: String - for +c: Char - for +e: {avoids(s, c) == True{} : Bool} - {String.split(s, c) == [s] : List<&2, String>} - -# LAW: a string that holds the character first at a known place splits there -# HTTP-QS-6 -# HTTP-CK-1 -law split_cut: - for +s: String - for +c: Char - for +r: String - for +e: {avoids(s, c) == True{} : Bool} - {String.split(String.append(s, SCon{c, r}), c) == s <> String.split(r, c) - : List<&2, String>} - -# LAW (RFC 9112 §7.1): an empty chunked body decodes to empty -law dechunk_empty: - {Http.dechunk("") == "" : String} - -# LAW (RFC 9112 §7.1): no fuel left means no more chunks -law chunks_empty_fuel: - {Http.chunks(0n, Http.NoCut{}) == "" : String} - -# LAW (RFC 9112 §7.1): a chunk-size extension after `;` does not change the -# length hexlen reads. -law hexlen_ignores_extension: - for +s: String - for ext: String - for +e: {avoids(s, ';') == True{} : Bool} - {Http.hexlen(String.append(s, SCon{';', ext})) == Http.hexlen(s) : Nat} - -# LAW: the first piece of a line split on a space is the word before it -law first_is_first_word: - for +w: String - for r: String - for +e: {avoids(w, ' ') == True{} : Bool} - {Http.first(String.split(String.append(w, SCon{' ', r}), ' ')) == w : String} - -# LAW: the second piece is the first word of what follows (status code on -# `HTTP/1.1 200 OK`, RFC 9112 §4) -law second_is_next_word: - for +w: String - for r: String - for +e: {avoids(w, ' ') == True{} : Bool} - {Http.second(String.split(String.append(w, SCon{' ', r}), ' ')) - == Http.first(String.split(r, ' ')) : String} - -# LAW (RFC 9112 §3.2.1): the request-line opens with the method it was given -law request_names_method: - for +method: String - for +host: String - for +path: String - for hs: List<&2, Http.Header> - for +body: String - {String.starts_with(Http.request(method, host, path, hs, body), method) - == True{} : Bool} - -# LAW: GET request-line opens with GET and the path -law get_req_names_path: - for +host: String - for +path: String - for hs: List<&2, Http.Header> - {String.starts_with(Http.get_req(host, path, hs), "GET " ++ path) - == True{} : Bool} - -# LAW: HEAD request-line opens with HEAD and the path (RFC 9110 §9.3.2) -law head_req_names_path: - for +host: String - for +path: String - for hs: List<&2, Http.Header> - {String.starts_with(Http.head_req(host, path, hs), "HEAD " ++ path) - == True{} : Bool} - -# LAW: POST request-line opens with POST and the path -law post_req_names_path: - for +host: String - for +path: String - for hs: List<&2, Http.Header> - for +body: String - {String.starts_with(Http.post_req(host, path, hs, body), "POST " ++ path) - == True{} : Bool} - -# LAW (RFC 9112 §2.1): a response whose header section never ended is torn -law parse_needs_blank_line: - for +raw: String - for e: {Http.NoCut{} == Http.divide(raw, "\r\n\r\n") : Http.Cut} - {Http.parse(raw) == Http.Torn{"the response head has no blank line after it"} - : Http.Reply} - -# LAW: when the head did end, parse uses everything before/after the blank line -# HTTP-CLI-9 -law parse_splits_at_blank_line: - for +raw: String - for before: String - for after: String - for e: {Http.Cut{before, after} == Http.divide(raw, "\r\n\r\n") : Http.Cut} - {Http.parse(raw) == Http.parse.head(String.lines(before), after) - : Http.Reply} - -# LAW (RFC 9112 §6.3): with neither framing header the rest of the connection -# is the body (Connection: close) -law unframed_is_rest: - for st: U32 - for hs: List<&2, Http.Header> - for body: String - {Http.parse.len(st, hs, None{}, body) == Http.Reply{st, hs, body} - : Http.Reply} - -# LAW (RFC 9110 §5.1): field names are compared case-insensitively -law head_hit_ignores_case: - for +h: String - for +name: String - {Http.head.hit(h, name) - == String.starts_with(String.to_lower(h), name ++ ":") : Bool} - -# LAW (RFC 9112 §4): a head whose first line holds no status code is torn -law parse_needs_status: - for +ls: List<&2, String> - for body: String - for e: {None{} == U32.read(Http.second(String.split(Http.first(ls), ' '))) - : Maybe<&2, U32>} - {Http.parse.head(ls, body) - == Http.Torn{"the response has no status line"} - : Http.Reply} - -# LAW: a torn response says so before it says anything else -law show_torn_is_marked: - for why: String - {String.starts_with(Http.show(Http.Torn{why}), "torn ") == True{} : Bool} - -# LAW: a reply opens with the status it holds -law show_reply_opens_with_status: - for +status: U32 - for hs: List<&2, Http.Header> - for body: String - {String.starts_with(Http.show(Http.Reply{status, hs, body}), U32.show(status)) - == True{} : Bool} - -# LAW (RFC 9112 §4): a response message opens with HTTP/1.1 -law respond_opens_with_version: - for +status: U32 - for hs: List<&2, Http.Header> - for +body: String - {String.starts_with(Http.respond(status, hs, body), "HTTP/1.1 ") - == True{} : Bool} - -# LAW (RFC 9112 §4): 200 uses the OK reason-phrase -law reason_200: - {Http.reason(200) == "OK" : String} - -# LAW (RFC 9112 §2.1): a request whose header section never ended is bad -law ask_needs_blank_line: - for +raw: String - for e: {Http.NoCut{} == Http.divide(raw, "\r\n\r\n") : Http.Cut} - {Http.ask(raw) == Http.Bad{"the request head has no blank line after it"} - : Http.Request} - -# LAW (RFC 9112 §3): an empty request-line list is bad -law ask_needs_request_line: - for body: String - {Http.ask.head([], body) == Http.Bad{"the request has no request line"} - : Http.Request} - -# LAW (RFC 9110 §8.6): a request body whose Content-Length matches comes back -law ask_clen_exact: - for +method: String - for +target: String - for hs: List<&2, Http.Header> - for +body: String - {Http.ask.clen(method, target, hs, Http.octs.len(body), body) - == Http.Request{method, target, hs, body} : Http.Request} - -# LAW: a request body shorter than Content-Length is bad -law ask_clen_short: - for +method: String - for +target: String - for hs: List<&2, Http.Header> - for +n: Nat - for +body: String - for e: {True{} == Nat.is_lt(Http.octs.len(body), n) : Bool} - {Http.ask.clen(method, target, hs, n, body) - == Http.Bad{"the request body stopped before its Content-Length"} - : Http.Request} - -# LAW: a bad request show is marked -law ask_show_bad_is_marked: - for why: String - {String.starts_with(Http.ask.show(Http.Bad{why}), "bad ") == True{} : Bool} - -# LAW (RFC 3986 §3.2.3): only https implies 443; other schemes get 80 -law dflt_plain: - for +scheme: String - for e: {False{} == String.eq(scheme, "https") : Bool} - {Url.dflt(scheme) == 80 : U32} - -# LAW: https implies 443 -law dflt_https: - {Url.dflt("https") == 443 : U32} - -# LAW (RFC 3986 §3.1): the scheme is everything before the first colon -law parse_scheme_first: - for +url: String - for h: String - for t: List<&2, String> - for e: {h <> t == String.split(url, ':') : List<&2, String>} - {Url.parse(url) == Url.parse.rest(String.to_lower(h), String.join(t, ":")) - : Url.Loc} - -# LAW (RFC 3986 §3): hier-part without "//" is refused -law parse_needs_slashes: - for +scheme: String - for +rest: String - for e: {False{} == String.starts_with(rest, "//") : Bool} - {Url.parse.rest(scheme, rest) - == Url.Bad{scheme ++ ":" ++ rest ++ " has no //"} - : Url.Loc} - -# LAW: a url that would not come apart says so first -law url_show_bad_is_marked: - for why: String - {String.starts_with(Url.show(Url.Bad{why}), "bad ") == True{} : Bool} - -# LAW: a url that came apart opens with its scheme -law url_show_opens_with_scheme: - for +scheme: String - for host: String - for port: U32 - for path: String - {String.starts_with(Url.show(Url.Loc{scheme, host, port, path}), scheme) - == True{} : Bool} - -# --- main API surface (named so bolt laws stay clean) --- - -# LAW: bearer builds Authorization: Bearer … -law bearer_is_authorization: - for token: String - {Auth.auth.bearer(token) == Http.H{"Authorization", "Bearer " ++ token} - : Http.Header} - -# --- RFC 3986 authority, query, fragment --- - -# LAW (RFC 3986 §3.3): an empty path-abempty is the origin-form `/` -law url_empty_path: - {Url.parse("http://example.com") - == Url.Loc{"http", "example.com", 80, "/"} : Url.Loc} - -# LAW (RFC 3986 §3.2.2): an IPv4 literal is the host -law url_ipv4_host: - {Url.parse("http://192.0.2.1/a") - == Url.Loc{"http", "192.0.2.1", 80, "/a"} : Url.Loc} - -# LAW (RFC 3986 §3.2.3): an explicit port wins over the scheme default -law url_ipv4_port: - {Url.parse("http://192.0.2.1:8080/a") - == Url.Loc{"http", "192.0.2.1", 8080, "/a"} : Url.Loc} - -# LAW (RFC 3986 §3.2.1): userinfo is stripped; it is not the host -law url_userinfo_stripped: - {Url.parse("http://user:pass@example.com/a") - == Url.Loc{"http", "example.com", 80, "/a"} : Url.Loc} - -# LAW (RFC 3986 §3.4, §3.5): the query stays on the path; the fragment does not -law url_query_drops_fragment: - {Url.parse("http://example.com/a/b?x=1#frag") - == Url.Loc{"http", "example.com", 80, "/a/b?x=1"} : Url.Loc} - -# LAW (RFC 3986 §3.3, §3.4): an empty path with a query is `/?query` -law url_query_on_empty_path: - {Url.parse("http://example.com?x=1") - == Url.Loc{"http", "example.com", 80, "/?x=1"} : Url.Loc} - -# LAW (RFC 3986 §3.1): the scheme is lowercased; the host is not -law url_scheme_lowercased: - {Url.parse("HTTPS://Example.com/x") - == Url.Loc{"https", "Example.com", 443, "/x"} : Url.Loc} - -# LAW (RFC 3986 §3.2.3 / RFC 2818): https with no port is 443 -law url_https_default_port: - {Url.parse("https://example.com/a") - == Url.Loc{"https", "example.com", 443, "/a"} : Url.Loc} - -# LAW (RFC 3986 §3.2.3): an explicit https port is kept -law url_https_explicit_port: - {Url.parse("https://example.com:8443/a") - == Url.Loc{"https", "example.com", 8443, "/a"} : Url.Loc} - -# --- RFC 9112 request and response writers --- - -# LAW (RFC 9112 §4, §9.6): a response status-line is HTTP/1.1 plus the -# reason-phrase, and the connection closes -# HTTP-SRV-5 -law respond_writes_reason_and_close: - for +status: U32 - for hs: List<&2, Http.Header> - for +body: String - {Http.respond(status, hs, body) - == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" - ++ "Connection: close\r\n" - ++ Http.headers.lines(hs) - ++ Http.clen.line(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)) - ++ "\r\n" - ++ body - : String} - -# LAW (RFC 9110 §9.3.4): PUT request-line opens with PUT and the path -law put_req_names_path: - for +host: String - for +path: String - for hs: List<&2, Http.Header> - for +body: String - {String.starts_with(Http.put_req(host, path, hs, body), "PUT " ++ path) - == True{} : Bool} - -# LAW (RFC 9110 §9.3.5): DELETE request-line opens with DELETE and the path -law delete_req_names_path: - for +host: String - for +path: String - for hs: List<&2, Http.Header> - for +body: String - {String.starts_with(Http.delete_req(host, path, hs, body), "DELETE " ++ path) - == True{} : Bool} - -# LAW (RFC 9110 §9.3.7): OPTIONS request-line opens with OPTIONS and the path -law options_req_names_path: - for +host: String - for +path: String - for hs: List<&2, Http.Header> - {String.starts_with(Http.options_req(host, path, hs), "OPTIONS " ++ path) - == True{} : Bool} - -# LAW (RFC 9110 §9.3): method tokens are case-sensitive -law method_tokens_are_case_sensitive: - {String.eq("GET", "get") == False{} : Bool} - -# LAW (RFC 9110 §9.2.1): GET, HEAD, and OPTIONS are safe; `get` is not GET -law method_safe_get: - {Http.method.safe("GET") == True{} : Bool} - -# LAW: a lower-case get is not the safe method GET -law method_safe_get_lower: - {Http.method.safe("get") == False{} : Bool} - -# LAW: POST is not safe -law method_safe_post: - {Http.method.safe("POST") == False{} : Bool} - -# LAW (RFC 9110 §9.2.2): PUT is idempotent -law method_idempotent_put: - {Http.method.idempotent("PUT") == True{} : Bool} - -# LAW: DELETE is idempotent -law method_idempotent_delete: - {Http.method.idempotent("DELETE") == True{} : Bool} - -# LAW: OPTIONS is idempotent because it is safe -law method_idempotent_options: - {Http.method.idempotent("OPTIONS") == True{} : Bool} - -# LAW: POST is not idempotent -law method_idempotent_post: - {Http.method.idempotent("POST") == False{} : Bool} - -# LAW (RFC 9110 §15.5.1): 400 Bad Request -law reason_400: - {Http.reason(400) == "Bad Request" : String} - -# LAW (RFC 9110 §15.5.5): 404 Not Found -law reason_404: - {Http.reason(404) == "Not Found" : String} - -# LAW (RFC 9110 §15.6.1): 500 Internal Server Error -law reason_500: - {Http.reason(500) == "Internal Server Error" : String} - -# LAW (RFC 9110 §9.3.2): a HEAD response is written with an empty body -law head_reply_drops_body: - {Http.reply.for("HEAD", Http.Reply{200, [], "secret"}) - == Http.Reply{200, [], ""} : Http.Reply} - -# LAW: any other method keeps the response body -law get_reply_keeps_body: - {Http.reply.for("GET", Http.Reply{200, [], "secret"}) - == Http.Reply{200, [], "secret"} : Http.Reply} - -# LAW (RFC 9110 §5.5): OWS around a field value is trimmed -law head_line_trims: - {Http.head.line("A: b ") == Some{Http.H{"A", "b"}} : Maybe<&2, Http.Header>} - -# LAW (RFC 9112 §5.2): a field value that still holds obs-fold is rejected -law head_line_rejects_obs_fold: - {Http.head.line("A: b\r\n c") == None{} : Maybe<&2, Http.Header>} - -# LAW: a continuation line with no colon is not a field -law head_line_rejects_continuation: - {Http.head.line(" folded") == None{} : Maybe<&2, Http.Header>} - -# LAW (RFC 9112 §3.2.1): a request-target without a leading `/` is not -# origin-form -law ask_rejects_relative_target: - {Http.ask("GET relative HTTP/1.1\r\nHost: h\r\n\r\n") - == Http.Bad{"the request-target is not in origin-form"} : Http.Request} - -# LAW (RFC 9112 §3.2.1): origin-form keeps an absolute-path and its query -law ask_origin_form_keeps_query: - {Http.ask("GET /ok?a=1 HTTP/1.1\r\nHost: h\r\n\r\n") - == Http.Request{"GET", "/ok?a=1", [Http.H{"Host", "h"}], ""} : Http.Request} - -# LAW (RFC 9112 §6.3): Content-Length and Transfer-Encoding together are torn -law parse_rejects_cl_and_te: - {Http.parse("HTTP/1.1 200 OK\r\nContent-Length: 1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n") - == Http.Torn{"Content-Length and Transfer-Encoding conflict"} : Http.Reply} - -# LAW (RFC 9112 §6.3): Content-Length values that differ are a bad request -law ask_rejects_disagreeing_cl: - {Http.ask("GET / HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 2\r\n\r\nab") - == Http.Bad{"Content-Length values disagree"} : Http.Request} - -# LAW (RFC 9112 §7.1): a non-hexadecimal chunk size is torn -law parse_rejects_non_hex_chunk: - {Http.parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nZ\r\n\r\n") - == Http.Torn{"chunk size is not hexadecimal"} : Http.Reply} - -# LAW (RFC 9112 §7.1): two non-empty chunks round-trip -law dechunk_two_chunks: - {Http.dechunk("1\r\nZ\r\n2\r\nOK\r\n0\r\n\r\n") == "ZOK" : String} - -# LAW (RFC 9112 §7.1): the chunked example wiki + pedia is wikipedia -law dechunk_wiki: - {Http.dechunk("4\r\nwiki\r\n5\r\npedia\r\n0\r\n\r\n") == "wikipedia" : String} - -# LAW (RFC 9110 §8.6): Content-Length takes that many octets and no more -law parse_clen_takes_prefix: - {Http.parse("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello!!") - == Http.Reply{200, [Http.H{"Content-Length", "5"}], "hello"} : Http.Reply} - -# LAW (RFC 9112 §7.1): a chunked response body is the concatenated chunks -law parse_chunked_body: - {Http.parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nwiki\r\n5\r\npedia\r\n0\r\n\r\n") - == Http.Reply{200, [Http.H{"Transfer-Encoding", "chunked"}], "wikipedia"} - : Http.Reply} - -# --- RFC 6750 / 7617 / 4648 credentials --- - -# LAW (RFC 6750 §2.1): a token with no whitespace is acceptable -law token_ok_plain: - {Http.token.ok("abc") == True{} : Bool} - -# LAW (RFC 6750 §2.1): a space is not a bearer token character -law token_ok_space: - {Http.token.ok("a b") == False{} : Bool} - -# LAW (RFC 6750 §2.1): a tab is not a bearer token character -law token_ok_tab: - {Http.token.ok("a\tb") == False{} : Bool} - -# LAW (RFC 4648 §4): the empty string encodes to empty -law b64_empty: - {B64.b64.encode("") == "" : String} - -# LAW (RFC 4648 §10): `f` encodes to `Zg==` -law b64_f: - {B64.b64.encode("f") == "Zg==" : String} - -# LAW (RFC 4648 §10): `fo` encodes to `Zm8=` -law b64_fo: - {B64.b64.encode("fo") == "Zm8=" : String} - -# LAW (RFC 4648 §10): `foo` encodes to `Zm9v` -law b64_foo: - {B64.b64.encode("foo") == "Zm9v" : String} - -# LAW (RFC 7617 §2): Basic credentials are Base64 of user:password -law basic_user_pass: - {Auth.auth.basic("user", "pass") - == Http.H{"Authorization", "Basic dXNlcjpwYXNz"} : Http.Header} - -# --- RFC 6265 cookies --- - -# LAW (RFC 6265 §4.1.1): attributes serialize with `; ` separators -law cookie_line_attributes: - {Cookie.cookie.line(Cookie.Cookie{"a", "b", - "Wed, 21 Oct 2015 07:28:00 GMT", "12", "example.com", "/app", - True{}, True{}, "Lax", False{}}) - == "a=b; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Max-Age=12; Domain=example.com; Path=/app; Secure; HttpOnly; SameSite=Lax" - : String} - -# LAW (RFC 6265 §4.1.1): Set-Cookie parses back to those attributes -law cookie_parse_round_trip: - {Cookie.cookie.parse("a=b; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Max-Age=12; Domain=example.com; Path=/app; Secure; HttpOnly; SameSite=Lax") - == Some{Cookie.Cookie{"a", "b", "Wed, 21 Oct 2015 07:28:00 GMT", "12", - "example.com", "/app", True{}, True{}, "Lax", False{}}} - : Maybe<&2, Cookie.Cookie>} - -# LAW (RFC 6265 §4.1.1): an empty cookie-name is rejected -law cookie_rejects_empty_name: - {Cookie.cookie.parse("=nope") == None{} : Maybe<&2, Cookie.Cookie>} - -# LAW (RFC 6265 §5.4): the Cookie header joins pairs with `; ` and drops an -# empty name -law cookie_request_joins: - {Cookie.cookie.join([Cookie.cookie.new("a", "b"), Cookie.cookie.new("c", "d"), - Cookie.cookie.new("", "x")]) - == "a=b; c=d" : String} - -# LAW (RFC 6265 §5.1.4): default-path is the directory of a path with two slashes -law cookie_default_path_dir: - {Cookie.cookie.default_path("/a/b") == "/a" : String} - -# LAW (RFC 6265 §5.1.4): fewer than two slashes means default-path `/` -law cookie_default_path_root: - {Cookie.cookie.default_path("/a") == "/" : String} - -# LAW (RFC 6265 §5.1.4): a trailing slash keeps the directory, not the slash -law cookie_default_path_trailing: - {Cookie.cookie.default_path("/foo/bar/") == "/foo/bar" : String} - -# LAW (RFC 6265 §5.4): a host-only cookie stores the response host and default-path -law cookie_receive_host_only: - {Cookie.cookie.line(Cookie.cookie.receive("Example.COM", "/a/b", - Cookie.cookie.new("a", "b"))) - == "a=b; Path=/a" : String} - -# LAW (RFC 6265 §5.1.3): a domain-match allows a leading-dot suffix -law cookie_domain_suffix: - {Cookie.cookie.domain_ok("www.example.com", "example.com", False{}) - == True{} : Bool} - -# LAW (RFC 6265 §5.1.3): a suffix without the separating dot does not match -law cookie_domain_not_suffix: - {Cookie.cookie.domain_ok("notexample.com", "example.com", False{}) - == False{} : Bool} - -# LAW (RFC 6265 §5.1.3): an IPv4 host does not domain-match a suffix -law cookie_domain_ip_suffix: - {Cookie.cookie.domain_ok("192.0.2.1", "2.1", False{}) == False{} : Bool} - -# LAW (RFC 6265 §5.1.3): an IPv4 host matches its own address exactly -law cookie_domain_ip_exact: - {Cookie.cookie.domain_ok("192.0.2.1", "192.0.2.1", False{}) == True{} : Bool} - -# LAW (RFC 6265 §5.1.4): a path-match allows a prefix whose next character is `/` -law cookie_path_prefix: - {Cookie.cookie.path_ok("/app/x", "/app") == True{} : Bool} - -# LAW (RFC 6265 §5.1.4): a longer segment is not a path-match -law cookie_path_not_prefix: - {Cookie.cookie.path_ok("/application", "/app") == False{} : Bool} - -# LAW (RFC 6265 §5.4): a Secure cookie is not sent on a cleartext request -law cookie_secure_needs_tls: - {Cookie.cookie.send.secure(False{}, True{}) == False{} : Bool} - -# LAW (RFC 6265 §5.4): a Secure cookie is sent when the request is TLS -law cookie_secure_on_tls: - {Cookie.cookie.send.secure(True{}, True{}) == True{} : Bool} - -# LAW (RFC 6265 §5.4): Max-Age 0 is not alive at age 0 -law cookie_max_age_zero: - {Cookie.cookie.alive("0", 0n) == False{} : Bool} - -# LAW (RFC 6265 §5.4): an age under Max-Age is still alive -law cookie_max_age_open: - {Cookie.cookie.alive("10", 0n) == True{} : Bool} - -# LAW (RFC 6265 §5.4): a session cookie (no Max-Age) stays sendable -law cookie_session_alive: - {Cookie.cookie.alive("", 5n) == True{} : Bool} - -# --- RFC 9111 cache directives --- - -# LAW (RFC 9111 §5.2): directive names are case-insensitive -law cache_directive_case: - {Cache.cache.parse("No-Store, MAX-AGE=0, Public") - == Cache.Cc{True{}, False{}, True{}, False{}, "0", ""} : Cache.Cc} - -# LAW (RFC 9111 §5.2): no-store, no-cache, public, private, and max-age -law cache_directives: - {Cache.cache.parse("no-store, no-cache, public, private, max-age=5") - == Cache.Cc{True{}, True{}, True{}, True{}, "5", ""} : Cache.Cc} - -# LAW (RFC 9111 §4.2.1): max-age=0 is stale at age 0 -law fresh_max_age_zero: - {Cache.cache.fresh(Cache.cache.parse("max-age=0"), 0n) == False{} : Bool} - -# LAW (RFC 9111 §4.2.1): an age under max-age is fresh -law fresh_max_age_open: - {Cache.cache.fresh(Cache.cache.parse("max-age=10"), 0n) == True{} : Bool} - -# LAW (RFC 9111 §4.2.1): an age equal to max-age is stale -law fresh_max_age_limit: - {Cache.cache.fresh(Cache.cache.parse("max-age=10"), 10n) == False{} : Bool} - -# LAW (RFC 9111 §5.2.2.2): no-cache is not fresh -law fresh_no_cache: - {Cache.cache.fresh(Cache.cache.parse("no-cache, max-age=10"), 0n) - == False{} : Bool} - -# LAW (RFC 9111 §5.2.2.3): no-store is not fresh -law fresh_no_store: - {Cache.cache.fresh(Cache.cache.parse("no-store, max-age=10"), 0n) - == False{} : Bool} - -# LAW (RFC 9111 §4.2.1): without max-age the response is not treated as fresh -law fresh_public_only: - {Cache.cache.fresh(Cache.cache.parse("public"), 0n) == False{} : Bool} - -# LAW (RFC 9111 §5.3): Expires is recorded beside Cache-Control -law cache_note_expires: - {Cache.cache.note("max-age=0", "Wed, 21 Oct 2099 07:28:00 GMT") - == Cache.Cc{False{}, False{}, False{}, False{}, "0", - "Wed, 21 Oct 2099 07:28:00 GMT"} : Cache.Cc} - -# LAW (RFC 9111 §4.2.1): max-age overrides Expires for freshness -law fresh_max_age_overrides_expires: - {Cache.cache.fresh(Cache.cache.note("max-age=0", - "Wed, 21 Oct 2099 07:28:00 GMT"), 0n) == False{} : Bool} - -# LAW (RFC 9111 §5.2.2.5): whatever else they say, directives with no-store -# are never fresh -law fresh_no_store_any: - for no_cache: Bool - for public: Bool - for private: Bool - for max_age: String - for expires: String - for age: Nat - {Cache.cache.fresh(Cache.Cc{True{}, no_cache, public, private, max_age, - expires}, age) == False{} : Bool} - -# LAW (RFC 9111 §5.2.2.4): whatever else they say, directives with no-cache -# are never fresh -law fresh_no_cache_any: - for no_store: Bool - for public: Bool - for private: Bool - for max_age: String - for expires: String - for age: Nat - {Cache.cache.fresh(Cache.Cc{no_store, True{}, public, private, max_age, - expires}, age) == False{} : Bool} - -# LAW (RFC 6265 §5.2.2): Max-Age replaces the cookie's max-age and nothing -# else -law cookie_age_sets: - for name: String - for value: String - for expires: String - for max_age: String - for domain: String - for path: String - for secure: Bool - for http_only: Bool - for same_site: String - for host_only: Bool - for age: String - {Cookie.cookie.age(Cookie.Cookie{name, value, expires, max_age, domain, path, - secure, http_only, same_site, host_only}, age) - == Cookie.Cookie{name, value, expires, age, domain, path, secure, - http_only, same_site, host_only} : Cookie.Cookie} - -# LAW: a preflight for a request that did not parse is 400 with its reason, -# whatever the allow-list -law cors_preflight_bad: - for cfg: Cors.Cfg - for why: String - {Cors.cors.preflight_req(cfg, Http.Bad{why}) == Http.Reply{400, [], why} - : Http.Reply} - -# LAW (RFC 4648 §4): at the end of the input, the quantum in progress is -# written out with its padding -law b64_go_end: - for phase: B64.Phase - {B64.b64.go([], phase) == B64.b64.end(phase) : String} - -# LAW (RFC 9111 §4.2.1): a present max-age is what freshness uses -law cache_max_age_present: - {Cache.cache.max_age(Cache.cache.parse("max-age=1")) == True{} : Bool} - -# --- Fetch CORS (https://fetch.spec.whatwg.org/#cors-protocol) --- - -# LAW (Fetch CORS): credentials reflect the request origin, never `*` -law cors_credentials_reflect_origin: - {Cors.cors.acao(["*"], True{}, "https://app.example") - == "https://app.example" : String} - -# LAW (Fetch CORS): without credentials, `*` stays `*` -law cors_star_without_credentials: - {Cors.cors.acao(["*"], False{}, "https://app.example") == "*" : String} - -# LAW (Fetch CORS): an explicit allow-list reflects the matching origin -law cors_explicit_origin: - {Cors.cors.acao(["https://app.example"], False{}, "https://app.example") - == "https://app.example" : String} - -# LAW (Fetch CORS): a missing origin is refused -law cors_refused_origin: - {Cors.cors.acao(["https://ok.example"], False{}, "https://no.example") - == "" : String} - -# LAW (Fetch CORS): Vary is set when ACAO is a specific origin -law cors_vary_specific: - {Cors.cors.vary("https://app.example") == True{} : Bool} - -# LAW (Fetch CORS): Vary is not set for `*` -law cors_vary_star: - {Cors.cors.vary("*") == False{} : Bool} - -# LAW (Fetch CORS): Vary is not set when the origin was refused -law cors_vary_empty: - {Cors.cors.vary("") == False{} : Bool} - -# LAW (Fetch CORS): an allowed preflight is 200 with the CORS headers -law cors_preflight_allowed: - {Cors.cors.preflight(Cors.Cfg{["https://app.example"], ["GET", "PUT"], - ["Content-Type"], True{}, "600", ["X-Trace"]}, - "https://app.example", "PUT", "Content-Type") - == Http.Reply{200, [ - Http.H{"Access-Control-Allow-Credentials", "true"}, - Http.H{"Vary", "Origin"}, - Http.H{"Access-Control-Allow-Origin", "https://app.example"}, - Http.H{"Access-Control-Allow-Methods", "GET, PUT"}, - Http.H{"Access-Control-Allow-Headers", "Content-Type"}, - Http.H{"Access-Control-Max-Age", "600"} - ], ""} : Http.Reply} - -# LAW (Fetch CORS): a method outside the allow-list is a failed preflight -law cors_preflight_method_denied: - {Cors.cors.preflight(Cors.Cfg{["https://app.example"], ["GET", "PUT"], - ["Content-Type"], True{}, "600", ["X-Trace"]}, - "https://app.example", "DELETE", "") - == Http.Reply{400, [], ""} : Http.Reply} - -# LAW (Fetch CORS): a simple response keeps its body and gains CORS headers -law cors_simple_reflects: - {Cors.cors.simple(Cors.Cfg{["https://app.example"], ["GET", "PUT"], - ["Content-Type"], True{}, "600", ["X-Trace"]}, - "https://app.example", Http.Reply{200, [], "ok"}) - == Http.Reply{200, [ - Http.H{"Access-Control-Allow-Credentials", "true"}, - Http.H{"Vary", "Origin"}, - Http.H{"Access-Control-Expose-Headers", "X-Trace"}, - Http.H{"Access-Control-Allow-Origin", "https://app.example"} - ], "ok"} : Http.Reply} - -# LAW (Fetch CORS): `*` without credentials is the only ACAO header -law cors_simple_star: - {Cors.cors.simple(Cors.Cfg{["*"], ["GET"], [], False{}, "", []}, - "https://app.example", Http.Reply{200, [], "ok"}) - == Http.Reply{200, [Http.H{"Access-Control-Allow-Origin", "*"}], "ok"} - : Http.Reply} - -# LAW (Fetch CORS): a refused origin leaves the handler reply unchanged -law cors_simple_refused: - {Cors.cors.simple(Cors.Cfg{["https://ok.example"], [], [], False{}, "", []}, - "https://no.example", Http.Reply{200, [], "ok"}) - == Http.Reply{200, [], "ok"} : Http.Reply} - -# LAW (Fetch CORS): OPTIONS plus Access-Control-Request-Method is a preflight -law cors_on_preflight: - {Cors.cors.on(Cors.Cfg{["https://app.example"], ["GET", "PUT"], - ["Content-Type"], True{}, "600", ["X-Trace"]}, - Http.Request{"OPTIONS", "/", [ - Http.H{"Origin", "https://app.example"}, - Http.H{"Access-Control-Request-Method", "PUT"}, - Http.H{"Access-Control-Request-Headers", "Content-Type"} - ], ""}, Http.Reply{200, [], "nope"}) - == Http.Reply{200, [ - Http.H{"Access-Control-Allow-Credentials", "true"}, - Http.H{"Vary", "Origin"}, - Http.H{"Access-Control-Allow-Origin", "https://app.example"}, - Http.H{"Access-Control-Allow-Methods", "GET, PUT"}, - Http.H{"Access-Control-Allow-Headers", "Content-Type"}, - Http.H{"Access-Control-Max-Age", "600"} - ], ""} : Http.Reply} - -# LAW (Fetch CORS): Origin is the request header of that name -law cors_origin_header: - {Cors.cors.origin("https://app.example") - == Http.H{"Origin", "https://app.example"} : Http.Header} - -# LAW (Fetch CORS): Access-Control-Request-Method names the method -law cors_request_method_header: - {Cors.cors.request_method("PUT") - == Http.H{"Access-Control-Request-Method", "PUT"} : Http.Header} - -# LAW (Fetch CORS): Access-Control-Request-Headers names the headers -law cors_request_headers_header: - {Cors.cors.request_headers("Content-Type") - == Http.H{"Access-Control-Request-Headers", "Content-Type"} : Http.Header} - -# --- thicker fence: more closed witnesses of the same RFCs --- - -# LAW (RFC 3986 §3.2.3): http with no port is 80 -law url_http_default_port: - {Url.parse("http://example.com/a") - == Url.Loc{"http", "example.com", 80, "/a"} : Url.Loc} - -# LAW (RFC 3986 §3.4): a later `?` stays inside the query -law url_query_keeps_question: - {Url.parse("http://example.com/a?x=1?y=2") - == Url.Loc{"http", "example.com", 80, "/a?x=1?y=2"} : Url.Loc} - -# LAW (RFC 3986 §3.5): a `?` inside the fragment is not a query -law url_fragment_hides_query: - {Url.parse("http://example.com/a#b?c") - == Url.Loc{"http", "example.com", 80, "/a"} : Url.Loc} - -# LAW (RFC 3986 §3.2.1): the last `@` piece is the hostport -law url_userinfo_last_at: - {Url.parse("http://a@b@example.com/z") - == Url.Loc{"http", "example.com", 80, "/z"} : Url.Loc} - -# LAW (RFC 3986 §3.3): a trailing slash is part of the path -law url_trailing_slash: - {Url.parse("http://example.com/a/") - == Url.Loc{"http", "example.com", 80, "/a/"} : Url.Loc} - -# LAW (RFC 3986 §3.2.3): a non-numeric port falls back to the scheme default -law url_bad_port_uses_default: - {Url.parse("http://example.com:abc/x") - == Url.Loc{"http", "example.com", 80, "/x"} : Url.Loc} - -# LAW (RFC 9110 §8.6): a non-empty request body is framed by Content-Length -law post_writes_content_length: - {Http.request("POST", "h", "/a", [], "hi") - == "POST /a HTTP/1.1\r\n" - ++ "Host: h\r\n" - ++ "User-Agent: ezhttp\r\n" - ++ "Accept: */*\r\n" - ++ "Connection: close\r\n" - ++ "Content-Length: 2\r\n" - ++ "\r\n" - ++ "hi" - : String} - -# LAW (RFC 9110 §8.6): an empty GET omits Content-Length -law get_omits_content_length: - {Http.get_req("h", "/", []) - == "GET / HTTP/1.1\r\n" - ++ "Host: h\r\n" - ++ "User-Agent: ezhttp\r\n" - ++ "Accept: */*\r\n" - ++ "Connection: close\r\n" - ++ "\r\n" - : String} - -# LAW (RFC 9110 §15.3.2): 201 Created -law reason_201: - {Http.reason(201) == "Created" : String} - -# LAW (RFC 9110 §15.3.5): 204 No Content -law reason_204: - {Http.reason(204) == "No Content" : String} - -# LAW (RFC 9110 §15.5.6): 405 Method Not Allowed -law reason_405: - {Http.reason(405) == "Method Not Allowed" : String} - -# LAW (RFC 9110 §15): a status we do not name has an empty reason-phrase -law reason_unknown_empty: - {Http.reason(418) == "" : String} - -# LAW (RFC 9110 §15.3.5): a 204 response is written with an empty body -law respond_204_drops_body: - {Http.respond.of(Http.Reply{204, [], "nope"}) == Http.respond(204, [], "") - : String} - -# LAW (RFC 9110 §15.4.5): a 304 response is written with an empty body -law respond_304_drops_body: - {Http.respond.of(Http.Reply{304, [], "nope"}) == Http.respond(304, [], "") - : String} - -# LAW (RFC 9110 §15.3.1): a 200 response keeps its body -law respond_200_keeps_body: - {Http.respond.of(Http.Reply{200, [], "ok"}) == Http.respond(200, [], "ok") - : String} - -# LAW (RFC 9110 §9.2.1): HEAD is safe -law method_safe_head: - {Http.method.safe("HEAD") == True{} : Bool} - -# LAW (RFC 9110 §9.2.1): OPTIONS is safe -law method_safe_options: - {Http.method.safe("OPTIONS") == True{} : Bool} - -# LAW (RFC 9110 §9.2.1): PUT is not safe -law method_safe_put: - {Http.method.safe("PUT") == False{} : Bool} - -# LAW (RFC 9110 §9.2.1): DELETE is not safe -law method_safe_delete: - {Http.method.safe("DELETE") == False{} : Bool} - -# LAW (RFC 9110 §9.2.2): GET is idempotent -law method_idempotent_get: - {Http.method.idempotent("GET") == True{} : Bool} - -# LAW (RFC 9110 §9.2.2): HEAD is idempotent -law method_idempotent_head: - {Http.method.idempotent("HEAD") == True{} : Bool} - -# LAW (RFC 9110 §9.2.2): PATCH is not one of the idempotent methods we name -law method_idempotent_patch: - {Http.method.idempotent("PATCH") == False{} : Bool} - -# LAW (RFC 9110 §5.1): field lookup folds the name and keeps the value -law headers_find_ignores_case: - {Http.headers.find([Http.H{"hOsT", "Ex.COM"}], "host") == "Ex.COM" : String} - -# LAW (RFC 9110 §5.1): a missing field looks up as empty -law headers_find_missing: - {Http.headers.find([], "host") == "" : String} - -# LAW (RFC 9110 §5.2): the first field of a repeated name is the one found -law headers_find_first: - {Http.headers.find([Http.H{"Host", "a"}, Http.H{"Host", "b"}], "host") - == "a" : String} - -# LAW (RFC 9110 §5.1): the wire spelling of a field name is kept -law ask_keeps_field_name_spelling: - {Http.ask("GET / HTTP/1.1\r\nhOsT: Ex.COM\r\n\r\n") - == Http.Request{"GET", "/", [Http.H{"hOsT", "Ex.COM"}], ""} : Http.Request} - -# LAW (RFC 9112 §6.3): identical Content-Length values are that one length -law parse_equal_content_lengths: - {Http.parse("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\nhello!!") - == Http.Reply{200, [Http.H{"Content-Length", "5"}, - Http.H{"Content-Length", "5"}], "hello"} : Http.Reply} - -# LAW (RFC 9112 §6.3): a request with both framings is bad -law ask_rejects_cl_and_te: - {Http.ask("POST / HTTP/1.1\r\nContent-Length: 1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n") - == Http.Bad{"Content-Length and Transfer-Encoding conflict"} : Http.Request} - -# LAW (RFC 9112 §6.3): a response whose Content-Length values differ is torn -law parse_rejects_disagreeing_cl: - {Http.parse("HTTP/1.1 200 OK\r\nContent-Length: 1\r\nContent-Length: 2\r\n\r\nab") - == Http.Torn{"Content-Length values disagree"} : Http.Reply} - -# LAW (RFC 9112 §6.3): identical request Content-Length values frame one body -law ask_equal_content_lengths: - {Http.ask("POST / HTTP/1.1\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\nhello!!") - == Http.Request{"POST", "/", [Http.H{"Content-Length", "5"}, - Http.H{"Content-Length", "5"}], "hello"} : Http.Request} - -# LAW (RFC 9112 §7.1): chunked is recognized in any case -law parse_chunked_ignores_case: - {Http.parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: CHUNKED\r\n\r\n1\r\nZ\r\n0\r\n\r\n") - == Http.Reply{200, [Http.H{"Transfer-Encoding", "CHUNKED"}], "Z"} - : Http.Reply} - -# LAW (RFC 9112 §7.1): a chunk-ext after `;` does not change the decoded body -law dechunk_ignores_extension: - {Http.dechunk("5;ext=1\r\nhello\r\n0\r\n\r\n") == "hello" : String} - -# LAW (RFC 9112 §7.1): the chunk of size 0 ends the body; a trailer is not data -law dechunk_stops_at_zero: - {Http.dechunk("5\r\nhello\r\n0\r\nIgnored: 1\r\n\r\n") == "hello" : String} - -# LAW (RFC 9112 §7.1): a body that is only the last chunk is empty -law dechunk_only_last: - {Http.dechunk("0\r\n\r\n") == "" : String} - -# LAW (RFC 9112 §7.1): chunk-size is hexadecimal, and an extension is skipped -law hexlen_extension_and_case: - {Http.hexlen("5;ext=1") == 5n : Nat} - -# LAW (RFC 9112 §7.1): `a` and `A` are the same chunk-size digit -law hexlen_alpha: - {Http.hexlen("a") == Http.hexlen("A") : Nat} - -# LAW (RFC 9112 §6.3): Content-Length is a decimal, not a prefix of junk -law digits_read_decimal: - {Http.digits.read("12") == Some{12n} : Maybe<&2, Nat>} - -# LAW (RFC 9112 §6.3): a Content-Length that is not all digits is absent -law digits_read_rejects_junk: - {Http.digits.read("12a") == None{} : Maybe<&2, Nat>} - -# LAW (RFC 9112 §3.2.4): OPTIONS asterisk-form names the whole server -law ask_options_asterisk: - {Http.ask("OPTIONS * HTTP/1.1\r\nHost: h\r\n\r\n") - == Http.Request{"OPTIONS", "*", [Http.H{"Host", "h"}], ""} : Http.Request} - -# LAW (RFC 9112 §3.2.4): asterisk-form is not a GET target -law ask_get_asterisk_rejected: - {Http.ask("GET * HTTP/1.1\r\nHost: h\r\n\r\n") - == Http.Bad{"the request-target is not in origin-form"} : Http.Request} - -# LAW (RFC 9112 §3.2.2): absolute-form is not accepted as origin-form -law ask_rejects_absolute_form: - {Http.ask("GET http://example.com/a HTTP/1.1\r\nHost: h\r\n\r\n") - == Http.Bad{"the request-target is not in origin-form"} : Http.Request} - -# LAW (RFC 4648 §10 / RFC 7617 §2): user:pass is the Basic credential alphabet -law b64_user_pass: - {B64.b64.encode("user:pass") == "dXNlcjpwYXNz" : String} - -# LAW (RFC 6750 §2.1): a newline is not a bearer token character -law token_ok_newline: - {Http.token.ok("a\nb") == False{} : Bool} - -# LAW (RFC 6265 §4.1.1): SameSite=STRICT is canonical Strict -law cookie_samesite_strict: - {Cookie.cookie.parse("a=b; SameSite=STRICT") - == Some{Cookie.Cookie{"a", "b", "", "", "", "", False{}, False{}, - "Strict", True{}}} : Maybe<&2, Cookie.Cookie>} - -# LAW (RFC 6265 §4.1.1): SameSite=none is canonical None -law cookie_samesite_none: - {Cookie.cookie.parse("a=b; SameSite=none") - == Some{Cookie.Cookie{"a", "b", "", "", "", "", False{}, False{}, - "None", True{}}} : Maybe<&2, Cookie.Cookie>} - -# LAW (RFC 6265 §4.1.1): an unknown SameSite attribute is dropped -law cookie_samesite_unknown: - {Cookie.cookie.parse("a=b; SameSite=weird") - == Some{Cookie.cookie.new("a", "b")} : Maybe<&2, Cookie.Cookie>} - -# LAW (RFC 6265 §5.2.2): a Max-Age that is not digits is ignored -law cookie_max_age_ignores_junk: - {Cookie.cookie.parse("a=b; Max-Age=nope") - == Some{Cookie.cookie.new("a", "b")} : Maybe<&2, Cookie.Cookie>} - -# LAW (RFC 6265 §5.2.3): a Domain attribute is stored lowercase -law cookie_domain_lowercased: - {Cookie.cookie.parse("a=b; Domain=Example.COM") - == Some{Cookie.Cookie{"a", "b", "", "", "example.com", "", False{}, - False{}, "", False{}}} : Maybe<&2, Cookie.Cookie>} - -# LAW (RFC 6265 §5.1.4): identical paths match -law cookie_path_identical: - {Cookie.cookie.path_ok("/app", "/app") == True{} : Bool} - -# LAW (RFC 6265 §5.1.4): default-path `/` matches a longer request path -law cookie_path_root: - {Cookie.cookie.path_ok("/a", "/") == True{} : Bool} - -# LAW (RFC 6265 §5.1.4): a cookie-path the request does not extend fails -law cookie_path_longer_cookie: - {Cookie.cookie.path_ok("/app", "/app/") == False{} : Bool} - -# LAW (RFC 6265 §5.1.4): a cookie-path that already ends in `/` is a prefix -law cookie_path_slash_prefix: - {Cookie.cookie.path_ok("/app/x", "/app/") == True{} : Bool} - -# LAW (RFC 6265 §5.1.4): one slash is not enough for a directory default-path -law cookie_default_path_one_slash: - {Cookie.cookie.default_path("/") == "/" : String} - -# LAW (RFC 6265 §5.1.3): an IPv4 literal is recognized, a name is not -law cookie_ipv4_literal: - {Cookie.cookie.ipv4("192.0.2.1") == True{} : Bool} - -# LAW (RFC 6265 §5.1.3): a registered name is not an IPv4 address -law cookie_ipv4_name: - {Cookie.cookie.ipv4("example.com") == False{} : Bool} - -# LAW (RFC 6265 §5.4): a host-only cookie is sent on its host and default-path -law cookie_send_host_only: - {Cookie.cookie.send("example.com", "/a/x", False{}, - Cookie.cookie.receive("Example.COM", "/a/b", Cookie.cookie.new("a", "b"))) - == True{} : Bool} - -# LAW (RFC 6265 §5.4): a host-only cookie is not sent to another host -law cookie_send_other_host: - {Cookie.cookie.send("other.com", "/", False{}, - Cookie.cookie.receive("example.com", "/", Cookie.cookie.new("a", "b"))) - == False{} : Bool} - -# LAW (RFC 6265 §5.4): a cookie with no Domain does not match a different host -law cookie_send_unset_domain: - {Cookie.cookie.send("h", "/", False{}, Cookie.cookie.new("a", "b")) - == False{} : Bool} - -# LAW (RFC 6265 §5.4): a cookie that is not Secure is sent on cleartext -law cookie_insecure_on_tcp: - {Cookie.cookie.send.secure(False{}, False{}) == True{} : Bool} - -# LAW (RFC 6265 §5.4): Max-Age 1 is alive at age 0 and stale at age 1 -law cookie_max_age_one: - {Bool.and(Cookie.cookie.alive("1", 0n), - Bool.not(Cookie.cookie.alive("1", 1n))) == True{} : Bool} - -# LAW (RFC 9111 §5.2): an unrecognized directive is ignored -law cache_ignores_s_maxage: - {Cache.cache.parse("s-maxage=9, max-age=3") - == Cache.Cc{False{}, False{}, False{}, False{}, "3", ""} : Cache.Cc} - -# LAW (RFC 9111 §5.2.1.1): a non-digit max-age is not recorded -law cache_max_age_ignores_junk: - {Cache.cache.parse("max-age=nope") - == Cache.Cc{False{}, False{}, False{}, False{}, "", ""} : Cache.Cc} - -# LAW (RFC 9111 §5.2.2.2): no-cache alone is recorded and not fresh -law cache_no_cache_alone: - {Cache.cache.parse("no-cache") - == Cache.Cc{False{}, True{}, False{}, False{}, "", ""} : Cache.Cc} - -# LAW (RFC 9111 §5.2.2.6): private alone is recorded -law cache_private_alone: - {Cache.cache.parse("private") - == Cache.Cc{False{}, False{}, False{}, True{}, "", ""} : Cache.Cc} - -# LAW (RFC 9111 §4.2.1): max-age=1 is fresh at 0 and stale at 1 -law fresh_max_age_one: - {Bool.and(Cache.cache.fresh(Cache.cache.parse("max-age=1"), 0n), - Bool.not(Cache.cache.fresh(Cache.cache.parse("max-age=1"), 1n))) - == True{} : Bool} - -# LAW (Fetch CORS): requested header names are matched case-insensitively -law cors_preflight_header_case: - {Cors.cors.preflight(Cors.Cfg{["https://app.example"], ["PUT"], - ["Content-Type"], False{}, "10", []}, - "https://app.example", "PUT", "content-type") - == Http.Reply{200, [ - Http.H{"Vary", "Origin"}, - Http.H{"Access-Control-Allow-Origin", "https://app.example"}, - Http.H{"Access-Control-Allow-Methods", "PUT"}, - Http.H{"Access-Control-Allow-Headers", "Content-Type"}, - Http.H{"Access-Control-Max-Age", "10"} - ], ""} : Http.Reply} - -# LAW (Fetch CORS): a preflight whose origin is not allowed is 400 -law cors_preflight_origin_denied: - {Cors.cors.preflight(Cors.Cfg{["https://app.example"], ["PUT"], - ["Content-Type"], False{}, "10", []}, - "https://no.example", "PUT", "") - == Http.Reply{400, [], ""} : Http.Reply} - -# LAW (Fetch CORS): a requested header outside the allow-list fails preflight -law cors_preflight_header_denied: - {Cors.cors.preflight(Cors.Cfg{["https://app.example"], ["PUT"], - ["Content-Type"], False{}, "10", []}, - "https://app.example", "PUT", "X-Nope") - == Http.Reply{400, [], ""} : Http.Reply} - -# LAW (Fetch CORS): credentials with an explicit origin still reflect it -law cors_credentials_explicit_origin: - {Cors.cors.acao(["https://app.example"], True{}, "https://app.example") - == "https://app.example" : String} - -# LAW (Fetch CORS): a GET with no Access-Control-Request-Method is not a preflight -law cors_get_is_not_preflight: - {Cors.cors.is_preflight(Http.Request{"GET", "/", - [Http.H{"Origin", "https://a"}], ""}) == False{} : Bool} - -# LAW: a JSON string is a text body of its compact encoding. The HTTP types -# stay text (the body is not a JSON value). -law json_string_is_text_body: - {Json.json.body(Ezjson.str("hi")) == Body.body.text("\"hi\"") : Body.Body} - -# LAW: a JSON string parses back to that string -law json_parse_string: - {Json.json.as_str(Json.json.parse("\"hi\"")) == Some{"hi"} : Maybe<&2, String>} - -# LAW: text that is not JSON is none -law json_parse_rejects: - {Json.json.as_str(Json.json.parse("nope")) == None{} : Maybe<&2, String>} - -# LAW (Fetch CORS): `*` without credentials is ACAO on a successful preflight -law cors_preflight_star: - {Cors.cors.preflight(Cors.Cfg{["*"], ["GET"], [], False{}, "0", []}, - "https://a", "GET", "") - == Http.Reply{200, [ - Http.H{"Access-Control-Allow-Origin", "*"}, - Http.H{"Access-Control-Allow-Methods", "GET"}, - Http.H{"Access-Control-Allow-Headers", ""}, - Http.H{"Access-Control-Max-Age", "0"} - ], ""} : Http.Reply} - -# --- the server's decisions (SPEC.md HTTP-SRV) --- - -# LAW: a request that does not parse is answered 400 with its reason, and -# the handler is not called -# HTTP-SRV-1 -law step_bad: - for why: String - {Step.step.of(Http.Bad{why}) == Step.Answer{Http.Reply{400, [], why}} - : Step.Step} - -# LAW: a request that parses goes to the handler exactly as it was parsed -# HTTP-SRV-2 -law step_call: - for method: String - for target: String - for hs: List<&2, Http.Header> - for body: String - {Step.step.of(Http.Request{method, target, hs, body}) - == Step.Call{Http.Request{method, target, hs, body}} : Step.Step} - -# LAW (RFC 9110 §9.3.2): a reply to HEAD loses its body and keeps the rest -# HTTP-SRV-3 -law shape_head: - for status: U32 - for hs: List<&2, Http.Header> - for body: String - {Http.reply.for("HEAD", Http.Reply{status, hs, body}) - == Http.Reply{status, hs, ""} : Http.Reply} - -# LAW: a reply to any method other than HEAD is left as it is -# HTTP-SRV-3 -law shape_other: - for method: String - for e: {False{} == String.eq(method, "HEAD") : Bool} - for reply: Http.Reply - {Http.reply.for(method, reply) == reply : Http.Reply} - -# LAW (RFC 9110 §15.3.5): a 204 is written with no body -# HTTP-SRV-3 -law respond_204: - for hs: List<&2, Http.Header> - for body: String - {Http.respond.of(Http.Reply{204, hs, body}) == Http.respond(204, hs, "") - : String} - -# LAW (RFC 9110 §15.4.5): a 304 is written with no body -# HTTP-SRV-3 -law respond_304: - for hs: List<&2, Http.Header> - for body: String - {Http.respond.of(Http.Reply{304, hs, body}) == Http.respond(304, hs, "") - : String} - -# LAW: any other status is written with its headers and body as they are -# HTTP-SRV-3 -law respond_other: - for +status: U32 - for e: {False{} == Http.status.nocontent(status) : Bool} - for hs: List<&2, Http.Header> - for body: String - {Http.respond.of(Http.Reply{status, hs, body}) - == Http.respond(status, hs, body) : String} - -# LAW: a failed accept or recv loses the connection with the line -# `ezhttp: : ` -# HTTP-SRV-4 -law lost_failed: - for -A: Type - for phase: String - for code: U32 - for why: String - {Step.step.got(A, phase, Fail{(code, why)}) - == Step.Lost{"ezhttp: " ++ phase ++ ": " ++ why} : Step.Got} - -# LAW: an accept or recv that succeeds hands on its value unchanged -# HTTP-SRV-4 -law lost_kept: - for -A: Type - for phase: String - for value: A - {Step.step.got(A, phase, Done{value}) == Step.Kept{value} : Step.Got} - -# LAW: a failed send loses the connection with the line `ezhttp: write: ` -# HTTP-SRV-4 -law lost_send: - for code: U32 - for why: String - for left: List<&2, U32> - {Step.step.sent(Fail{((code, why), left)}) - == Step.Lost{"ezhttp: write: " ++ why} : Step.Got} - -# LAW: a send that succeeds keeps the connection -# HTTP-SRV-4 -law lost_sent: - for done: Unit - {Step.step.sent(Done{done}) == Step.Kept{done} : Step.Got} - -# the reason-phrases SPEC.md HTTP-SRV-5 lists, as it words them -def phrases() -> List<&2, Http.Phrase>: - [Http.P{200, "OK"}, Http.P{201, "Created"}, Http.P{202, "Accepted"}, - Http.P{204, "No Content"}, Http.P{301, "Moved Permanently"}, - Http.P{302, "Found"}, Http.P{303, "See Other"}, - Http.P{304, "Not Modified"}, Http.P{307, "Temporary Redirect"}, - Http.P{308, "Permanent Redirect"}, Http.P{400, "Bad Request"}, - Http.P{401, "Unauthorized"}, Http.P{403, "Forbidden"}, - Http.P{404, "Not Found"}, Http.P{405, "Method Not Allowed"}, - Http.P{409, "Conflict"}, Http.P{410, "Gone"}, - Http.P{413, "Content Too Large"}, Http.P{415, "Unsupported Media Type"}, - Http.P{422, "Unprocessable Content"}, Http.P{429, "Too Many Requests"}, - Http.P{500, "Internal Server Error"}, Http.P{501, "Not Implemented"}, - Http.P{502, "Bad Gateway"}, Http.P{503, "Service Unavailable"}, - Http.P{504, "Gateway Timeout"}] - -# LAW: every status's reason-phrase is the one HTTP-SRV-5 lists for it, and -# empty for a status it does not list -# HTTP-SRV-5 -law reason_phrases: - for +code: U32 - {Http.reason(code) == Http.reason.in(phrases(), code) : String} - -# LAW: a list with no entries gives every status the empty phrase -# HTTP-SRV-5 -# HTTP-CLI-14 -law reason_in_nil: - for +code: U32 - {Http.reason.in([], code) == "" : String} - -# LAW: a list gives a status its first entry's phrase when that entry is for -# that status -# HTTP-SRV-5 -# HTTP-CLI-14 -law reason_in_hit: - for +code: U32 - for c: U32 - for text: String - for t: List<&2, Http.Phrase> - for e: {True{} == U32.is_eq(code, c) : Bool} - {Http.reason.in(Http.P{c, text} <> t, code) == text : String} - -# LAW: and the rest's phrase when it is for another status -# HTTP-SRV-5 -# HTTP-CLI-14 -law reason_in_miss: - for +code: U32 - for c: U32 - for text: String - for t: List<&2, Http.Phrase> - for e: {False{} == U32.is_eq(code, c) : Bool} - {Http.reason.in(Http.P{c, text} <> t, code) == Http.reason.in(t, code) - : String} - -# --- percent-encoding (SPEC.md HTTP-URL) --- - -# LAW: the empty string is written as the empty string, in both forms -# HTTP-URL-1 -law quote_nil: - for +slash: Bool - {Pct.quote(slash, "") == "" : String} - -# LAW: a string is written one character at a time, the first character's -# writing followed by the rest's -# HTTP-URL-1 -law quote_cons: - for +slash: Bool - for +c: U32 - for t: String - {Pct.quote(slash, SCon{Chr{c}, t}) - == Pct.one(slash, c, Pct.quote(slash, t)) : String} - -# LAW (RFC 3986 §2.3): an unreserved character is written as itself -# HTTP-URL-1 -law one_unreserved: - for slash: Bool - for +c: U32 - for e: {True{} == Pct.unreserved(c) : Bool} - for rest: String - {Pct.one(slash, c, rest) == SCon{Chr{c}, rest} : String} - -# LAW (RFC 3986 §2.1): a character the form does not keep is written as the -# %XX triples of its UTF-8 bytes -# HTTP-URL-2 -law one_escaped: - for slash: Bool - for +c: U32 - for e: {False{} == Pct.safe(slash, c) : Bool} - for rest: String - {Pct.one(slash, c, rest) == Pct.pct(Pct.utf8(c), rest) : String} - -# LAW: no bytes write nothing -# HTTP-URL-2 -law pct_nil: - for rest: String - {Pct.pct([], rest) == rest : String} - -# LAW (RFC 3986 §2.1): a byte is written as "%", its high nibble, its low -# nibble, before the bytes after it -# HTTP-URL-2 -law pct_cons: - for +b: U32 - for bs: List<&2, U32> - for rest: String - {Pct.pct(b <> bs, rest) - == SCon{'%', SCon{Pct.hex(U32.shrn(b, 4n)), - SCon{Pct.hex(U32.and(b, 15)), Pct.pct(bs, rest)}}} : String} - -# LAW: a nibble below 10 is the digit '0' + n -# HTTP-URL-2 -law hex_digit: - for +n: U32 - for e: {True{} == U32.is_lt(n, 10) : Bool} - {Pct.hex(n) == Chr{U32.add(48, n)} : Char} - -# LAW (RFC 3986 §2.1, upper case): a nibble of 10 or more is the letter -# 'A' + (n - 10), that is code 55 + n -# HTTP-URL-2 -law hex_letter: - for +n: U32 - for e: {False{} == U32.is_lt(n, 10) : Bool} - {Pct.hex(n) == Chr{U32.add(55, n)} : Char} - -# LAW (RFC 3629 §3): U+0000 to U+007F is one byte, 0xxxxxxx -# HTTP-URL-2 -law utf8_one: - for +c: U32 - for e: {True{} == U32.is_lt(c, 128) : Bool} - {Pct.utf8(c) == [c] : List<&2, U32>} - -# LAW (RFC 3629 §3): U+0080 to U+07FF is two bytes, 110xxxxx 10xxxxxx -# HTTP-URL-2 -law utf8_two: - for +c: U32 - for e1: {False{} == U32.is_lt(c, 128) : Bool} - for e2: {True{} == U32.is_lt(c, 2048) : Bool} - {Pct.utf8(c) == [U32.or(192, U32.shrn(c, 6n)), - U32.or(128, U32.and(c, 63))] : List<&2, U32>} - -# LAW (RFC 3629 §3): U+0800 to U+FFFF is three bytes, -# 1110xxxx 10xxxxxx 10xxxxxx -# HTTP-URL-2 -law utf8_three: - for +c: U32 - for e1: {False{} == U32.is_lt(c, 128) : Bool} - for e2: {False{} == U32.is_lt(c, 2048) : Bool} - for e3: {True{} == U32.is_lt(c, 65536) : Bool} - {Pct.utf8(c) == [U32.or(224, U32.shrn(c, 12n)), - U32.or(128, U32.and(U32.shrn(c, 6n), 63)), - U32.or(128, U32.and(c, 63))] : List<&2, U32>} - -# LAW (RFC 3629 §3): U+10000 to U+10FFFF is four bytes, -# 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx -# HTTP-URL-2 -law utf8_four: - for +c: U32 - for e1: {False{} == U32.is_lt(c, 128) : Bool} - for e2: {False{} == U32.is_lt(c, 2048) : Bool} - for e3: {False{} == U32.is_lt(c, 65536) : Bool} - {Pct.utf8(c) == [U32.or(240, U32.shrn(c, 18n)), - U32.or(128, U32.and(U32.shrn(c, 12n), 63)), - U32.or(128, U32.and(U32.shrn(c, 6n), 63)), - U32.or(128, U32.and(c, 63))] : List<&2, U32>} - -# LAW: the path form writes "/" as itself -# HTTP-URL-3 -law path_keeps_slash: - for rest: String - {Pct.one(True{}, 47, rest) == SCon{'/', rest} : String} - -# LAW: the component form writes "/" as %2F -# HTTP-URL-3 -law component_escapes_slash: - for rest: String - {Pct.one(False{}, 47, rest) == SCon{'%', SCon{'2', SCon{'F', rest}}} - : String} - -# LAW: on every character but "/" the component form writes what the path -# form writes -# HTTP-URL-3 -law component_is_path_elsewhere: - for +c: U32 - for e: {False{} == U32.is_eq(c, 47) : Bool} - for rest: String - {Pct.one(False{}, c, rest) == Pct.one(True{}, c, rest) : String} - -# an upper-case hex digit: 0-9 or A-F -def hexdig(ch: Char) -> Bool: - match ch: - case Chr{+code}: - Bool.or(Bool.and(U32.is_le(48, code), U32.is_le(code, 57)), - Bool.and(U32.is_le(65, code), U32.is_le(code, 70))) - -# whether character cp may come next, `need` hex digits still owed to a "%": -# with none owed, a kept character or a "%"; otherwise a hex digit -def framed.ok(slash: Bool, +cp: U32, need: Nat) -> Bool: - match need: - case 0n: - Bool.or(Pct.safe(slash, cp), U32.is_eq(cp, 37)) - case 1n+p: - hexdig(Chr{cp}) - -# hex digits owed after character cp: two after a "%", one fewer after a digit -def framed.next(slash: Bool, +cp: U32, need: Nat) -> Nat: - match need: - case 0n: - Bool.pick(Nat, Pct.safe(slash, cp), 0n, - Bool.pick(Nat, U32.is_eq(cp, 37), 2n, 0n)) - case 1n+p: - p - -# no hex digit is still owed at the end -def framed.end(need: Nat) -> Bool: - match need: - case 0n: - True{} - case 1n+p: - False{} - -# the HTTP-URL-4 frame read left to right: kept characters, and "%" followed -# by exactly two hex digits -def framed(+slash: Bool, str: String, +need: Nat) -> Bool: - match str: - case SNil{}: - framed.end(need) - case SCon{Chr{+cp}, tail}: - Bool.and(framed.ok(slash, cp, need), - framed(slash, tail, framed.next(slash, cp, need))) - -# both nibbles of every byte are written as hex digits -def hexes(bs: List<&2, U32>) -> Bool: - match bs: - case []: - True{} - case +byte <> tail: - Bool.and(hexdig(Pct.hex(U32.shrn(byte, 4n))), - Bool.and(hexdig(Pct.hex(U32.and(byte, 15))), hexes(tail))) - -# LAW (partial): a kept character stays in the frame -# HTTP-URL-4 -law frame_kept: - for +slash: Bool - for +c: U32 - for +e: {True{} == Pct.safe(slash, c) : Bool} - for rest: String - {framed(slash, Pct.one(slash, c, rest), 0n) == framed(slash, rest, 0n) - : Bool} - -# LAW (partial): %XX triples stay in the frame when their nibbles are hex -# digits -# HTTP-URL-4 -law frame_bytes: - for +slash: Bool - for bs: List<&2, U32> - for +e: {hexes(bs) == True{} : Bool} - for +rest: String - {framed(slash, Pct.pct(bs, rest), 0n) == framed(slash, rest, 0n) : Bool} - -# every character the form does not keep has UTF-8 bytes whose nibbles are -# written as hex digits -def nibbles(+slash: Bool, str: String) -> Bool: - match str: - case SNil{}: - True{} - case SCon{Chr{+cp}, tail}: - Bool.and(Bool.or(Pct.safe(slash, cp), hexes(Pct.utf8(cp))), - nibbles(slash, tail)) - -# LAW (partial): the whole output is in the frame, given that every escaped -# character's nibbles are written as hex digits -# HTTP-URL-4 -law frame_quote: - for +slash: Bool - for +str: String - for +e: {nibbles(slash, str) == True{} : Bool} - {framed(slash, Pct.quote(slash, str), 0n) == True{} : Bool} - -# --- the client's deadline (SPEC.md HTTP-CLI-1, 2) --- - -# LAW: an exchange made with no timeout gives every call 60000 ms -# HTTP-CLI-1 -law ms_none: - for m: Maybe<&2, U32> - for e: {None{} == m : Maybe<&2, U32>} - {Spec.wire.ms.of(m) == 60000 : U32} - -# LAW: a timeout of 1 ms or more is the deadline every call gets, unchanged -# HTTP-CLI-1 -# HTTP-CLI-2 -law ms_given: - for +ms: U32 - for e: {False{} == U32.is_eq(ms, 0) : Bool} - {Spec.wire.ms.of(Some{ms}) == ms : U32} - -# LAW: a timeout of 0 is a deadline of 1 ms (bend-kit-wire reads 0 as none) -# HTTP-CLI-1 -# HTTP-CLI-2 -law ms_zero: - for +ms: U32 - for e: {True{} == U32.is_eq(ms, 0) : Bool} - {Spec.wire.ms.of(Some{ms}) == 1 : U32} - -# --- what a failed socket call is reported as (SPEC.md HTTP-CLI-3, 13, 14) --- - -# LAW: a failed exchange with status 28 is reported as `timed out: ` -# HTTP-CLI-3 -law why_late: - for why: String - {Spec.wire.why(Some{28}, why) == "timed out: " ++ why : String} - -# LAW: a failed exchange with any other status keeps its reason unchanged -# HTTP-CLI-3 -law why_other: - for +n: U32 - for e: {False{} == U32.is_eq(n, 28) : Bool} - for why: String - {Spec.wire.why(Some{n}, why) == why : String} - -# LAW: a failed exchange with no status keeps its reason unchanged -# HTTP-CLI-3 -law why_none: - for why: String - {Spec.wire.why(None{}, why) == why : String} - -# LAW: the reason reported for status 28 is one timed_out recognizes -# HTTP-CLI-3 -law why_late_timed_out: - for why: String - {Spec.wire.timed_out(Spec.wire.why(Some{28}, why)) == True{} : Bool} - -# LAW (HTTP-CLI-13): a resolve that fails with any number but ENOENT is -# status 6 and names the host and the cause, ran out of time or not -# HTTP-CLI-13 -law fail_resolve: - for +host: String - for +port: U32 - for +ms: U32 - for +n: U32 - for e: {False{} == U32.is_eq(n, 2) : Bool} - for +why: String - {Spec.wire.fail(Spec.Resolve{}, host, port, ms, n, why) - == "6\ncould not resolve " ++ host ++ ": " ++ Spec.wire.cause(n) : String} - -# LAW: a name with no address (ENOENT) is status 6 and the host alone -# HTTP-CLI-13 -law fail_resolve_none: - for +host: String - for +port: U32 - for +ms: U32 - for +n: U32 - for e: {True{} == U32.is_eq(n, 2) : Bool} - for +why: String - {Spec.wire.fail(Spec.Resolve{}, host, port, ms, n, why) - == "6\ncould not resolve " ++ host : String} - -# LAW: a connect that ran out of time is status 28 and says how long it took -# HTTP-CLI-13 -law fail_connect_late: - for host: String - for +port: U32 - for +ms: U32 - for +n: U32 - for e: {True{} == U32.is_eq(n, 110) : Bool} - for why: String - {Spec.wire.fail(Spec.Connect{}, host, port, ms, n, why) - == "28\nconnecting to " ++ host ++ " port " ++ U32.show(port) - ++ " took over " ++ U32.show(ms) ++ " ms" : String} - -# LAW: a TLS handshake that ran out of time is status 28 -# HTTP-CLI-13 -law fail_handshake_late: - for host: String - for +port: U32 - for +ms: U32 - for +n: U32 - for e: {True{} == U32.is_eq(n, 110) : Bool} - for why: String - {Spec.wire.fail(Spec.Handshake{}, host, port, ms, n, why) - == "28\nthe TLS handshake with " ++ host ++ " took over " - ++ U32.show(ms) ++ " ms" : String} - -# LAW: a send that ran out of time is status 28 -# HTTP-CLI-13 -law fail_send_late: - for host: String - for +port: U32 - for +ms: U32 - for +n: U32 - for e: {True{} == U32.is_eq(n, 110) : Bool} - for why: String - {Spec.wire.fail(Spec.Send{}, host, port, ms, n, why) - == "28\nsending the request to " ++ host ++ " took over " - ++ U32.show(ms) ++ " ms" : String} - -# LAW: a receive that ran out of time is status 28 -# HTTP-CLI-13 -law fail_receive_late: - for host: String - for +port: U32 - for +ms: U32 - for +n: U32 - for e: {True{} == U32.is_eq(n, 110) : Bool} - for why: String - {Spec.wire.fail(Spec.Receive{}, host, port, ms, n, why) - == "28\n" ++ host ++ " sent nothing for " ++ U32.show(ms) ++ " ms" - : String} - -# LAW: a connect that failed otherwise is status 7 and names host, port and -# cause -# HTTP-CLI-13 -law fail_connect: - for host: String - for +port: U32 - for +ms: U32 - for +n: U32 - for e: {False{} == U32.is_eq(n, 110) : Bool} - for why: String - {Spec.wire.fail(Spec.Connect{}, host, port, ms, n, why) - == "7\ncould not reach " ++ host ++ " port " ++ U32.show(port) ++ ": " - ++ Spec.wire.cause(n) : String} - -# LAW: a TLS handshake that failed otherwise is status 35, with the cause and -# the reason bend-kit-wire gave -# HTTP-CLI-13 -law fail_handshake: - for host: String - for +port: U32 - for +ms: U32 - for +n: U32 - for e: {False{} == U32.is_eq(n, 110) : Bool} - for why: String - {Spec.wire.fail(Spec.Handshake{}, host, port, ms, n, why) - == "35\nthe TLS handshake with " ++ host ++ " failed: " - ++ Spec.wire.cause(n) ++ ": " ++ why : String} - -# LAW: a send that failed otherwise is status 55 -# HTTP-CLI-13 -law fail_send: - for host: String - for +port: U32 - for +ms: U32 - for +n: U32 - for e: {False{} == U32.is_eq(n, 110) : Bool} - for why: String - {Spec.wire.fail(Spec.Send{}, host, port, ms, n, why) - == "55\nthe request to " ++ host ++ " could not be sent: " - ++ Spec.wire.cause(n) : String} - -# LAW: a receive that failed otherwise is status 56 -# HTTP-CLI-13 -law fail_receive: - for host: String - for +port: U32 - for +ms: U32 - for +n: U32 - for e: {False{} == U32.is_eq(n, 110) : Bool} - for why: String - {Spec.wire.fail(Spec.Receive{}, host, port, ms, n, why) - == "56\nthe answer from " ++ host ++ " was cut short: " - ++ Spec.wire.cause(n) : String} - -# LAW: a cause is the error number's name, then the number in parentheses -# HTTP-CLI-13 -law cause_is: - for +n: U32 - {Spec.wire.cause(n) == Spec.wire.errno.name(n) ++ " (" ++ U32.show(n) ++ ")" - : String} - -# the Linux errno symbols HTTP-CLI-14 lists, as the law states them -def errnos() -> List<&2, Http.Phrase>: - [Http.P{1, "EPERM"}, Http.P{2, "ENOENT"}, Http.P{4, "EINTR"}, - Http.P{5, "EIO"}, Http.P{9, "EBADF"}, Http.P{11, "EAGAIN"}, - Http.P{12, "ENOMEM"}, Http.P{13, "EACCES"}, Http.P{22, "EINVAL"}, - Http.P{23, "ENFILE"}, Http.P{24, "EMFILE"}, Http.P{32, "EPIPE"}, - Http.P{71, "EPROTO"}, Http.P{88, "ENOTSOCK"}, - Http.P{93, "EPROTONOSUPPORT"}, Http.P{97, "EAFNOSUPPORT"}, - Http.P{98, "EADDRINUSE"}, Http.P{99, "EADDRNOTAVAIL"}, - Http.P{100, "ENETDOWN"}, Http.P{101, "ENETUNREACH"}, - Http.P{103, "ECONNABORTED"}, Http.P{104, "ECONNRESET"}, - Http.P{105, "ENOBUFS"}, Http.P{106, "EISCONN"}, - Http.P{107, "ENOTCONN"}, Http.P{110, "ETIMEDOUT"}, - Http.P{111, "ECONNREFUSED"}, Http.P{112, "EHOSTDOWN"}, - Http.P{113, "EHOSTUNREACH"}, Http.P{114, "EALREADY"}, - Http.P{115, "EINPROGRESS"}] - -# LAW: an error number's name is the one the table lists for it, or `errno` -# when the table lists none (reason_in_nil, reason_in_hit, reason_in_miss -# read the table) -# HTTP-CLI-14 -law errno_names: - for +n: U32 - {Spec.wire.errno.name(n) == Spec.wire.errno.or(Http.reason.in(errnos(), n)) - : String} - -# LAW: a number the table has no name for is named `errno` -# HTTP-CLI-14 -law errno_or_none: - for s: String - for e: {True{} == String.is_empty(s) : Bool} - {Spec.wire.errno.or(s) == "errno" : String} - -# LAW: a name the table gives is kept as it is -# HTTP-CLI-14 -law errno_or_named: - for s: String - for e: {False{} == String.is_empty(s) : Bool} - {Spec.wire.errno.or(s) == s : String} - -# --- the client's requests (SPEC.md HTTP-CLI-4) --- - -# LAW (RFC 9110 §7.2): on port 80 an http request's Host is the host alone -# HTTP-CLI-4 -law host_http_default: - for host: String - {Http.authority(False{}, host, 80) == host : String} - -# LAW (RFC 9110 §7.2): on port 443 an https request's Host is the host alone -# HTTP-CLI-4 -law host_https_default: - for host: String - {Http.authority(True{}, host, 443) == host : String} - -# LAW (RFC 9110 §7.2): on any other port an http request's Host is -# `host:port` -# HTTP-CLI-4 -law host_http_other: - for host: String - for +port: U32 - for e: {False{} == U32.is_eq(port, 80) : Bool} - {Http.authority(False{}, host, port) == host ++ ":" ++ U32.show(port) - : String} - -# LAW (RFC 9110 §7.2): on any other port an https request's Host is -# `host:port` -# HTTP-CLI-4 -law host_https_other: - for host: String - for +port: U32 - for e: {False{} == U32.is_eq(port, 443) : Bool} - {Http.authority(True{}, host, port) == host ++ ":" ++ U32.show(port) - : String} - -# LAW (RFC 9110 §7.2, RFC 9112 §3): the request a client writes for a host -# and port is its request line, the header fields request.fields gives for -# that Host value, a blank line and the body -# HTTP-CLI-4 -law request_at: - for +method: String - for secure: Bool - for host: String - for port: U32 - for +path: String - for hs: List<&2, Http.Header> - for +body: String - {Http.request.at(method, secure, host, port, path, hs, body) - == method ++ " " ++ path ++ " HTTP/1.1\r\n" - ++ Http.headers.lines( - Http.request.fields(Http.authority(secure, host, port), hs, body)) - ++ "\r\n" - ++ body - : String} - -# LAW: a URL that does not parse sends nothing and keeps its reason -# HTTP-CLI-4 -law exchange_bad: - for method: String - for why: String - for hs: List<&2, Http.Header> - for body: String - {Spec.wire.exchange(method, Url.Bad{why}, hs, body) == Spec.NoUrl{why} - : Spec.Exchange} - -# LAW: a URL that parses is sent to its host and port, over TLS exactly when -# its scheme is https, with the method it was given and the request -# request.at writes for them -# HTTP-CLI-4 -# HTTP-CLI-7 -law exchange_loc: - for method: String - for +scheme: String - for +host: String - for +port: U32 - for path: String - for hs: List<&2, Http.Header> - for body: String - {Spec.wire.exchange(method, Url.Loc{scheme, host, port, path}, hs, body) - == Spec.Exchange{String.eq(scheme, "https"), host, port, method, - Http.request.at(method, String.eq(scheme, "https"), host, port, path, hs, - body)} : Spec.Exchange} - -# LAW: the method the wire layer reads the answer by is the method the -# exchange was built for, for every method and every URL that parses -# HTTP-CLI-7 -law exchange_method: - for method: String - for scheme: String - for host: String - for port: U32 - for path: String - for hs: List<&2, Http.Header> - for body: String - {Spec.wire.exchange.method( - Spec.wire.exchange(method, Url.Loc{scheme, host, port, path}, hs, body)) - == Some{method} : Maybe<&2, String>} - -# LAW (RFC 9112 §3): for every method, the request a client writes opens with -# that method, the path and the protocol version on its request line -# HTTP-CLI-7 -law request_line: - for +method: String - for secure: Bool - for host: String - for port: U32 - for +path: String - for hs: List<&2, Http.Header> - for +body: String - {String.starts_with(Http.request.at(method, secure, host, port, path, hs, - body), method ++ " " ++ path ++ " HTTP/1.1\r\n") == True{} : Bool} - -# --- the request's header fields (SPEC.md HTTP-CLI-8) --- - -# LAW (RFC 9110 §5.1): a field name whose lower-case form is `host` is Host -# HTTP-CLI-8 -law field_host: - for +name: String - for e: {"host" == String.to_lower(name) : String} - {Http.field.name(name) == Http.FHost{} : Http.Field} - -# LAW (RFC 9110 §5.1): a field name whose lower-case form is `user-agent` is -# User-Agent -# HTTP-CLI-8 -law field_agent: - for +name: String - for e: {"user-agent" == String.to_lower(name) : String} - {Http.field.name(name) == Http.FAgent{} : Http.Field} - -# LAW (RFC 9110 §5.1): a field name whose lower-case form is `accept` is -# Accept -# HTTP-CLI-8 -law field_accept: - for +name: String - for e: {"accept" == String.to_lower(name) : String} - {Http.field.name(name) == Http.FAccept{} : Http.Field} - -# LAW (RFC 9110 §5.1): a field name whose lower-case form is `connection` is -# Connection -# HTTP-CLI-8 -law field_conn: - for +name: String - for e: {"connection" == String.to_lower(name) : String} - {Http.field.name(name) == Http.FConn{} : Http.Field} - -# LAW (RFC 9110 §5.1): a field name whose lower-case form is -# `content-length` is Content-Length -# HTTP-CLI-8 -law field_clen: - for +name: String - for e: {"content-length" == String.to_lower(name) : String} - {Http.field.name(name) == Http.FClen{} : Http.Field} - -# LAW (RFC 9110 §5.1): a field name whose lower-case form is none of the five -# is another field -# HTTP-CLI-8 -law field_other: - for +name: String - for eh: {False{} == String.eq(String.to_lower(name), "host") : Bool} - for ea: {False{} == String.eq(String.to_lower(name), "user-agent") : Bool} - for ec: {False{} == String.eq(String.to_lower(name), "accept") : Bool} - for en: {False{} == String.eq(String.to_lower(name), "connection") : Bool} - for el: {False{} == String.eq(String.to_lower(name), "content-length") - : Bool} - {Http.field.name(name) == Http.FOther{} : Http.Field} - -# LAW (RFC 9112 §3): the request is its request line, its header fields, a -# blank line and the body -# HTTP-CLI-8 -law request_text: - for +method: String - for +host: String - for +path: String - for hs: List<&2, Http.Header> - for +body: String - {Http.request(method, host, path, hs, body) - == method ++ " " ++ path ++ " HTTP/1.1\r\n" - ++ Http.headers.lines(Http.request.fields(host, hs, body)) - ++ "\r\n" - ++ body - : String} - -# LAW: a caller's Host fields replace the default: the request's Host fields -# are exactly the caller's, in order -# HTTP-CLI-8 -law fields_host_caller: - for +host: String - for +hs: List<&2, Http.Header> - for +body: String - for +h: Http.Header - for +t: List<&2, Http.Header> - for +e: {Http.fields.of(Http.FHost{}, hs) == h <> t : List<&2, Http.Header>} - {Http.fields.of(Http.FHost{}, Http.request.fields(host, hs, body)) - == h <> t : List<&2, Http.Header>} - -# LAW (RFC 9110 §7.2): with no Host field from the caller, the request's Host -# fields are exactly the default one -# HTTP-CLI-8 -law fields_host_dflt: - for +host: String - for +hs: List<&2, Http.Header> - for +body: String - for +e: {Http.fields.of(Http.FHost{}, hs) == [] : List<&2, Http.Header>} - {Http.fields.of(Http.FHost{}, Http.request.fields(host, hs, body)) - == [Http.H{"Host", host}] : List<&2, Http.Header>} - -# LAW: a caller's User-Agent fields replace the default: the request's -# User-Agent fields are exactly the caller's, in order -# HTTP-CLI-8 -law fields_agent_caller: - for +host: String - for +hs: List<&2, Http.Header> - for +body: String - for +h: Http.Header - for +t: List<&2, Http.Header> - for +e: {Http.fields.of(Http.FAgent{}, hs) == h <> t : List<&2, Http.Header>} - {Http.fields.of(Http.FAgent{}, Http.request.fields(host, hs, body)) - == h <> t : List<&2, Http.Header>} - -# LAW: with no User-Agent field from the caller, the request's User-Agent -# fields are exactly `User-Agent: ezhttp` -# HTTP-CLI-8 -law fields_agent_dflt: - for +host: String - for +hs: List<&2, Http.Header> - for +body: String - for +e: {Http.fields.of(Http.FAgent{}, hs) == [] : List<&2, Http.Header>} - {Http.fields.of(Http.FAgent{}, Http.request.fields(host, hs, body)) - == [Http.H{"User-Agent", "ezhttp"}] : List<&2, Http.Header>} - -# LAW: a caller's Accept fields replace the default: the request's Accept -# fields are exactly the caller's, in order -# HTTP-CLI-8 -law fields_accept_caller: - for +host: String - for +hs: List<&2, Http.Header> - for +body: String - for +h: Http.Header - for +t: List<&2, Http.Header> - for +e: {Http.fields.of(Http.FAccept{}, hs) == h <> t - : List<&2, Http.Header>} - {Http.fields.of(Http.FAccept{}, Http.request.fields(host, hs, body)) - == h <> t : List<&2, Http.Header>} - -# LAW: with no Accept field from the caller, the request's Accept fields are -# exactly `Accept: */*` -# HTTP-CLI-8 -law fields_accept_dflt: - for +host: String - for +hs: List<&2, Http.Header> - for +body: String - for +e: {Http.fields.of(Http.FAccept{}, hs) == [] : List<&2, Http.Header>} - {Http.fields.of(Http.FAccept{}, Http.request.fields(host, hs, body)) - == [Http.H{"Accept", "*/*"}] : List<&2, Http.Header>} - -# LAW (RFC 9112 §9.6): whatever the caller gave, the request's Connection -# fields are exactly `Connection: close` -# HTTP-CLI-8 -law fields_conn: - for +host: String - for +hs: List<&2, Http.Header> - for +body: String - {Http.fields.of(Http.FConn{}, Http.request.fields(host, hs, body)) - == [Http.H{"Connection", "close"}] : List<&2, Http.Header>} - -# LAW (RFC 9110 §8.6): whatever the caller gave, the request's Content-Length -# fields are exactly the one computed from the body -# HTTP-CLI-8 -law fields_clen: - for +host: String - for +hs: List<&2, Http.Header> - for +body: String - {Http.fields.of(Http.FClen{}, Http.request.fields(host, hs, body)) - == Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)) - : List<&2, Http.Header>} - -# LAW: the caller's fields of every other name are sent unchanged and in -# order, and the request adds none of its own -# HTTP-CLI-8 -law fields_other: - for +host: String - for +hs: List<&2, Http.Header> - for +body: String - {Http.fields.of(Http.FOther{}, Http.request.fields(host, hs, body)) - == Http.fields.of(Http.FOther{}, hs) : List<&2, Http.Header>} - -# LAW (RFC 9110 §7.2, RFC 9112 §3, §9.6): a caller that names none of the -# five defaulted fields gets the request ezhttp has always written: Host, -# User-Agent, Accept, Connection: close, its own fields, Content-Length -# HTTP-CLI-8 -law request_plain: - for +method: String - for +host: String - for +path: String - for +hs: List<&2, Http.Header> - for +body: String - for +eh: {Http.fields.of(Http.FHost{}, hs) == [] : List<&2, Http.Header>} - for +ea: {Http.fields.of(Http.FAgent{}, hs) == [] : List<&2, Http.Header>} - for +ec: {Http.fields.of(Http.FAccept{}, hs) == [] : List<&2, Http.Header>} - for +eo: {Http.fields.of(Http.FOther{}, hs) == hs : List<&2, Http.Header>} - {Http.request(method, host, path, hs, body) - == method ++ " " ++ path ++ " HTTP/1.1\r\n" - ++ "Host: " ++ host ++ "\r\n" - ++ "User-Agent: ezhttp\r\n" - ++ "Accept: */*\r\n" - ++ "Connection: close\r\n" - ++ Http.headers.lines(hs) - ++ Http.clen.line(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)) - ++ "\r\n" - ++ body - : String} - -# --- when a response is whole (SPEC.md HTTP-CLI-5) --- - -# LAW: a text is read head by head for the request's method, at most as many -# heads as it has characters -# HTTP-CLI-5 -law whole_go: - for +method: String - for +raw: String - {Http.whole(method, raw) - == Http.whole.go(String.length(raw), String.eq(method, "HEAD"), - Http.divide(raw, "\r\n\r\n")) : Bool} - -# LAW: a read with no heads left is not whole -# HTTP-CLI-5 -law whole_fuel: - for asked: Bool - for c: Http.Cut - {Http.whole.go(0n, asked, c) == False{} : Bool} - -# LAW (RFC 9112 §2.1): a response whose header section has not ended is not -# whole -# HTTP-CLI-5 -law whole_open: - for f: Nat - for asked: Bool - {Http.whole.go(1n+f, asked, Http.NoCut{}) == False{} : Bool} - -# LAW (RFC 9110 §15.2): after an interim (1xx) head, the response is whole -# exactly when the text after that head is -# HTTP-CLI-5 -law whole_interim: - for f: Nat - for +asked: Bool - for +before: String - for +after: String - for e: {True{} == Http.status.interim(Http.status.of(String.lines(before))) - : Bool} - {Http.whole.go(1n+f, asked, Http.Cut{before, after}) - == Http.whole.go(f, asked, Http.divide(after, "\r\n\r\n")) : Bool} - -# LAW (RFC 9112 §6.3): once a final header section has ended, a response is -# whole exactly when its body is, under the framing its head and the -# request's method give -# HTTP-CLI-5 -law whole_head: - for f: Nat - for +asked: Bool - for +before: String - for +after: String - for e: {False{} == Http.status.interim(Http.status.of(String.lines(before))) - : Bool} - {Http.whole.go(1n+f, asked, Http.Cut{before, after}) - == Http.whole.of(Http.frame(Bool.or(asked, - Http.status.bare(Http.status.of(String.lines(before)))), - Http.head.go(String.lines(before), "transfer-encoding"), - Http.head.values(String.lines(before), "content-length")), after) - : Bool} - -# LAW (RFC 9110 §15.2): a status is interim exactly when it is 100 to 199 -# HTTP-CLI-5 -# HTTP-CLI-9 -law status_interim_some: - for +n: U32 - {Http.status.interim(Some{n}) - == Bool.and(U32.is_le(100, n), U32.is_lt(n, 200)) : Bool} - -# LAW: a status line that does not parse is not interim -# HTTP-CLI-5 -# HTTP-CLI-9 -law status_interim_none: - for m: Maybe<&2, U32> - for e: {None{} == m : Maybe<&2, U32>} - {Http.status.interim(m) == False{} : Bool} - -# LAW (RFC 9112 §4): a status line's code is its second word, as a number -# HTTP-CLI-5 -law status_of: - for ls: List<&2, String> - {Http.status.of(ls) == U32.read(Http.second(String.split(Http.first(ls), ' '))) - : Maybe<&2, U32>} - -# LAW (RFC 9110 §15.3.5, §15.4.5): a 204 or a 304 has no body -# HTTP-CLI-5 -law status_bare_some: - for +n: U32 - {Http.status.bare(Some{n}) == Bool.or(U32.is_eq(n, 204), U32.is_eq(n, 304)) - : Bool} - -# LAW: a status line that does not parse leaves the framing to the headers -# HTTP-CLI-5 -law status_bare_none: - for m: Maybe<&2, U32> - for e: {None{} == m : Maybe<&2, U32>} - {Http.status.bare(m) == False{} : Bool} - -# LAW (RFC 9110 §9.3.2, RFC 9112 §6.3 item 1): a reply to HEAD, a 204 and a -# 304 have no body, whatever their fields say -# HTTP-CLI-5 -law frame_bare: - for te: String - for lens: List<&2, String> - {Http.frame(True{}, te, lens) == Http.Bare{} : Http.Frame} - -# LAW (RFC 9112 §6.3 items 3, 4): Transfer-Encoding chunked frames the body, -# Content-Length or not -# HTTP-CLI-5 -law frame_chunked: - for te: String - for +lens: List<&2, String> - for e: {True{} == String.contains(String.to_lower(te), "chunked") : Bool} - {Http.frame(False{}, te, lens) == Http.Chunked{} : Http.Frame} - -# LAW (RFC 9112 §6.3 item 6): otherwise Content-Length values that agree on -# a number frame the body at that many octets -# HTTP-CLI-5 -law frame_sized: - for te: String - for +lens: List<&2, String> - for e1: {False{} == String.contains(String.to_lower(te), "chunked") : Bool} - for e2: {True{} == Http.clen.agree(lens) : Bool} - for n: Nat - for e3: {Some{n} == Http.digits.read(Http.first(lens)) : Maybe<&2, Nat>} - {Http.frame(False{}, te, lens) == Http.Sized{n} : Http.Frame} - -# LAW (RFC 9112 §6.3 items 5, 8): Content-Length values that disagree leave -# the body to the close -# HTTP-CLI-5 -law frame_disagree: - for te: String - for +lens: List<&2, String> - for e1: {False{} == String.contains(String.to_lower(te), "chunked") : Bool} - for e2: {False{} == Http.clen.agree(lens) : Bool} - {Http.frame(False{}, te, lens) == Http.Closed{} : Http.Frame} - -# LAW (RFC 9112 §6.3 item 8): no Content-Length, or one that is not a -# number, leaves the body to the close -# HTTP-CLI-5 -law frame_unsized: - for te: String - for +lens: List<&2, String> - for e1: {False{} == String.contains(String.to_lower(te), "chunked") : Bool} - for e2: {True{} == Http.clen.agree(lens) : Bool} - for e3: {None{} == Http.digits.read(Http.first(lens)) : Maybe<&2, Nat>} - {Http.frame(False{}, te, lens) == Http.Closed{} : Http.Frame} - -# LAW: a response with no body is whole as soon as its head is -# HTTP-CLI-5 -law whole_bare: - for +body: String - {Http.whole.of(Http.Bare{}, body) == True{} : Bool} - -# LAW (RFC 9110 §8.6): a sized body is whole once it holds that many octets -# HTTP-CLI-5 -law whole_sized: - for n: Nat - for +body: String - {Http.whole.of(Http.Sized{n}, body) - == Bool.not(Nat.is_lt(Http.octs.len(body), n)) : Bool} - -# LAW (RFC 9112 §7.1): a chunked body is whole once it reaches the end of its -# last chunk, read from its first chunk header -# HTTP-CLI-5 -law whole_chunked: - for +body: String - {Http.whole.of(Http.Chunked{}, body) - == Http.chunks.ended(String.length(body), Http.divide(body, "\r\n")) - : Bool} - -# LAW (RFC 9112 §6.3 item 8): a body framed by the close is never whole -# before the close -# HTTP-CLI-5 -law whole_closed: - for +body: String - {Http.whole.of(Http.Closed{}, body) == False{} : Bool} - -# LAW: a chunk walk with no fuel left has not ended -# HTTP-CLI-5 -law ended_fuel: - for c: Http.Cut - {Http.chunks.ended(0n, c) == False{} : Bool} - -# LAW (RFC 9112 §7.1): a chunk header that has not all arrived has not ended -# HTTP-CLI-5 -law ended_open: - for f: Nat - {Http.chunks.ended(1n+f, Http.NoCut{}) == False{} : Bool} - -# LAW (RFC 9112 §7.1): a chunk size that is not hexadecimal ends the read, -# and the parse rejects it -# HTTP-CLI-5 -law ended_bad: - for f: Nat - for +before: String - for +after: String - for e: {False{} == Http.hex.well(Http.chunks.token(before)) : Bool} - {Http.chunks.ended(1n+f, Http.Cut{before, after}) == True{} : Bool} - -# LAW (RFC 9112 §7.1.2): the zero-size chunk ends the body once its trailer -# section has ended -# HTTP-CLI-5 -law ended_last: - for f: Nat - for +before: String - for +after: String - for e1: {True{} == Http.hex.well(Http.chunks.token(before)) : Bool} - for e2: {True{} == Nat.is_eq(Http.hexlen(before), 0n) : Bool} - {Http.chunks.ended(1n+f, Http.Cut{before, after}) - == Http.chunks.trailer(after) : Bool} - -# LAW (RFC 9112 §7.1): a chunk that has all arrived, with its CRLF, moves on -# to the next chunk header -# HTTP-CLI-5 -law ended_next: - for f: Nat - for +before: String - for +after: String - for e1: {True{} == Http.hex.well(Http.chunks.token(before)) : Bool} - for e2: {False{} == Nat.is_eq(Http.hexlen(before), 0n) : Bool} - for e3: {True{} == Http.chunks.ready(after, Http.hexlen(before)) : Bool} - {Http.chunks.ended(1n+f, Http.Cut{before, after}) - == Http.chunks.ended(f, Http.divide(Http.octs.drop(after, - Nat.add(Http.hexlen(before), 2n)), "\r\n")) : Bool} - -# LAW (RFC 9112 §7.1): a chunk that has not all arrived has not ended -# HTTP-CLI-5 -law ended_short: - for f: Nat - for +before: String - for +after: String - for e1: {True{} == Http.hex.well(Http.chunks.token(before)) : Bool} - for e2: {False{} == Nat.is_eq(Http.hexlen(before), 0n) : Bool} - for e3: {False{} == Http.chunks.ready(after, Http.hexlen(before)) : Bool} - {Http.chunks.ended(1n+f, Http.Cut{before, after}) == False{} : Bool} - -# LAW (RFC 9112 §7.1.2): a trailer section has ended at once (CRLF), or after -# its fields (a blank line) -# HTTP-CLI-5 -law trailer_ended: - for +after: String - {Http.chunks.trailer(after) == Bool.or(String.starts_with(after, "\r\n"), - String.contains(after, "\r\n\r\n")) : Bool} - -# LAW (RFC 9112 §7.1): a chunk of size n has arrived when n octets and a -# CRLF follow its header -# HTTP-CLI-5 -law chunk_ready: - for +after: String - for +n: Nat - {Http.chunks.ready(after, n) - == Bool.and(Bool.not(Nat.is_lt(Http.octs.len(after), Nat.add(n, 2n))), - String.starts_with(Http.octs.drop(after, n), "\r\n")) : Bool} - -# --- octet bodies on the wire (SPEC.md HTTP-CLI-10) --- - -# LAW: a text body is sent inside the request text, as HTTP-CLI-4 says -# HTTP-CLI-10 -law exchange_body_text: - for method: String - for l: Url.Loc - for hs: List<&2, Http.Header> - for text: String - {Spec.wire.exchange.body(method, l, hs, - Pair.fst(Body.Shape, Bytes.Bytes, Body.body.shape(Body.Plain{text}))) - == Spec.wire.exchange(method, l, hs, text) : Spec.Exchange} - -# LAW: an empty body is sent as the empty text body -# HTTP-CLI-10 -law exchange_body_empty: - for method: String - for l: Url.Loc - for hs: List<&2, Http.Header> - {Spec.wire.exchange.body(method, l, hs, - Pair.fst(Body.Shape, Bytes.Bytes, Body.body.shape(Body.Empty{}))) - == Spec.wire.exchange(method, l, hs, "") : Spec.Exchange} - -# LAW: an octet body's request text is built for its number of octets -# HTTP-CLI-10 -law exchange_body_octets: - for method: String - for l: Url.Loc - for hs: List<&2, Http.Header> - for +n: U32 - for buf: Array - {Spec.wire.exchange.body(method, l, hs, - Pair.fst(Body.Shape, Bytes.Bytes, - Body.body.shape(Body.Octets{Bytes.Bytes{n, buf}}))) - == Spec.wire.exchange.octets(method, l, hs, U32.to_nat(n)) - : Spec.Exchange} - -# LAW: a URL that does not parse sends nothing and keeps its reason -# HTTP-CLI-10 -law exchange_octets_bad: - for method: String - for why: String - for hs: List<&2, Http.Header> - for n: Nat - {Spec.wire.exchange.octets(method, Url.Bad{why}, hs, n) == Spec.NoUrl{why} - : Spec.Exchange} - -# LAW: a URL that parses is sent to its host and port, over TLS exactly when -# its scheme is https, with the method it was given and the request text -# request.head.at writes for n octets -# HTTP-CLI-10 -law exchange_octets_loc: - for +method: String - for +scheme: String - for +host: String - for +port: U32 - for path: String - for hs: List<&2, Http.Header> - for n: Nat - {Spec.wire.exchange.octets(method, Url.Loc{scheme, host, port, path}, hs, n) - == Spec.Exchange{String.eq(scheme, "https"), host, port, method, - Http.request.head.at(method, String.eq(scheme, "https"), host, port, - path, hs, n)} : Spec.Exchange} - -# LAW (RFC 9112 §3): the request text ahead of n octets is the request line, -# the header fields for n octets, and the blank line, nothing after it -# HTTP-CLI-10 -law request_head_at: - for +method: String - for secure: Bool - for host: String - for port: U32 - for +path: String - for hs: List<&2, Http.Header> - for n: Nat - {Http.request.head.at(method, secure, host, port, path, hs, n) - == method ++ " " ++ path ++ " HTTP/1.1\r\n" - ++ Http.headers.lines( - Http.request.fields.n(Http.authority(secure, host, port), hs, n)) - ++ "\r\n" - : String} - -# LAW (RFC 9110 §8.6): a text body's header fields are the fields for its -# UTF-8 octets -# HTTP-CLI-10 -law fields_n: - for host: String - for +hs: List<&2, Http.Header> - for +body: String - {Http.request.fields(host, hs, body) - == Http.request.fields.n(host, hs, Http.utf8.len(body)) - : List<&2, Http.Header>} - -# LAW (RFC 9110 §8.6): whatever the caller gave, the Content-Length fields for -# n octets are exactly `Content-Length: n`, and none for none -# HTTP-CLI-10 -law fields_clen_n: - for +host: String - for +hs: List<&2, Http.Header> - for +n: Nat - {Http.fields.of(Http.FClen{}, Http.request.fields.n(host, hs, n)) - == Http.clen.field(n, Nat.is_eq(n, 0n)) : List<&2, Http.Header>} - -# LAW: what is sent ahead of the body's octets is the request text's octets -# HTTP-CLI-10 -law payload_is: - for req: String - {Spec.wire.payload(req) == Spec.wire.payload.text(req, "") : String} - -# LAW: an empty request text writes no octets ahead of what follows it -# HTTP-CLI-10 -law payload_nil: - for rest: String - {Spec.wire.payload.text("", rest) == rest : String} - -# LAW (RFC 3629 §3): every character of the request text is sent as its -# UTF-8 octets, in order, ahead of the rest -# HTTP-CLI-10 -law payload_cons: - for +cp: U32 - for t: String - for rest: String - {Spec.wire.payload.text(SCon{Chr{cp}, t}, rest) - == Body.octets.chars(Pct.utf8(cp)) ++ Spec.wire.payload.text(t, rest) - : String} - -# --- the response, read by the request's method (SPEC.md HTTP-CLI-9) --- - -# LAW: a response is read head by head for the request's method, at most as -# many heads as the text has characters -# HTTP-CLI-9 -law reply_to: - for +method: String - for +raw: String - {Http.parse.to(method, raw) - == Http.parse.to.go(String.length(raw), String.eq(method, "HEAD"), - Http.divide(raw, "\r\n\r\n")) : Http.Reply} - -# LAW: a read with no heads left found no final head -# HTTP-CLI-9 -law reply_fuel: - for head: Bool - for c: Http.Cut - {Http.parse.to.go(0n, head, c) - == Http.Torn{"the response head has no blank line after it"} - : Http.Reply} - -# LAW (RFC 9112 §2.1): a head that never ends is torn -# HTTP-CLI-9 -law reply_open: - for f: Nat - for head: Bool - {Http.parse.to.go(1n+f, head, Http.NoCut{}) - == Http.Torn{"the response head has no blank line after it"} - : Http.Reply} - -# LAW (RFC 9110 §15.2): an interim head with text after it is skipped, and the -# text after it is read in its place -# HTTP-CLI-9 -law reply_interim: - for f: Nat - for +head: Bool - for +before: String - for +after: String - for e: {True{} == Http.interim.skip(Http.status.of(String.lines(before)), - after) : Bool} - {Http.parse.to.go(1n+f, head, Http.Cut{before, after}) - == Http.parse.to.go(f, head, Http.divide(after, "\r\n\r\n")) : Http.Reply} - -# LAW (RFC 9110 §6.4.1, §9.3.2; RFC 9112 §6.3 item 1): a final response to -# HEAD, or with a 1xx, 204 or 304 status, has its status, its fields and no -# body, whatever its framing fields say -# HTTP-CLI-9 -law reply_bare: - for f: Nat - for +head: Bool - for +before: String - for +after: String - for e1: {False{} == Http.interim.skip(Http.status.of(String.lines(before)), - after) : Bool} - for e2: {True{} == Bool.or(head, - Http.status.empty(Http.status.of(String.lines(before)))) : Bool} - for n: U32 - for e3: {Some{n} == Http.status.of(String.lines(before)) : Maybe<&2, U32>} - {Http.parse.to.go(1n+f, head, Http.Cut{before, after}) - == Http.Reply{n, Http.head.list(String.lines(before)), ""} : Http.Reply} - -# LAW (RFC 9112 §4): a final head with no status code is torn -# HTTP-CLI-9 -law reply_bare_none: - for f: Nat - for +head: Bool - for +before: String - for +after: String - for e1: {False{} == Http.interim.skip(Http.status.of(String.lines(before)), - after) : Bool} - for e2: {True{} == Bool.or(head, - Http.status.empty(Http.status.of(String.lines(before)))) : Bool} - for e3: {None{} == Http.status.of(String.lines(before)) : Maybe<&2, U32>} - {Http.parse.to.go(1n+f, head, Http.Cut{before, after}) - == Http.Torn{"the response has no status line"} : Http.Reply} - -# LAW: any other final response is read as parse_response reads that head and -# the text after it -# HTTP-CLI-9 -law reply_framed: - for f: Nat - for +head: Bool - for +before: String - for +after: String - for e1: {False{} == Http.interim.skip(Http.status.of(String.lines(before)), - after) : Bool} - for e2: {False{} == Bool.or(head, - Http.status.empty(Http.status.of(String.lines(before)))) : Bool} - {Http.parse.to.go(1n+f, head, Http.Cut{before, after}) - == Http.parse.head(String.lines(before), after) : Http.Reply} - -# LAW (RFC 9110 §15.2): a head is skipped exactly when it is interim and text -# follows it -# HTTP-CLI-9 -law interim_skip: - for m: Maybe<&2, U32> - for after: String - {Http.interim.skip(m, after) - == Bool.and(Http.status.interim(m), Bool.not(String.is_empty(after))) - : Bool} - -# LAW (RFC 9110 §6.4.1): a 1xx, 204 or 304 status has no body -# HTTP-CLI-9 -law status_empty: - for +m: Maybe<&2, U32> - {Http.status.empty(m) - == Bool.or(Http.status.interim(m), Http.status.bare(m)) : Bool} - -# LAW: a successful exchange's text is read by the request's method -# HTTP-CLI-9 -law heard_ok: - for +method: String - for +text: String - {Spec.wire.heard(method, "0\n" ++ text) == Http.parse.to(method, text) - : Http.Reply} - -# LAW: a failed exchange is torn with the reason it is reported with -# HTTP-CLI-9 -law heard_fail: - for +method: String - for +raw: String - for e: {False{} == Spec.wire.ok(raw) : Bool} - {Spec.wire.heard(method, raw) - == Http.Torn{Spec.wire.why(Spec.wire.code(raw), Spec.wire.text(raw))} - : Http.Reply} - -# --- a response's reason phrase (SPEC.md HTTP-CLI-11) --- - -# LAW: a text's reason phrase is read head by head, at most as many heads as -# it has characters -# HTTP-CLI-11 -law phrase_to: - for +raw: String - {Http.phrase.to(raw) - == Http.phrase.to.go(String.length(raw), Http.divide(raw, "\r\n\r\n")) - : String} - -# LAW: a read with no heads left found no reason -# HTTP-CLI-11 -law phrase_fuel: - for c: Http.Cut - {Http.phrase.to.go(0n, c) == "" : String} - -# LAW (RFC 9112 §2.1): a head that never ends has no reason -# HTTP-CLI-11 -law phrase_open: - for f: Nat - {Http.phrase.to.go(1n+f, Http.NoCut{}) == "" : String} - -# LAW (RFC 9110 §15.2): an interim head with text after it is skipped by the -# same rule HTTP-CLI-9 skips it by, and the reason read from the text after it -# HTTP-CLI-11 -law phrase_interim: - for f: Nat - for +before: String - for +after: String - for e: {True{} == Http.interim.skip(Http.status.of(String.lines(before)), - after) : Bool} - {Http.phrase.to.go(1n+f, Http.Cut{before, after}) - == Http.phrase.to.go(f, Http.divide(after, "\r\n\r\n")) : String} - -# LAW: the head HTTP-CLI-9 stops at gives the reason on its status line -# HTTP-CLI-11 -law phrase_final: - for f: Nat - for +before: String - for +after: String - for e: {False{} == Http.interim.skip(Http.status.of(String.lines(before)), - after) : Bool} - {Http.phrase.to.go(1n+f, Http.Cut{before, after}) - == Http.phrase.line(Http.first(String.lines(before))) : String} - -# LAW (RFC 9112 §4): a status line ` SP SP ` whose version and code -# hold no SP and whose reason holds no CR has reason exactly r -# HTTP-CLI-11 -law phrase_line: - for +v: String - for +c: String - for +r: String - for e1: {True{} == avoids(v, ' ') : Bool} - for e2: {True{} == avoids(c, ' ') : Bool} - for e3: {True{} == avoids(r, '\r') : Bool} - {Http.phrase.line(v ++ SCon{' ', c ++ SCon{' ', r}}) == r : String} - -# LAW (RFC 9112 §4): the CR that ends a status line's CRLF is not part of -# its reason -# HTTP-CLI-11 -law phrase_line_cr: - for +v: String - for +c: String - for +r: String - for e1: {True{} == avoids(v, ' ') : Bool} - for e2: {True{} == avoids(c, ' ') : Bool} - for e3: {True{} == avoids(r, '\r') : Bool} - {Http.phrase.line(v ++ SCon{' ', c ++ SCon{' ', r ++ "\r"}}) == r : String} - -# LAW: a status line with one SP has an empty reason -# HTTP-CLI-11 -law phrase_line_one: - for +v: String - for +c: String - for e1: {True{} == avoids(v, ' ') : Bool} - for e2: {True{} == avoids(c, ' ') : Bool} - {Http.phrase.line(v ++ SCon{' ', c}) == "" : String} - -# LAW: a status line with no SP has an empty reason -# HTTP-CLI-11 -law phrase_line_none: - for +v: String - for e: {True{} == avoids(v, ' ') : Bool} - {Http.phrase.line(v) == "" : String} - -# LAW: the client reads the reason from the same text it reads the response -# from -# HTTP-CLI-11 -law phrase_heard: - for +text: String - {Spec.wire.phrase("0\n" ++ text) == Http.phrase.to(text) : String} - -# --- reading until the response is whole (SPEC.md HTTP-CLI-6) --- - -# LAW: a read of no octets finds the connection closed, and the answer is -# everything received -# HTTP-CLI-6 -law read_closed: - for h: Http.Hear - {Spec.wire.read.count(h, 0n) == Some{Spec.Heard{}} : Maybe<&2, Spec.Read>} - -# LAW: a read whose count alone advances the state is judged by its count -# exactly as by its text -# HTTP-CLI-6 -law read_count: - for +h: Http.Hear - for +b: String - for e1: {False{} == String.is_empty(b) : Bool} - for e2: {True{} == Http.hear.counts(h, String.length(b)) : Bool} - {Spec.wire.read.count(h, String.length(b)) - == Some{Spec.wire.read.next(h, b)} : Maybe<&2, Spec.Read>} - -# LAW: a state the count alone advances is advanced by a text's count -# exactly as by the text -# HTTP-CLI-6 -law hear_count: - for +h: Http.Hear - for +b: String - for e: {True{} == Http.hear.counts(h, String.length(b)) : Bool} - {Http.hear.more.n(h, String.length(b)) == Http.hear.more(h, b) : Http.Hear} - -# LAW: walking n octets off a text of at most n octets leaves n less its -# length owed -# HTTP-CLI-6 -law skip_short: - for +b: String - for +n: Nat - for e: {False{} == Nat.is_lt(n, String.length(b)) : Bool} - {Http.octs.skip(b, n) == Http.Short{Nat.sub(n, String.length(b))} - : Http.Skip} - -# LAW: a read whose text completes the response is heard -# HTTP-CLI-6 -law read_whole: - for +method: String - for +a: String - for +text: String - for e1: {False{} == String.is_empty(text) : Bool} - for e2: {True{} == Http.whole(method, a ++ text) : Bool} - {Spec.wire.read.next(Http.hear.of(method, a), text) == Spec.Heard{} - : Spec.Read} - -# LAW: a read whose text does not complete the response reads again, in the -# state for what has been received with it -# HTTP-CLI-6 -law read_more: - for +method: String - for +a: String - for +text: String - for e1: {False{} == String.is_empty(text) : Bool} - for e2: {False{} == Http.whole(method, a ++ text) : Bool} - {Spec.wire.read.next(Http.hear.of(method, a), text) - == Spec.Again{Http.hear.of(method, a ++ text)} : Spec.Read} - -# LAW: the pieces put together in one pass are the text they stand for (the -# server puts a request's pieces together with it) -law join_joined: - for +ps: List<&2, String> - {Spec.wire.join(ps) == Spec.wire.joined(ps) : String} - -# --- judging a response read by read (SPEC.md HTTP-CLI-12) --- - -# LAW: the state for a text says the response is complete exactly when the -# whole-text check (HTTP-CLI-5) says that text is -# HTTP-CLI-12 -law hear_done: - for +method: String - for +raw: String - {Http.hear.done(Http.hear.of(method, raw)) == Http.whole(method, raw) - : Bool} - -# LAW: the state for a text, advanced by more text, is the state for the two -# together -# HTTP-CLI-12 -law hear_more: - for +method: String - for +a: String - for +b: String - {Http.hear.more(Http.hear.of(method, a), b) == Http.hear.of(method, a ++ b) - : Http.Hear} - -# LAW (RFC 9112 §7.1): a chunked body's walk from a chunk header has ended -# exactly when chunks.ended says, with the same fuel -# HTTP-CLI-12 -law walk_done: - for f: Nat - for +rest: String - {Http.walk.done(Http.walk(f, rest)) - == Http.chunks.ended(f, Http.divide(rest, "\r\n")) : Bool} - -# LAW: the walk over a text and more text, with fuel for both, is the walk -# over the text advanced by the more -# HTTP-CLI-12 -law walk_more: - for f: Nat - for +rest: String - for +b: String - {Http.walk(Nat.add(f, String.length(b)), rest ++ b) - == Http.walk.more(Http.walk(f, rest), String.length(b), b) : Http.Walk} - -# --- a response planned from its head (SPEC.md HTTP-CLI-15) --- - -# LAW (RFC 9112 §4): a final head with no status code is torn -# HTTP-CLI-15 -law plan_none: - for ls: List<&2, String> - {Http.head.plan(None{}, ls) - == Http.Unplanned{"the response has no status line"} : Http.Plan} - -# LAW (RFC 9112 §6.3 item 5): Content-Length values that are not all the -# same digits are torn -# HTTP-CLI-15 -law plan_disagree: - for n: U32 - for +ls: List<&2, String> - for e: {False{} == Http.clen.agree(Http.head.values(ls, "content-length")) - : Bool} - {Http.head.plan(Some{n}, ls) - == Http.Unplanned{"Content-Length values disagree"} : Http.Plan} - -# LAW (RFC 9112 §6.3 item 3): chunked Transfer-Encoding with a -# Content-Length is torn -# HTTP-CLI-15 -law plan_conflict: - for n: U32 - for +ls: List<&2, String> - for e1: {True{} == Http.clen.agree(Http.head.values(ls, "content-length")) - : Bool} - for e2: {True{} == String.contains(String.to_lower(Http.head.go(ls, - "transfer-encoding")), "chunked") : Bool} - for e3: {True{} == Http.clen.any(Http.head.values(ls, "content-length")) - : Bool} - {Http.head.plan(Some{n}, ls) - == Http.Unplanned{"Content-Length and Transfer-Encoding conflict"} - : Http.Plan} - -# LAW (RFC 9112 §6.3 item 4): chunked Transfer-Encoding alone frames the -# body by its chunks -# HTTP-CLI-15 -law plan_chunked: - for n: U32 - for +ls: List<&2, String> - for e1: {True{} == Http.clen.agree(Http.head.values(ls, "content-length")) - : Bool} - for e2: {True{} == String.contains(String.to_lower(Http.head.go(ls, - "transfer-encoding")), "chunked") : Bool} - for e3: {False{} == Http.clen.any(Http.head.values(ls, "content-length")) - : Bool} - {Http.head.plan(Some{n}, ls) - == Http.Planned{n, Http.head.list(ls), Http.Chunked{}} : Http.Plan} - -# LAW (RFC 9112 §6.3 item 6): otherwise a Content-Length that reads as a -# number sizes the body -# HTTP-CLI-15 -law plan_sized: - for n: U32 - for +ls: List<&2, String> - for e1: {True{} == Http.clen.agree(Http.head.values(ls, "content-length")) - : Bool} - for e2: {False{} == String.contains(String.to_lower(Http.head.go(ls, - "transfer-encoding")), "chunked") : Bool} - for k: Nat - for e3: {Some{k} == Http.digits.read(Http.first(Http.head.values(ls, - "content-length"))) : Maybe<&2, Nat>} - {Http.head.plan(Some{n}, ls) - == Http.Planned{n, Http.head.list(ls), Http.Sized{k}} : Http.Plan} - -# LAW (RFC 9112 §6.3 item 8): otherwise the body runs to the close -# HTTP-CLI-15 -law plan_closed: - for n: U32 - for +ls: List<&2, String> - for e1: {True{} == Http.clen.agree(Http.head.values(ls, "content-length")) - : Bool} - for e2: {False{} == String.contains(String.to_lower(Http.head.go(ls, - "transfer-encoding")), "chunked") : Bool} - for e3: {None{} == Http.digits.read(Http.first(Http.head.values(ls, - "content-length"))) : Maybe<&2, Nat>} - {Http.head.plan(Some{n}, ls) - == Http.Planned{n, Http.head.list(ls), Http.Closed{}} : Http.Plan} - -# LAW: parse_response reads a head and its body as the head's plan says -# HTTP-CLI-15 -law plan_status: - for m: Maybe<&2, U32> - for +ls: List<&2, String> - for +body: String - {Http.parse.status(m, ls, body) == Http.plan.text(Http.head.plan(m, ls), body) - : Http.Reply} - -# LAW (RFC 9110 §6.4.1, §9.3.2): a final head is read as its plan says, with -# no body for HEAD or a 1xx, 204 or 304 status -# HTTP-CLI-15 -law plan_final: - for f: Nat - for +head: Bool - for +before: String - for +after: String - for e: {False{} == Http.interim.skip(Http.status.of(String.lines(before)), - after) : Bool} - {Http.parse.to.go(1n+f, head, Http.Cut{before, after}) - == Http.plan.text(Http.head.final(head, String.lines(before)), after) - : Http.Reply} - -# LAW (RFC 9112 §7.1): the chunked decoder's step is decided by the chunk-size -# line alone, then by the octets after it -# HTTP-CLI-15 -law size_next: - for +before: String - for +after: String - {Http.chunks.next(before, after) - == Http.chunk.next(Http.chunk.size(before), after) : Http.Next} - -# --- redirects (SPEC.md HTTP-REDIR) --- - -# LAW: a redirect status, with hops left, whose Location has a target goes to -# exactly that target -# HTTP-REDIR-1 -law redir_go: - for +status: U32 - for e: {True{} == Bool.or(U32.is_eq(status, 301), Bool.or(U32.is_eq(status, - 302), Bool.or(U32.is_eq(status, 303), Bool.or(U32.is_eq(status, 307), - U32.is_eq(status, 308))))) : Bool} - for n: Nat - for url: String - for +loc: String - for next: String - for et: {Some{next} == Redirect.redirect.target(url, loc) - : Maybe<&2, String>} - {Redirect.redirect.next(1n+n, url, status, loc) == Redirect.Go{next} - : Redirect.Hop} - -# LAW: a redirect whose Location has no target stops the chain -# HTTP-REDIR-1 -law redir_no_target: - for +status: U32 - for left: Nat - for url: String - for +loc: String - for et: {None{} == Redirect.redirect.target(url, loc) : Maybe<&2, String>} - {Redirect.redirect.next(left, url, status, loc) == Redirect.Stop{} - : Redirect.Hop} - -# LAW: the Location the planner reads is the response's Location header -# (case-insensitive, "" when absent) -# HTTP-REDIR-1 -law redir_plan_location: - for left: Nat - for url: String - for +status: U32 - for hs: List<&2, Http.Header> - {Redirect.redirect.plan(left, url, status, hs) - == Redirect.redirect.next(left, url, status, - Http.headers.find(hs, "location")) : Redirect.Hop} - -# LAW: every status other than 301, 302, 303, 307 and 308 stops the chain -# HTTP-REDIR-2 -law redir_other: - for +status: U32 - for e: {False{} == Bool.or(U32.is_eq(status, 301), Bool.or(U32.is_eq(status, - 302), Bool.or(U32.is_eq(status, 303), Bool.or(U32.is_eq(status, 307), - U32.is_eq(status, 308))))) : Bool} - for left: Nat - for url: String - for +loc: String - {Redirect.redirect.next(left, url, status, loc) == Redirect.Stop{} - : Redirect.Hop} - -# LAW: with no hops left the chain stops, whatever the response -# HTTP-REDIR-2 -law redir_zero: - for url: String - for +status: U32 - for +loc: String - {Redirect.redirect.next(0n, url, status, loc) == Redirect.Stop{} - : Redirect.Hop} - -# LAW: an absolute http or https Location is its own target -# HTTP-REDIR-3 -law target_absolute: - for url: String - for +loc: String - for e: {True{} == Bool.or(String.starts_with(loc, "http://"), - String.starts_with(loc, "https://")) : Bool} - {Redirect.redirect.target(url, loc) == Some{loc} : Maybe<&2, String>} - -# LAW: against a URL that does not parse, a Location that is not an absolute -# URL has no target -# HTTP-REDIR-3 -law target_bad: - for url: String - for +loc: String - for e1: {False{} == Bool.or(String.starts_with(loc, "http://"), - String.starts_with(loc, "https://")) : Bool} - for why: String - for e2: {Url.Bad{why} == Url.parse(url) : Url.Loc} - {Redirect.redirect.target(url, loc) == None{} : Maybe<&2, String>} - -# LAW (RFC 3986 §5.2.2): against a URL that parses, a Location that is not an -# absolute URL is resolved by its sort against the URL's parts -# HTTP-REDIR-3 -# HTTP-REDIR-4 -law target_against: - for url: String - for +loc: String - for e1: {False{} == Bool.or(String.starts_with(loc, "http://"), - String.starts_with(loc, "https://")) : Bool} - for scheme: String - for host: String - for port: U32 - for path: String - for e2: {Url.Loc{scheme, host, port, path} == Url.parse(url) : Url.Loc} - {Redirect.redirect.target(url, loc) - == Redirect.redirect.against(scheme, host, port, path, loc, - Redirect.redirect.sort(loc)) : Maybe<&2, String>} - -# LAW (RFC 3986 §5.2.2): a network-path reference keeps only the scheme -# HTTP-REDIR-3 -law against_net: - for scheme: String - for host: String - for port: U32 - for path: String - for +loc: String - {Redirect.redirect.against(scheme, host, port, path, loc, Redirect.Net{}) - == Some{scheme ++ ":" ++ loc} : Maybe<&2, String>} - -# LAW (RFC 3986 §5.2.2): an absolute path keeps the origin, its own path with -# dot segments removed, then its query -# HTTP-REDIR-3 -law against_abs: - for scheme: String - for host: String - for port: U32 - for path: String - for +loc: String - {Redirect.redirect.against(scheme, host, port, path, loc, Redirect.Abs{}) - == Some{scheme ++ "://" ++ host ++ ":" ++ U32.show(port) - ++ Redirect.redirect.abs.of(Redirect.redirect.split(loc))} - : Maybe<&2, String>} - -# LAW: a Location the client stays on has no target -# HTTP-REDIR-3 -law against_stay: - for scheme: String - for host: String - for port: U32 - for path: String - for +loc: String - {Redirect.redirect.against(scheme, host, port, path, loc, Redirect.Stay{}) - == None{} : Maybe<&2, String>} - -# LAW (RFC 3986 §5.2.2): a query keeps the origin and the current path -# HTTP-REDIR-4 -law against_query: - for scheme: String - for host: String - for port: U32 - for path: String - for +loc: String - {Redirect.redirect.against(scheme, host, port, path, loc, Redirect.Query{}) - == Some{scheme ++ "://" ++ host ++ ":" ++ U32.show(port) - ++ Redirect.redirect.base(path) ++ loc} : Maybe<&2, String>} - -# LAW (RFC 3986 §5.2.2): a relative path keeps the origin and is merged with -# the current path -# HTTP-REDIR-4 -law against_rel: - for scheme: String - for host: String - for port: U32 - for path: String - for +loc: String - {Redirect.redirect.against(scheme, host, port, path, loc, Redirect.Rel{}) - == Some{scheme ++ "://" ++ host ++ ":" ++ U32.show(port) - ++ Redirect.redirect.rel.of(Redirect.redirect.base(path), - Redirect.redirect.split(loc))} : Maybe<&2, String>} - -# LAW (RFC 3986 §4.2): a Location that starts `//` is a network path -# HTTP-REDIR-3 -law sort_net: - for +loc: String - for e: {True{} == String.starts_with(loc, "//") : Bool} - {Redirect.redirect.sort(loc) == Redirect.Net{} : Redirect.Sort} - -# LAW (RFC 3986 §4.2): one that starts with a single `/` is an absolute path -# HTTP-REDIR-3 -law sort_abs: - for +loc: String - for e1: {False{} == String.starts_with(loc, "//") : Bool} - for e2: {True{} == String.starts_with(loc, "/") : Bool} - {Redirect.redirect.sort(loc) == Redirect.Abs{} : Redirect.Sort} - -# LAW: any other Location the client does not follow stays -# HTTP-REDIR-3 -law sort_stay: - for +loc: String - for e1: {False{} == String.starts_with(loc, "//") : Bool} - for e2: {False{} == String.starts_with(loc, "/") : Bool} - for e3: {True{} == Redirect.redirect.stays(loc) : Bool} - {Redirect.redirect.sort(loc) == Redirect.Stay{} : Redirect.Sort} - -# LAW (RFC 3986 §4.2): any other Location that starts `?` is a query -# HTTP-REDIR-4 -law sort_query: - for +loc: String - for e1: {False{} == String.starts_with(loc, "//") : Bool} - for e2: {False{} == String.starts_with(loc, "/") : Bool} - for e3: {False{} == Redirect.redirect.stays(loc) : Bool} - for e4: {True{} == String.starts_with(loc, "?") : Bool} - {Redirect.redirect.sort(loc) == Redirect.Query{} : Redirect.Sort} - -# LAW (RFC 3986 §4.2): and every other one is a relative path -# HTTP-REDIR-4 -law sort_rel: - for +loc: String - for e1: {False{} == String.starts_with(loc, "//") : Bool} - for e2: {False{} == String.starts_with(loc, "/") : Bool} - for e3: {False{} == Redirect.redirect.stays(loc) : Bool} - for e4: {False{} == String.starts_with(loc, "?") : Bool} - {Redirect.redirect.sort(loc) == Redirect.Rel{} : Redirect.Sort} - -# LAW: the client stays on an empty Location, a fragment, and one that names -# another scheme -# HTTP-REDIR-3 -law stays_on: - for +loc: String - {Redirect.redirect.stays(loc) == Bool.or(String.is_empty(loc), - Bool.or(String.starts_with(loc, "#"), Redirect.redirect.schemed(loc))) - : Bool} - -# LAW (RFC 3986 §3.1): an empty reference names no scheme -# HTTP-REDIR-3 -law schemed_nil: - for s: String - for e: {True{} == String.is_empty(s) : Bool} - {Redirect.redirect.schemed(s) == False{} : Bool} - -# LAW (RFC 3986 §3.1): a `:` before the first `/`, `?` or `#` names a scheme -# HTTP-REDIR-3 -law schemed_colon: - for t: String - {Redirect.redirect.schemed(SCon{':', t}) == True{} : Bool} - -# LAW (RFC 3986 §4.2): a `/`, `?` or `#` first means no scheme -# HTTP-REDIR-3 -law schemed_stop: - for +c: Char - for t: String - for e: {True{} == Redirect.redirect.schemed.end(c) : Bool} - {Redirect.redirect.schemed(SCon{c, t}) == False{} : Bool} - -# LAW: any other first character leaves the question to the rest -# HTTP-REDIR-3 -law schemed_other: - for +c: Char - for t: String - for e1: {False{} == Char.is_eq(c, ':') : Bool} - for e2: {False{} == Redirect.redirect.schemed.end(c) : Bool} - {Redirect.redirect.schemed(SCon{c, t}) == Redirect.redirect.schemed(t) - : Bool} - -# LAW: the characters that end a reference's first segment are `/`, `?`, `#` -# HTTP-REDIR-3 -law schemed_end: - for +c: Char - {Redirect.redirect.schemed.end(c) == Bool.or(Char.is_eq(c, '/'), - Bool.or(Char.is_eq(c, '?'), Char.is_eq(c, '#'))) : Bool} - -# LAW (RFC 3986 §3.4, §3.5): a Location with a `?` is its path before it and -# its query from it, its fragment dropped -# HTTP-REDIR-3 -# HTTP-REDIR-4 -law split_query: - for +loc: String - for p: String - for q: String - for e: {Http.Cut{p, q} == Http.divide(Url.frag.drop(loc), "?") : Http.Cut} - {Redirect.redirect.split(loc) == Redirect.Ref{p, "?" ++ q} : Redirect.Ref} - -# LAW: a Location with no `?` is all path, its fragment dropped -# HTTP-REDIR-3 -# HTTP-REDIR-4 -law split_plain: - for +loc: String - for e: {Http.NoCut{} == Http.divide(Url.frag.drop(loc), "?") : Http.Cut} - {Redirect.redirect.split(loc) == Redirect.Ref{Url.frag.drop(loc), ""} - : Redirect.Ref} - -# LAW (RFC 3986 §5.2.2): an absolute path is its segments after the leading -# `/`, dot segments removed, then its query -# HTTP-REDIR-3 -law abs_of: - for path: String - for query: String - {Redirect.redirect.abs.of(Redirect.Ref{path, query}) - == Redirect.redirect.clean(List.tail(&2, String, String.split(path, '/'))) - ++ query : String} - -# LAW (RFC 3986 §5.2.3): a relative path follows every segment of the current -# path but its last, then dot segments are removed, then its query -# HTTP-REDIR-4 -law rel_of: - for base: String - for path: String - for query: String - {Redirect.redirect.rel.of(base, Redirect.Ref{path, query}) - == Redirect.redirect.clean(List.tail(&2, String, - List.append(&2, String, Redirect.redirect.init(String.split(base, '/')), - String.split(path, '/')))) ++ query : String} - -# LAW: the current path, for merging, is the part before its `?` -# HTTP-REDIR-4 -law base_path: - for path: String - {Redirect.redirect.base(path) == Url.query.hier(String.split(path, '?')) - : String} - -# LAW: the first piece of a split, empty when there is none -# HTTP-REDIR-4 -law hier_first: - for h: String - for t: List<&2, String> - {Url.query.hier(h <> t) == h : String} - -# LAW: no segments have no segments but the last -# HTTP-REDIR-4 -law init_nil: - for ss: List<&2, String> - for e: {True{} == List.is_empty(&2, String, ss) : Bool} - {Redirect.redirect.init(ss) == [] : List<&2, String>} - -# LAW: segments but the last start from the first -# HTTP-REDIR-4 -law init_cons: - for s: String - for t: List<&2, String> - {Redirect.redirect.init(s <> t) == Redirect.redirect.init.of(t, s) - : List<&2, String>} - -# LAW: the last segment is dropped -# HTTP-REDIR-4 -law init_last: - for s: String - {Redirect.redirect.init.of([], s) == [] : List<&2, String>} - -# LAW: every segment before the last is kept, in order -# HTTP-REDIR-4 -law init_keep: - for s: String - for u: String - for r: List<&2, String> - {Redirect.redirect.init.of(u <> r, s) == s <> Redirect.redirect.init.of(r, u) - : List<&2, String>} - -# LAW (RFC 3986 §5.2.4): a path is `/`, then its segments with dot segments -# removed, joined by `/` -# HTTP-REDIR-5 -law clean_path: - for ss: List<&2, String> - {Redirect.redirect.clean(ss) - == "/" ++ String.join(Redirect.redirect.dots(ss, []), "/") : String} - -# LAW: once every segment is read, the kept ones are the path, in order -# HTTP-REDIR-5 -law dots_nil: - for out: List<&2, String> - {Redirect.redirect.dots([], out) == List.reverse(&2, String, out) - : List<&2, String>} - -# LAW (RFC 3986 §5.2.4 B, D): a `.` segment is dropped -# HTTP-REDIR-5 -law dots_dot: - for +s: String - for +t: List<&2, String> - for out: List<&2, String> - for e: {True{} == String.eq(s, ".") : Bool} - {Redirect.redirect.dots(s <> t, out) - == Redirect.redirect.dots(t, - Redirect.redirect.dots.end(List.is_empty(&2, String, t), out)) - : List<&2, String>} - -# LAW (RFC 3986 §5.2.4 C, D): a `..` segment drops the last segment kept -# HTTP-REDIR-5 -law dots_up: - for +s: String - for +t: List<&2, String> - for out: List<&2, String> - for e1: {False{} == String.eq(s, ".") : Bool} - for e2: {True{} == String.eq(s, "..") : Bool} - {Redirect.redirect.dots(s <> t, out) - == Redirect.redirect.dots(t, - Redirect.redirect.dots.end(List.is_empty(&2, String, t), - Redirect.redirect.pop(out))) : List<&2, String>} - -# LAW (RFC 3986 §5.2.4 E): every other segment is kept -# HTTP-REDIR-5 -law dots_seg: - for +s: String - for +t: List<&2, String> - for out: List<&2, String> - for e1: {False{} == String.eq(s, ".") : Bool} - for e2: {False{} == String.eq(s, "..") : Bool} - {Redirect.redirect.dots(s <> t, out) == Redirect.redirect.dots(t, s <> out) - : List<&2, String>} - -# LAW (RFC 3986 §5.2.4 B, C): a path that ends in `.` or `..` keeps its -# trailing `/` -# HTTP-REDIR-5 -law dots_last: - for out: List<&2, String> - {Redirect.redirect.dots.end(True{}, out) == "" <> out : List<&2, String>} - -# LAW: a `.` or `..` before other segments adds nothing -# HTTP-REDIR-5 -law dots_inner: - for out: List<&2, String> - {Redirect.redirect.dots.end(False{}, out) == out : List<&2, String>} - -# LAW (RFC 3986 §5.2.4 C): `..` with nothing kept drops nothing -# HTTP-REDIR-5 -law pop_nil: - for out: List<&2, String> - for e: {True{} == List.is_empty(&2, String, out) : Bool} - {Redirect.redirect.pop(out) == [] : List<&2, String>} - -# LAW (RFC 3986 §5.2.4 C): `..` drops the last segment kept -# HTTP-REDIR-5 -law pop_cons: - for s: String - for t: List<&2, String> - {Redirect.redirect.pop(s <> t) == t : List<&2, String>} - -# --- HTML escaping (SPEC.md HTTP-HTML) --- - -# LAW: escaping a string is escaping it in front of nothing -# HTTP-HTML-1 -law html_escape_onto: - for s: String - {Html.escape(s) == Html.escape.onto(s, "") : String} - -# LAW: nothing is written for the empty string -# HTTP-HTML-1 -law html_nil: - for rest: String - {Html.escape.onto("", rest) == rest : String} - -# LAW: a string is written one character at a time, the first character's -# writing followed by the rest's -# HTTP-HTML-1 -law html_cons: - for +c: Char - for t: String - for rest: String - {Html.escape.onto(SCon{c, t}, rest) == Html.one(c, Html.escape.onto(t, rest)) - : String} - -# LAW: `&` is written as `&` -# HTTP-HTML-2 -law html_amp: - for rest: String - {Html.one('&', rest) == "&" ++ rest : String} - -# LAW: `<` is written as `<` -# HTTP-HTML-2 -law html_lt: - for rest: String - {Html.one('<', rest) == "<" ++ rest : String} - -# LAW: `>` is written as `>` -# HTTP-HTML-2 -law html_gt: - for rest: String - {Html.one('>', rest) == ">" ++ rest : String} - -# LAW: `"` is written as `"` -# HTTP-HTML-2 -law html_dq: - for rest: String - {Html.one('"', rest) == """ ++ rest : String} - -# LAW: `'` is written as `'` -# HTTP-HTML-2 -law html_sq: - for rest: String - {Html.one('\'', rest) == "'" ++ rest : String} - -# LAW: every other character is written as itself -# HTTP-HTML-3 -law html_other: - for +c: Char - for e1: {False{} == Char.is_eq(c, '&') : Bool} - for e2: {False{} == Char.is_eq(c, '<') : Bool} - for e3: {False{} == Char.is_eq(c, '>') : Bool} - for e4: {False{} == Char.is_eq(c, '"') : Bool} - for e5: {False{} == Char.is_eq(c, '\'') : Bool} - for rest: String - {Html.one(c, rest) == SCon{c, rest} : String} - -# where the HTTP-HTML-4 frame is, reading left to right: between entities, -# after `&`, after `&#`, after ``, or owing exactly the rest of an entity -type Ent is Data: - Out{} - Amp{} - Hash{} - Three{} - Want{c: Char, rest: String} - -# between entities: anything but `<`, `>`, `"` and `'` -def ent.out.ok(lt: Bool, gt: Bool, dq: Bool, sq: Bool) -> Bool: - Bool.not(Bool.or(lt, Bool.or(gt, Bool.or(dq, sq)))) - -# between entities, an `&` opens one -def ent.out.next(amp: Bool) -> Ent: - Bool.pick(Ent, amp, Amp{}, Out{}) - -# what is owed after the rest of an entity's next character -def ent.want(rest: String) -> Ent: - match rest: - case SNil{}: - Out{} - case SCon{c, t}: - Want{c, t} - -# whether character c may come next -def ent.ok(+c: Char, st: Ent) -> Bool: - match st: - case Out{}: - ent.out.ok(Char.is_eq(c, '<'), Char.is_eq(c, '>'), Char.is_eq(c, '"'), - Char.is_eq(c, '\'')) - case Amp{}: - Bool.or(Char.is_eq(c, 'a'), Bool.or(Char.is_eq(c, 'l'), - Bool.or(Char.is_eq(c, 'g'), Char.is_eq(c, '#')))) - case Hash{}: - Char.is_eq(c, '3') - case Three{}: - Bool.or(Char.is_eq(c, '4'), Char.is_eq(c, '9')) - case Want{w, t}: - Char.is_eq(c, w) - -# where the frame is after character c -def ent.next(+c: Char, st: Ent) -> Ent: - match st: - case Out{}: - ent.out.next(Char.is_eq(c, '&')) - case Amp{}: - Bool.pick(Ent, Char.is_eq(c, 'a'), Want{'m', "p;"}, - Bool.pick(Ent, Char.is_eq(c, '#'), Hash{}, Want{'t', ";"})) - case Hash{}: - Three{} - case Three{}: - Want{';', ""} - case Want{w, t}: - ent.want(t) - -# nothing is owed at the end -def ent.end(st: Ent) -> Bool: - match st: - case Out{}: - True{} - case Amp{}: - False{} - case Hash{}: - False{} - case Three{}: - False{} - case Want{w, t}: - False{} - -# the HTTP-HTML-4 frame: no `<`, `>`, `"` or `'`, and every `&` opens one of -# `&`, `<`, `>`, `"`, `'` -def html.framed(str: String, +st: Ent) -> Bool: - match str: - case SNil{}: - ent.end(st) - case SCon{+c, tail}: - Bool.and(ent.ok(c, st), html.framed(tail, ent.next(c, st))) - -# LAW (frame): every escaped string holds no `<`, `>`, `"` or `'`, and each -# `&` in it opens one of the five entities -# HTTP-HTML-4 -law html_frame: - for s: String - {html.framed(Html.escape(s), Out{}) == True{} : Bool} - -# --- which requests follow redirects (SPEC.md HTTP-REDIR-6) --- - -# LAW: a GET follows redirects exactly when it is asked to -# HTTP-REDIR-6 -law follows_get: - for follow: Bool - {Redirect.redirect.follows("GET", follow) == follow : Bool} - -# LAW (RFC 9110 §9.1): every other method never follows redirects -# HTTP-REDIR-6 -law follows_other: - for +method: String - for e: {False{} == String.eq(method, "GET") : Bool} - for follow: Bool - {Redirect.redirect.follows(method, follow) == False{} : Bool} - -# --- multipart/form-data bodies (SPEC.md HTTP-FORM) --- - -# LAW: a boundary that is not valid is refused -# HTTP-FORM-1 -law form_bad_boundary: - for +b: String - for ps: List<&2, Form.Part> - for e: {False{} == Form.form.boundary.ok(b) : Bool} - {Form.form.plan(b, ps) - == Form.NoPlan{"the boundary must be 1 to 70 of A-Z a-z 0-9 - _ ."} - : Form.Plan} - -# LAW: a valid boundary whose delimiter occurs in a part's content is refused -# HTTP-FORM-1 -law form_clash: - for +b: String - for +ps: List<&2, Form.Part> - for e1: {True{} == Form.form.boundary.ok(b) : Bool} - for e2: {True{} == Form.form.clash(b, ps) : Bool} - {Form.form.plan(b, ps) == Form.NoPlan{"the boundary occurs in a part's content"} - : Form.Plan} - -# LAW: a file part whose content type holds a CR or LF is refused -# HTTP-FORM-1 -law form_bad_type: - for +b: String - for +ps: List<&2, Form.Part> - for e1: {True{} == Form.form.boundary.ok(b) : Bool} - for e2: {False{} == Form.form.clash(b, ps) : Bool} - for e3: {True{} == Form.form.badtype(ps) : Bool} - {Form.form.plan(b, ps) - == Form.NoPlan{"a file part's content type holds a CR or LF"} : Form.Plan} - -# LAW (RFC 7578 §4.1): every other form is planned: its Content-Type value -# and its parts' runs -# HTTP-FORM-1 -law form_ok: - for +b: String - for +ps: List<&2, Form.Part> - for e1: {True{} == Form.form.boundary.ok(b) : Bool} - for e2: {False{} == Form.form.clash(b, ps) : Bool} - for e3: {False{} == Form.form.badtype(ps) : Bool} - {Form.form.plan(b, ps) == Form.Plan{Form.form.type(b), Form.form.parts(b, ps)} - : Form.Plan} - -# LAW (RFC 7578 §4.1): the Content-Type field value names the boundary -# HTTP-FORM-1 -law form_type: - for b: String - {Form.form.type(b) == "multipart/form-data; boundary=" ++ b : String} - -# LAW (RFC 2046 §5.1.1): a boundary is 1 to 70 boundary characters -# HTTP-FORM-2 -law boundary_ok: - for +b: String - {Form.form.boundary.ok(b) - == Bool.and(Bool.and(Bool.not(String.is_empty(b)), - Nat.is_le(String.length(b), 70n)), Form.form.bchars(b)) : Bool} - -# LAW: the boundary characters are A-Z, a-z, 0-9, `-`, `_` and `.` -# HTTP-FORM-2 -law bchar: - for +cp: U32 - {Form.form.bchar(cp) - == Bool.or(Bool.and(U32.is_le(65, cp), U32.is_le(cp, 90)), - Bool.or(Bool.and(U32.is_le(97, cp), U32.is_le(cp, 122)), - Bool.or(Bool.and(U32.is_le(48, cp), U32.is_le(cp, 57)), - Bool.or(U32.is_eq(cp, 45), - Bool.or(U32.is_eq(cp, 95), U32.is_eq(cp, 46)))))) : Bool} - -# LAW: the empty string has no character that is not a boundary character -# HTTP-FORM-2 -law bchars_nil: - for s: String - for e: {SNil{} == s : String} - {Form.form.bchars(s) == True{} : Bool} - -# LAW: a string's characters are boundary characters exactly when its first -# is and the rest's are -# HTTP-FORM-2 -law bchars_cons: - for +c: U32 - for t: String - {Form.form.bchars(SCon{Chr{c}, t}) - == Bool.and(Form.form.bchar(c), Form.form.bchars(t)) : Bool} - -# LAW (RFC 2046 §5.1.1): the delimiter is `--` and the boundary, as octets -# HTTP-FORM-2 -law delim: - for b: String - {Form.form.delim(b) == Form.octets.onto("--" ++ b, []) : List<&2, U32>} - -# LAW: no parts, no clash -# HTTP-FORM-2 -law clash_nil: - for b: String - {Form.form.clash(b, []) == False{} : Bool} - -# LAW: a list of parts clashes exactly when its first part's content holds -# the delimiter or the rest clashes -# HTTP-FORM-2 -law clash_cons: - for +b: String - for p: Form.Part - for t: List<&2, Form.Part> - {Form.form.clash(b, p <> t) - == Bool.or(Form.form.hit(b, p), Form.form.clash(b, t)) : Bool} - -# LAW: a field's content holds the delimiter exactly when its value's UTF-8 -# octets hold the delimiter's octets as a contiguous run -# HTTP-FORM-2 -law hit_field: - for +b: String - for name: String - for value: String - {Form.form.hit(b, Form.Field{name, value}) - == Form.octets.has(Form.octets.onto(value, []), Form.form.delim(b)) : Bool} - -# LAW: a file's content holds the delimiter exactly when the search of its -# bytes found it -# HTTP-FORM-2 -law hit_file: - for +b: String - for name: String - for filename: String - for ctype: String - for clash: Bool - {Form.form.hit(b, Form.File{name, filename, ctype, clash}) == clash : Bool} - -# LAW: a check of this and the rest holds exactly when both do -# HTTP-FORM-2 -law all_step: - for ok: Bool - for rest: Unit -> Bool - {Form.all.step(ok, rest) == Bool.and(ok, rest(Unit{})) : Bool} - -# LAW: a search of this and the rest hits exactly when either does -# HTTP-FORM-2 -law any_step: - for hit: Bool - for rest: Unit -> Bool - {Form.any.step(hit, rest) == Bool.or(hit, rest(Unit{})) : Bool} - -# LAW: no octets hold a pattern exactly when the pattern is empty -# HTTP-FORM-2 -law has_nil: - for +pat: List<&2, U32> - {Form.octets.has([], pat) == Form.octets.starts([], pat) : Bool} - -# LAW: octets hold a pattern exactly when they open with it or their rest -# holds it -# HTTP-FORM-2 -law has_cons: - for +x: U32 - for +t: List<&2, U32> - for +pat: List<&2, U32> - {Form.octets.has(x <> t, pat) - == Bool.or(Form.octets.starts(x <> t, pat), Form.octets.has(t, pat)) - : Bool} - -# LAW: every octets open with the empty pattern -# HTTP-FORM-2 -law starts_done: - for xs: List<&2, U32> - {Form.octets.starts(xs, []) == True{} : Bool} - -# LAW: no octets open with a pattern that is not empty -# HTTP-FORM-2 -law starts_short: - for p: U32 - for q: List<&2, U32> - {Form.octets.starts([], p <> q) == False{} : Bool} - -# LAW: octets open with a pattern exactly when their first octets are equal -# and the rest opens with the pattern's rest -# HTTP-FORM-2 -law starts_cons: - for x: U32 - for t: List<&2, U32> - for p: U32 - for q: List<&2, U32> - {Form.octets.starts(x <> t, p <> q) - == Bool.and(U32.is_eq(x, p), Form.octets.starts(t, q)) : Bool} - -# LAW: a field's content is a run of its value's UTF-8 octets -# HTTP-FORM-3 -law content_field: - for name: String - for value: String - for rest: List<&2, Form.Seg> - {Form.form.content(Form.Field{name, value}, rest) - == Form.Run{Form.octets.onto(value, [])} <> rest : List<&2, Form.Seg>} - -# LAW: a file's content is the next file's bytes, unchanged -# HTTP-FORM-3 -law content_file: - for name: String - for filename: String - for ctype: String - for clash: Bool - for rest: List<&2, Form.Seg> - {Form.form.content(Form.File{name, filename, ctype, clash}, rest) - == Form.Blob{} <> rest : List<&2, Form.Seg>} - -# LAW: no parts hold a bad content type -# HTTP-FORM-2 -law badtype_nil: - for ps: List<&2, Form.Part> - for e: {[] == ps : List<&2, Form.Part>} - {Form.form.badtype(ps) == False{} : Bool} - -# LAW: a list of parts holds a bad content type exactly when its first part -# or its rest does -# HTTP-FORM-2 -law badtype_cons: - for p: Form.Part - for t: List<&2, Form.Part> - {Form.form.badtype(p <> t) - == Bool.or(Form.form.badtype.one(p), Form.form.badtype(t)) : Bool} - -# LAW: a field part has no content type to be bad -# HTTP-FORM-2 -law badtype_field: - for name: String - for value: String - {Form.form.badtype.one(Form.Field{name, value}) == False{} : Bool} - -# LAW: a file part's content type is bad exactly when it holds a CR or an LF -# HTTP-FORM-2 -law badtype_file: - for name: String - for filename: String - for +ctype: String - for clash: Bool - {Form.form.badtype.one(Form.File{name, filename, ctype, clash}) - == Bool.or(String.contains(ctype, "\r"), String.contains(ctype, "\n")) - : Bool} - -# LAW (RFC 2046 §5.1.1): after the last part comes the close delimiter -# HTTP-FORM-3 -law parts_nil: - for +b: String - {Form.form.parts(b, []) == [Form.Run{Form.octets.onto("--" ++ b ++ "--\r\n", [])}] - : List<&2, Form.Seg>} - -# LAW: the parts are written in order, each in front of the rest -# HTTP-FORM-3 -law parts_cons: - for +b: String - for p: Form.Part - for t: List<&2, Form.Part> - {Form.form.parts(b, p <> t) == Form.form.part(b, p, Form.form.parts(b, t)) - : List<&2, Form.Seg>} - -# LAW (RFC 7578 §4.1): a part is the delimiter line, its header lines, a blank -# line, its content and a CRLF -# HTTP-FORM-3 -law part: - for +b: String - for +p: Form.Part - for rest: List<&2, Form.Seg> - {Form.form.part(b, p, rest) - == Form.Run{Form.octets.onto("--" ++ b ++ "\r\n" ++ Form.form.head(p) - ++ "\r\n", [])} <> Form.form.content(p, - Form.Run{Form.octets.onto("\r\n", [])} <> rest) : List<&2, Form.Seg>} - -# LAW: the empty string writes no octets -# HTTP-FORM-3 -law onto_nil: - for rest: List<&2, U32> - {Form.octets.onto("", rest) == rest : List<&2, U32>} - -# LAW (RFC 3629 §3): a string is written as its first character's UTF-8 -# octets, then the rest's -# HTTP-FORM-3 -law onto_cons: - for +c: U32 - for t: String - for rest: List<&2, U32> - {Form.octets.onto(SCon{Chr{c}, t}, rest) - == List.append(&2, U32, Pct.utf8(c), Form.octets.onto(t, rest)) - : List<&2, U32>} - -# LAW: form_field makes a field part -# HTTP-FORM-4 -law field_is: - for +d: String - for name: String - for value: String - {Pair.fst(List<&2, Form.Part>, List<&1, Bytes.Bytes>, - Form.form.split(d, [Form.form.field(name, value)])) - == [Form.Field{name, value}] : List<&2, Form.Part>} - -# LAW: a file part is planned with its names, its content type and whether -# the search of its bytes found the delimiter -# HTTP-FORM-2 -# HTTP-FORM-4 -law file_is: - for name: String - for filename: String - for ctype: String - for n: U32 - for buf: Array - for hit: Bool - {Pair.fst(List<&2, Form.Part>, List<&1, Bytes.Bytes>, - Form.form.split.file(name, filename, ctype, (Bytes.Bytes{n, buf}, hit), - ([], []))) - == [Form.File{name, filename, ctype, hit}] : List<&2, Form.Part>} - -# LAW (RFC 7578 §4.2): a field part's header is its Content-Disposition line -# HTTP-FORM-4 -law head_field: - for name: String - for value: String - {Form.form.head(Form.Field{name, value}) - == Form.form.disposition(name) ++ "\r\n" : String} - -# LAW (RFC 7578 §4.2, §4.4): a file part's header is its Content-Disposition -# line with the filename, then its Content-Type line -# HTTP-FORM-4 -law head_file: - for name: String - for filename: String - for ctype: String - for clash: Bool - {Form.form.head(Form.File{name, filename, ctype, clash}) - == Form.form.disposition(name) ++ "; filename=\"" ++ Form.form.quote(filename) - ++ "\"\r\n" ++ "Content-Type: " ++ ctype ++ "\r\n" : String} - -# LAW (RFC 7578 §4.2): the disposition names the part, quoted -# HTTP-FORM-4 -law disposition: - for name: String - {Form.form.disposition(name) - == "Content-Disposition: form-data; name=\"" ++ Form.form.quote(name) - ++ "\"" : String} - -# LAW: the empty name is written as the empty string -# HTTP-FORM-5 -law fq_nil: - for s: String - for e: {SNil{} == s : String} - {Form.form.quote(s) == "" : String} - -# LAW: a name is written one character at a time, the first character's -# writing followed by the rest's -# HTTP-FORM-5 -law fq_cons: - for +c: U32 - for t: String - {Form.form.quote(SCon{Chr{c}, t}) == Form.form.one(c, Form.form.quote(t)) - : String} - -# LAW (RFC 7578 §4.2): `"` is written as %22 -# HTTP-FORM-5 -law fq_dq: - for rest: String - {Form.form.one(34, rest) == "%22" ++ rest : String} - -# LAW (RFC 7578 §4.2): CR is written as %0D -# HTTP-FORM-5 -law fq_cr: - for rest: String - {Form.form.one(13, rest) == "%0D" ++ rest : String} - -# LAW (RFC 7578 §4.2): LF is written as %0A -# HTTP-FORM-5 -law fq_lf: - for rest: String - {Form.form.one(10, rest) == "%0A" ++ rest : String} - -# LAW: every other character is written as itself -# HTTP-FORM-5 -law fq_other: - for +c: U32 - for e1: {False{} == U32.is_eq(c, 34) : Bool} - for e2: {False{} == U32.is_eq(c, 13) : Bool} - for e3: {False{} == U32.is_eq(c, 10) : Bool} - for rest: String - {Form.form.one(c, rest) == SCon{Chr{c}, rest} : String} - -# --- percent-decoding and query strings (SPEC.md HTTP-QS) --- - -# LAW: unquote reads a string into bytes and characters from plain text, then -# decodes them as UTF-8 from between characters -# HTTP-QS-1 -law qs_unquote: - for s: String - {Qs.unquote(s) == Qs.utf.dec(Qs.toks(s, False{}, Qs.Plain{}), Qs.Idle{}) - : String} - -# LAW: unquote_plus is the same reading with `+` as a space -# HTTP-QS-3 -law qs_unquote_plus: - for s: String - {Qs.unquote.plus(s) - == Qs.utf.dec(Qs.toks(s, True{}, Qs.Plain{}), Qs.Idle{}) : String} - -# LAW: at the end of the text, what is left open is written as it stands -# HTTP-QS-1 -law qs_toks_nil: - for plus: Bool - for st: Qs.Esc - {Qs.toks("", plus, st) == Qs.tok.end(st) : List<&2, Qs.Tok>} - -# LAW: the text is read one character at a time, from where the reading stands -# HTTP-QS-1 -law qs_toks_cons: - for +c: Char - for t: String - for +plus: Bool - for st: Qs.Esc - {Qs.toks(SCon{c, t}, plus, st) - == Qs.tok.step(plus, st, c, st2 => Qs.toks(t, plus, st2)) - : List<&2, Qs.Tok>} - -# LAW: in plain text, a character outside ASCII is kept as itself -# HTTP-QS-1 -law qs_tok_wide: - for plus: Bool - for +c: Char - for k: Qs.Esc -> List<&2, Qs.Tok> - for e: {True{} == U32.is_ge(Char.to_u32(c), 128) : Bool} - {Qs.tok.step(plus, Qs.Plain{}, c, k) == Qs.W{c} <> k(Qs.Plain{}) - : List<&2, Qs.Tok>} - -# LAW (RFC 3986 §2.1): in plain text, `%` opens an escape -# HTTP-QS-1 -law qs_tok_open: - for plus: Bool - for +c: Char - for k: Qs.Esc -> List<&2, Qs.Tok> - for e1: {False{} == U32.is_ge(Char.to_u32(c), 128) : Bool} - for e2: {True{} == Char.is_eq(c, '%') : Bool} - {Qs.tok.step(plus, Qs.Plain{}, c, k) == k(Qs.Pct{}) : List<&2, Qs.Tok>} - -# LAW: in plain text, every other ASCII character is a byte -# HTTP-QS-1 -# HTTP-QS-3 -law qs_tok_byte: - for plus: Bool - for +c: Char - for k: Qs.Esc -> List<&2, Qs.Tok> - for e1: {False{} == U32.is_ge(Char.to_u32(c), 128) : Bool} - for e2: {False{} == Char.is_eq(c, '%') : Bool} - {Qs.tok.step(plus, Qs.Plain{}, c, k) - == Qs.B{Qs.tok.byte(plus, c)} <> k(Qs.Plain{}) : List<&2, Qs.Tok>} - -# LAW (WHATWG URL §5.1 step 3.3): with `plus`, `+` is the byte of a space -# HTTP-QS-3 -law qs_byte_plus: - for +c: Char - for e: {True{} == Char.is_eq(c, '+') : Bool} - {Qs.tok.byte(True{}, c) == 32 : U32} - -# LAW: any other plain character is the byte of its own code -# HTTP-QS-1 -# HTTP-QS-3 -law qs_byte_other: - for plus: Bool - for +c: Char - for e: {False{} == Bool.and(plus, Char.is_eq(c, '+')) : Bool} - {Qs.tok.byte(plus, c) == Char.to_u32(c) : U32} - -# LAW (RFC 3986 §2.1): after `%`, a hex digit is held for the next one -# HTTP-QS-1 -law qs_tok_pct_hex: - for plus: Bool - for +c: Char - for k: Qs.Esc -> List<&2, Qs.Tok> - for e: {True{} == Qs.tok.hex(c) : Bool} - {Qs.tok.step(plus, Qs.Pct{}, c, k) == k(Qs.Pct1{c}) : List<&2, Qs.Tok>} - -# LAW: after `%`, any other character leaves the `%` as written and is read -# in plain text -# HTTP-QS-1 -law qs_tok_pct_other: - for +plus: Bool - for +c: Char - for k: Qs.Esc -> List<&2, Qs.Tok> - for e: {False{} == Qs.tok.hex(c) : Bool} - {Qs.tok.step(plus, Qs.Pct{}, c, k) - == Qs.B{37} <> Qs.tok.step(plus, Qs.Plain{}, c, k) : List<&2, Qs.Tok>} - -# LAW (RFC 3986 §2.1): `%` and two hex digits are the byte they name, the -# first digit high -# HTTP-QS-1 -law qs_tok_pct1_hex: - for plus: Bool - for +d: Char - for +c: Char - for k: Qs.Esc -> List<&2, Qs.Tok> - for e: {True{} == Qs.tok.hex(c) : Bool} - {Qs.tok.step(plus, Qs.Pct1{d}, c, k) - == Qs.B{U32.add(U32.mul(Http.hex.val(d), 16), Http.hex.val(c))} - <> k(Qs.Plain{}) : List<&2, Qs.Tok>} - -# LAW: `%` and one hex digit before anything else are written as they stand -# and the character is read in plain text -# HTTP-QS-1 -law qs_tok_pct1_other: - for +plus: Bool - for +d: Char - for +c: Char - for k: Qs.Esc -> List<&2, Qs.Tok> - for e: {False{} == Qs.tok.hex(c) : Bool} - {Qs.tok.step(plus, Qs.Pct1{d}, c, k) - == Qs.B{37} <> (Qs.B{Char.to_u32(d)} - <> Qs.tok.step(plus, Qs.Plain{}, c, k)) : List<&2, Qs.Tok>} - -# LAW: plain text ends with nothing left open -# HTTP-QS-1 -law qs_tok_end_plain: - for st: Qs.Esc - for e: {Qs.Plain{} == st : Qs.Esc} - {Qs.tok.end(st) == [] : List<&2, Qs.Tok>} - -# LAW: a `%` at the end is written as it stands -# HTTP-QS-1 -law qs_tok_end_pct: - for st: Qs.Esc - for e: {Qs.Pct{} == st : Qs.Esc} - {Qs.tok.end(st) == [Qs.B{37}] : List<&2, Qs.Tok>} - -# LAW: a `%` and one hex digit at the end are written as they stand -# HTTP-QS-1 -law qs_tok_end_pct1: - for +d: Char - {Qs.tok.end(Qs.Pct1{d}) == [Qs.B{37}, Qs.B{Char.to_u32(d)}] - : List<&2, Qs.Tok>} - -# LAW: a hex digit is a character whose hex value is below 16 -# HTTP-QS-1 -law qs_tok_hex: - for c: Char - {Qs.tok.hex(c) == U32.is_lt(Http.hex.val(c), 16) : Bool} - -# LAW: `0`-`9` are worth 0 to 9 -# HTTP-QS-1 -law qs_hexval_dec: - for +u: U32 - for e: {True{} == Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)) : Bool} - {Http.hex.val(Chr{u}) == U32.sub(u, 48) : U32} - -# LAW: `A`-`F` are worth 10 to 15 -# HTTP-QS-1 -law qs_hexval_upper: - for +u: U32 - for e1: {False{} == Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)) : Bool} - for e2: {False{} == Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)) : Bool} - for e3: {True{} == Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)) : Bool} - {Http.hex.val(Chr{u}) == U32.sub(u, 55) : U32} - -# LAW: `a`-`f` are worth 10 to 15 -# HTTP-QS-1 -law qs_hexval_lower: - for +u: U32 - for e1: {False{} == Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)) : Bool} - for e2: {True{} == Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)) : Bool} - {Http.hex.val(Chr{u}) == U32.sub(u, 87) : U32} - -# LAW: every other character is no hex digit (16) -# HTTP-QS-1 -law qs_hexval_none: - for +u: U32 - for e1: {False{} == Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)) : Bool} - for e2: {False{} == Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)) : Bool} - for e3: {False{} == Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)) : Bool} - {Http.hex.val(Chr{u}) == 16 : U32} - -# LAW: decoding ends where the pieces end -# HTTP-QS-2 -law qs_dec_nil: - for st: Qs.Utf - {Qs.utf.dec([], st) == Qs.utf.end(st) : String} - -# LAW: pieces are decoded one at a time, from where the decoding stands -# HTTP-QS-2 -law qs_dec_cons: - for t: Qs.Tok - for rest: List<&2, Qs.Tok> - for st: Qs.Utf - {Qs.utf.dec(t <> rest, st) - == Qs.utf.step(st, t, st2 => Qs.utf.dec(rest, st2)) : String} - -# LAW: between characters, the end leaves nothing -# HTTP-QS-2 -law qs_end_idle: - for st: Qs.Utf - for e: {Qs.Idle{} == st : Qs.Utf} - {Qs.utf.end(st) == "" : String} - -# LAW (Unicode §3.9): a sequence the end leaves unfinished is one U+FFFD -# HTTP-QS-2 -law qs_end_need: - for cp: U32 - for left: Nat - for lo: U32 - for hi: U32 - {Qs.utf.end(Qs.Need{cp, left, lo, hi}) == SCon{Chr{65533}, ""} : String} - -# LAW: between characters, a character outside ASCII is itself -# HTTP-QS-2 -law qs_idle_wide: - for c: Char - for k: Qs.Utf -> String - {Qs.utf.step(Qs.Idle{}, Qs.W{c}, k) == SCon{c, k(Qs.Idle{})} : String} - -# LAW: between characters, a byte is read by what it leads -# HTTP-QS-2 -law qs_idle_byte: - for +b: U32 - for k: Qs.Utf -> String - {Qs.utf.step(Qs.Idle{}, Qs.B{b}, k) == Qs.utf.lead(b, k, Qs.lead(b)) - : String} - -# LAW: an ASCII byte is its character -# HTTP-QS-2 -law qs_lead_on_ascii: - for +b: U32 - for k: Qs.Utf -> String - {Qs.utf.lead(b, k, Qs.Ascii{}) == SCon{Chr{b}, k(Qs.Idle{})} : String} - -# LAW (Unicode §3.9): a byte no sequence starts with is one U+FFFD -# HTTP-QS-2 -law qs_lead_on_bad: - for +b: U32 - for k: Qs.Utf -> String - {Qs.utf.lead(b, k, Qs.Bad{}) == SCon{Chr{65533}, k(Qs.Idle{})} : String} - -# LAW: a lead byte opens its sequence -# HTTP-QS-2 -law qs_lead_on_opens: - for +b: U32 - for k: Qs.Utf -> String - for cp: U32 - for left: Nat - for lo: U32 - for hi: U32 - {Qs.utf.lead(b, k, Qs.Opens{cp, left, lo, hi}) - == k(Qs.Need{cp, left, lo, hi}) : String} - -# LAW (RFC 3629 §4): 00 to 7F is ASCII -# HTTP-QS-2 -law qs_lead_ascii: - for +b: U32 - for e: {True{} == U32.is_lt(b, 128) : Bool} - {Qs.lead(b) == Qs.Ascii{} : Qs.Lead} - -# LAW (RFC 3629 §4): 80 to C1 start no sequence -# HTTP-QS-2 -law qs_lead_low: - for +b: U32 - for e1: {False{} == U32.is_lt(b, 128) : Bool} - for e2: {True{} == U32.is_lt(b, 194) : Bool} - {Qs.lead(b) == Qs.Bad{} : Qs.Lead} - -# LAW (RFC 3629 §4): C2 to DF need one byte 80 to BF -# HTTP-QS-2 -law qs_lead_two: - for +b: U32 - for e1: {False{} == U32.is_lt(b, 128) : Bool} - for e2: {False{} == U32.is_lt(b, 194) : Bool} - for e3: {True{} == U32.is_lt(b, 224) : Bool} - {Qs.lead(b) == Qs.Opens{U32.and(b, 31), 0n, 128, 191} : Qs.Lead} - -# LAW (RFC 3629 §4): E0 needs A0 to BF, then one byte -# HTTP-QS-2 -law qs_lead_e0: - for +b: U32 - for e1: {False{} == U32.is_lt(b, 128) : Bool} - for e2: {False{} == U32.is_lt(b, 194) : Bool} - for e3: {False{} == U32.is_lt(b, 224) : Bool} - for e4: {True{} == U32.is_eq(b, 224) : Bool} - {Qs.lead(b) == Qs.Opens{0, 1n, 160, 191} : Qs.Lead} - -# LAW (RFC 3629 §4): E1 to EC need 80 to BF, then one byte -# HTTP-QS-2 -law qs_lead_e1: - for +b: U32 - for e1: {False{} == U32.is_lt(b, 128) : Bool} - for e2: {False{} == U32.is_lt(b, 194) : Bool} - for e3: {False{} == U32.is_lt(b, 224) : Bool} - for e4: {False{} == U32.is_eq(b, 224) : Bool} - for e5: {True{} == U32.is_lt(b, 237) : Bool} - {Qs.lead(b) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} : Qs.Lead} - -# LAW (RFC 3629 §4): ED needs 80 to 9F (no surrogates), then one byte -# HTTP-QS-2 -law qs_lead_ed: - for +b: U32 - for e1: {False{} == U32.is_lt(b, 128) : Bool} - for e2: {False{} == U32.is_lt(b, 194) : Bool} - for e3: {False{} == U32.is_lt(b, 224) : Bool} - for e4: {False{} == U32.is_eq(b, 224) : Bool} - for e5: {False{} == U32.is_lt(b, 237) : Bool} - for e6: {True{} == U32.is_eq(b, 237) : Bool} - {Qs.lead(b) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} - -# LAW (RFC 3629 §4): EE and EF need 80 to BF, then one byte -# HTTP-QS-2 -law qs_lead_ee: - for +b: U32 - for e1: {False{} == U32.is_lt(b, 128) : Bool} - for e2: {False{} == U32.is_lt(b, 194) : Bool} - for e3: {False{} == U32.is_lt(b, 224) : Bool} - for e4: {False{} == U32.is_eq(b, 224) : Bool} - for e5: {False{} == U32.is_lt(b, 237) : Bool} - for e6: {False{} == U32.is_eq(b, 237) : Bool} - for e7: {True{} == U32.is_lt(b, 240) : Bool} - {Qs.lead(b) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} : Qs.Lead} - -# LAW (RFC 3629 §4): F0 needs 90 to BF, then two bytes -# HTTP-QS-2 -law qs_lead_f0: - for +b: U32 - for e1: {False{} == U32.is_lt(b, 128) : Bool} - for e2: {False{} == U32.is_lt(b, 194) : Bool} - for e3: {False{} == U32.is_lt(b, 224) : Bool} - for e4: {False{} == U32.is_eq(b, 224) : Bool} - for e5: {False{} == U32.is_lt(b, 237) : Bool} - for e6: {False{} == U32.is_eq(b, 237) : Bool} - for e7: {False{} == U32.is_lt(b, 240) : Bool} - for e8: {True{} == U32.is_eq(b, 240) : Bool} - {Qs.lead(b) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} - -# LAW (RFC 3629 §4): F1 to F3 need 80 to BF, then two bytes -# HTTP-QS-2 -law qs_lead_f1: - for +b: U32 - for e1: {False{} == U32.is_lt(b, 128) : Bool} - for e2: {False{} == U32.is_lt(b, 194) : Bool} - for e3: {False{} == U32.is_lt(b, 224) : Bool} - for e4: {False{} == U32.is_eq(b, 224) : Bool} - for e5: {False{} == U32.is_lt(b, 237) : Bool} - for e6: {False{} == U32.is_eq(b, 237) : Bool} - for e7: {False{} == U32.is_lt(b, 240) : Bool} - for e8: {False{} == U32.is_eq(b, 240) : Bool} - for e9: {True{} == U32.is_lt(b, 244) : Bool} - {Qs.lead(b) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} : Qs.Lead} - -# LAW (RFC 3629 §4): F4 needs 80 to 8F (up to U+10FFFF), then two bytes -# HTTP-QS-2 -law qs_lead_f4: - for +b: U32 - for e1: {False{} == U32.is_lt(b, 128) : Bool} - for e2: {False{} == U32.is_lt(b, 194) : Bool} - for e3: {False{} == U32.is_lt(b, 224) : Bool} - for e4: {False{} == U32.is_eq(b, 224) : Bool} - for e5: {False{} == U32.is_lt(b, 237) : Bool} - for e6: {False{} == U32.is_eq(b, 237) : Bool} - for e7: {False{} == U32.is_lt(b, 240) : Bool} - for e8: {False{} == U32.is_eq(b, 240) : Bool} - for e9: {False{} == U32.is_lt(b, 244) : Bool} - for e10: {True{} == U32.is_eq(b, 244) : Bool} - {Qs.lead(b) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} - -# LAW (RFC 3629 §4): F5 and up start no sequence -# HTTP-QS-2 -law qs_lead_high: - for +b: U32 - for e1: {False{} == U32.is_lt(b, 128) : Bool} - for e2: {False{} == U32.is_lt(b, 194) : Bool} - for e3: {False{} == U32.is_lt(b, 224) : Bool} - for e4: {False{} == U32.is_eq(b, 224) : Bool} - for e5: {False{} == U32.is_lt(b, 237) : Bool} - for e6: {False{} == U32.is_eq(b, 237) : Bool} - for e7: {False{} == U32.is_lt(b, 240) : Bool} - for e8: {False{} == U32.is_eq(b, 240) : Bool} - for e9: {False{} == U32.is_lt(b, 244) : Bool} - for e10: {False{} == U32.is_eq(b, 244) : Bool} - {Qs.lead(b) == Qs.Bad{} : Qs.Lead} - -# LAW (Unicode §3.9): a character outside ASCII inside a sequence ends it -# unfinished, one U+FFFD, and is itself -# HTTP-QS-2 -law qs_need_wide: - for cp: U32 - for left: Nat - for lo: U32 - for hi: U32 - for c: Char - for k: Qs.Utf -> String - {Qs.utf.step(Qs.Need{cp, left, lo, hi}, Qs.W{c}, k) - == SCon{Chr{65533}, SCon{c, k(Qs.Idle{})}} : String} - -# LAW (RFC 3629 §3): the last byte that fits ends the character -# HTTP-QS-2 -law qs_need_last: - for +cp: U32 - for +lo: U32 - for +hi: U32 - for +b: U32 - for k: Qs.Utf -> String - for e: {True{} == Bool.and(U32.is_le(lo, b), U32.is_le(b, hi)) : Bool} - {Qs.utf.step(Qs.Need{cp, 0n, lo, hi}, Qs.B{b}, k) - == SCon{Chr{Qs.utf.add(cp, b)}, k(Qs.Idle{})} : String} - -# LAW (RFC 3629 §3): a byte that fits with more to come adds its bits and -# needs the next in 80 to BF -# HTTP-QS-2 -law qs_need_more: - for +cp: U32 - for l: Nat - for +lo: U32 - for +hi: U32 - for +b: U32 - for k: Qs.Utf -> String - for e: {True{} == Bool.and(U32.is_le(lo, b), U32.is_le(b, hi)) : Bool} - {Qs.utf.step(Qs.Need{cp, 1n+l, lo, hi}, Qs.B{b}, k) - == k(Qs.Need{Qs.utf.add(cp, b), l, 128, 191}) : String} - -# LAW (Unicode §3.9): a byte that does not fit ends the sequence unfinished, -# one U+FFFD, and is read again between characters -# HTTP-QS-2 -law qs_need_miss: - for +cp: U32 - for left: Nat - for +lo: U32 - for +hi: U32 - for +b: U32 - for k: Qs.Utf -> String - for e: {False{} == Bool.and(U32.is_le(lo, b), U32.is_le(b, hi)) : Bool} - {Qs.utf.step(Qs.Need{cp, left, lo, hi}, Qs.B{b}, k) - == SCon{Chr{65533}, Qs.utf.lead(b, k, Qs.lead(b))} : String} - -# LAW (RFC 3629 §3): a continuation byte adds its six low bits -# HTTP-QS-2 -law qs_add: - for +cp: U32 - for +b: U32 - {Qs.utf.add(cp, b) == U32.or(U32.shln(cp, 6n), U32.and(b, 63)) : U32} - -# LAW: quote_plus writes nothing for the empty string -# HTTP-QS-4 -law qs_qp_nil: - for s: String - for e: {"" == s : String} - {Qs.quote.plus(s) == "" : String} - -# LAW: a space is written `+` -# HTTP-QS-4 -law qs_qp_space: - for t: String - {Qs.quote.plus(SCon{' ', t}) == SCon{'+', Qs.quote.plus(t)} : String} - -# LAW: every other character is written as quote_component writes it -# HTTP-QS-4 -law qs_qp_other: - for +cp: U32 - for t: String - for e: {False{} == U32.is_eq(cp, 32) : Bool} - {Qs.quote.plus(SCon{Chr{cp}, t}) == Pct.one(False{}, cp, Qs.quote.plus(t)) - : String} - -# LAW: no pairs are the empty query -# HTTP-QS-5 -law qs_ue_nil: - for ps: List<&2, Qs.Kv()> - for e: {[] == ps : List<&2, Qs.Kv()>} - {Qs.urlencode(ps) == "" : String} - -# LAW: the first pair, then the others each after `&` -# HTTP-QS-5 -law qs_ue_cons: - for name: String - for value: String - for t: List<&2, Qs.Kv()> - {Qs.urlencode((name, value) <> t) == Qs.form.pair(name, value) - ++ Qs.form.more(t) : String} - -# LAW: after the last pair, nothing -# HTTP-QS-5 -law qs_more_nil: - for ps: List<&2, Qs.Kv()> - for e: {[] == ps : List<&2, Qs.Kv()>} - {Qs.form.more(ps) == "" : String} - -# LAW: each pair after the first is written after `&` -# HTTP-QS-5 -law qs_more_cons: - for name: String - for value: String - for t: List<&2, Qs.Kv()> - {Qs.form.more((name, value) <> t) - == "&" ++ Qs.form.pair(name, value) ++ Qs.form.more(t) : String} - -# LAW: a pair is its quote_plus'd name, `=` and its quote_plus'd value -# HTTP-QS-5 -law qs_pair: - for name: String - for value: String - {Qs.form.pair(name, value) - == Qs.quote.plus(name) ++ "=" ++ Qs.quote.plus(value) : String} - -# LAW (WHATWG URL §5.1 step 1): a query is split on every `&` -# HTTP-QS-6 -law qs_parse: - for text: String - {Qs.parse(text) == Qs.fields(String.split(text, '&')) - : List<&2, Qs.Kv()>} - -# LAW: no pieces, no pairs -# HTTP-QS-6 -law qs_fields_nil: - for ps: List<&2, String> - for e: {[] == ps : List<&2, String>} - {Qs.fields(ps) == [] : List<&2, Qs.Kv()>} - -# LAW (WHATWG URL §5.1 step 3.1): an empty piece is skipped -# HTTP-QS-6 -law qs_fields_skip: - for t: List<&2, String> - {Qs.fields("" <> t) == Qs.fields(t) : List<&2, Qs.Kv()>} - -# LAW: any other piece is a pair, in front of the rest's, in order -# HTTP-QS-6 -law qs_fields_keep: - for +p: String - for t: List<&2, String> - for e: {False{} == String.is_empty(p) : Bool} - {Qs.fields(p <> t) == Qs.field(p) <> Qs.fields(t) : List<&2, Qs.Kv()>} - -# LAW: a piece is read by where its first `=` is -# HTTP-QS-6 -law qs_field: - for +piece: String - {Qs.field(piece) == Qs.field.of(Qs.cut(piece), piece) : Qs.Kv()} - -# LAW (WHATWG URL §5.1 step 3.2): a piece with no `=` is a name with an -# empty value -# HTTP-QS-6 -law qs_field_none: - for whole: String - {Qs.field.of(None{}, whole) == (Qs.unquote.plus(whole), "") : Qs.Kv()} - -# LAW (WHATWG URL §5.1 steps 3.2 to 3.4): a piece cut at its first `=` is the -# name before it and the value after it, both unquote_plus'd -# HTTP-QS-6 -law qs_field_some: - for name: String - for value: String - for whole: String - {Qs.field.of(Some{(name, value)}, whole) - == (Qs.unquote.plus(name), Qs.unquote.plus(value)) : Qs.Kv()} - -# LAW: a text with no `=` before one is cut at that one -# HTTP-QS-6 -# HTTP-CK-1 -law qs_cut_found: - for +s: String - for r: String - for +e: {avoids(s, '=') == True{} : Bool} - {Qs.cut(String.append(s, SCon{'=', r})) == Some{(s, r)} - : Maybe<&2, Qs.Kv()>} - -# LAW: a text with no `=` is not cut -# HTTP-QS-6 -# HTTP-CK-1 -law qs_cut_none: - for +s: String - for +e: {avoids(s, '=') == True{} : Bool} - {Qs.cut(s) == None{} : Maybe<&2, Qs.Kv()>} - -# LAW: no pairs give no value -# HTTP-QS-7 -law qs_get_nil: - for name: String - {Qs.get([], name) == None{} : Maybe<&2, String>} - -# LAW: a later pair with the name wins -# HTTP-QS-7 -law qs_get_later: - for key: String - for value: String - for t: List<&2, Qs.Kv()> - for +name: String - for x: String - for e: {Some{x} == Qs.get(t, name) : Maybe<&2, String>} - {Qs.get((key, value) <> t, name) == Some{x} : Maybe<&2, String>} - -# LAW: with no later pair with the name, this one's value when it has it -# HTTP-QS-7 -law qs_get_here: - for key: String - for value: String - for t: List<&2, Qs.Kv()> - for +name: String - for e1: {None{} == Qs.get(t, name) : Maybe<&2, String>} - for e2: {True{} == String.eq(key, name) : Bool} - {Qs.get((key, value) <> t, name) == Some{value} : Maybe<&2, String>} - -# LAW: and none when it does not -# HTTP-QS-7 -law qs_get_miss: - for key: String - for value: String - for t: List<&2, Qs.Kv()> - for +name: String - for e1: {None{} == Qs.get(t, name) : Maybe<&2, String>} - for e2: {False{} == String.eq(key, name) : Bool} - {Qs.get((key, value) <> t, name) == None{} : Maybe<&2, String>} - -# --- HTML character references (SPEC.md HTTP-REF) --- - -# LAW: html_unescape scans a text from text -# HTTP-REF-1 -law ref_unescape: - for s: String - {Ref.unescape(s) == Ref.scan(s, Ref.Txt{}) : String} - -# LAW: at the end of the text, what is open ends there -# HTTP-REF-1 -law ref_scan_nil: - for st: Ref.Scan - {Ref.scan("", st) == Ref.end(st) : String} - -# LAW: the text is scanned one character at a time -# HTTP-REF-1 -law ref_scan_cons: - for +c: Char - for t: String - for st: Ref.Scan - {Ref.scan(SCon{c, t}, st) == Ref.step(st, c, st2 => Ref.scan(t, st2)) - : String} - -# LAW: in text, `&` may open a reference -# HTTP-REF-1 -law ref_text_amp: - for +c: Char - for k: Ref.Scan -> String - for e: {True{} == Char.is_eq(c, '&') : Bool} - {Ref.step(Ref.Txt{}, c, k) == k(Ref.Amp{}) : String} - -# LAW: in text, every other character is itself -# HTTP-REF-1 -law ref_text_other: - for +c: Char - for k: Ref.Scan -> String - for e: {False{} == Char.is_eq(c, '&') : Bool} - {Ref.step(Ref.Txt{}, c, k) == SCon{c, k(Ref.Txt{})} : String} - -# LAW (§13.2.5.72): after `&`, `#` opens a number -# HTTP-REF-1 -law ref_amp_hash: - for +c: Char - for k: Ref.Scan -> String - for e: {True{} == Char.is_eq(c, '#') : Bool} - {Ref.step(Ref.Amp{}, c, k) == k(Ref.Hash{}) : String} - -# LAW (§13.2.5.73): after `&`, a name character opens a name -# HTTP-REF-1 -law ref_amp_name: - for +c: Char - for k: Ref.Scan -> String - for e1: {False{} == Char.is_eq(c, '#') : Bool} - for e2: {True{} == Ref.name.char(c) : Bool} - {Ref.step(Ref.Amp{}, c, k) == k(Ref.Name{SCon{c, ""}, 1n}) : String} - -# LAW: after `&`, anything else leaves the `&` as text and is read in text -# HTTP-REF-1 -law ref_amp_other: - for +c: Char - for k: Ref.Scan -> String - for e1: {False{} == Char.is_eq(c, '#') : Bool} - for e2: {False{} == Ref.name.char(c) : Bool} - {Ref.step(Ref.Amp{}, c, k) == SCon{'&', Ref.step(Ref.Txt{}, c, k)} - : String} - -# LAW (§13.2.5.78): after `&#`, a decimal digit opens a decimal number -# HTTP-REF-1 -law ref_hash_dec: - for +c: Char - for k: Ref.Scan -> String - for e: {True{} == U32.is_lt(Ref.digit(False{}, c), 16) : Bool} - {Ref.step(Ref.Hash{}, c, k) == k(Ref.Num{False{}, Ref.digit(False{}, c)}) - : String} - -# LAW (§13.2.5.76): after `&#`, `x` or `X` may open a hex number -# HTTP-REF-1 -law ref_hash_x: - for +c: Char - for k: Ref.Scan -> String - for e1: {False{} == U32.is_lt(Ref.digit(False{}, c), 16) : Bool} - for e2: {True{} == Bool.or(Char.is_eq(c, 'x'), Char.is_eq(c, 'X')) : Bool} - {Ref.step(Ref.Hash{}, c, k) == k(Ref.HashX{c}) : String} - -# LAW: after `&#`, anything else leaves `&#` as text and is read in text -# HTTP-REF-1 -law ref_hash_other: - for +c: Char - for k: Ref.Scan -> String - for e1: {False{} == U32.is_lt(Ref.digit(False{}, c), 16) : Bool} - for e2: {False{} == Bool.or(Char.is_eq(c, 'x'), Char.is_eq(c, 'X')) : Bool} - {Ref.step(Ref.Hash{}, c, k) == "&#" ++ Ref.step(Ref.Txt{}, c, k) : String} - -# LAW (§13.2.5.77): after `&#x`, a hex digit opens a hex number -# HTTP-REF-1 -law ref_hashx_digit: - for +x: Char - for +c: Char - for k: Ref.Scan -> String - for e: {True{} == U32.is_lt(Ref.digit(True{}, c), 16) : Bool} - {Ref.step(Ref.HashX{x}, c, k) == k(Ref.Num{True{}, Ref.digit(True{}, c)}) - : String} - -# LAW: after `&#x`, anything else leaves `&#x` as text and is read in text -# HTTP-REF-1 -law ref_hashx_other: - for +x: Char - for +c: Char - for k: Ref.Scan -> String - for e: {False{} == U32.is_lt(Ref.digit(True{}, c), 16) : Bool} - {Ref.step(Ref.HashX{x}, c, k) - == SCon{'&', SCon{'#', SCon{x, Ref.step(Ref.Txt{}, c, k)}}} : String} - -# LAW: inside a number, a digit of its base grows it -# HTTP-REF-1 -law ref_num_digit: - for +hex: Bool - for +n: U32 - for +c: Char - for k: Ref.Scan -> String - for e: {True{} == U32.is_lt(Ref.digit(hex, c), 16) : Bool} - {Ref.step(Ref.Num{hex, n}, c, k) - == k(Ref.Num{hex, Ref.grow(hex, n, Ref.digit(hex, c))}) : String} - -# LAW (§13.2.5.79): inside a number, `;` ends the reference with it -# HTTP-REF-1 -law ref_num_semi: - for +hex: Bool - for +n: U32 - for +c: Char - for k: Ref.Scan -> String - for e1: {False{} == U32.is_lt(Ref.digit(hex, c), 16) : Bool} - for e2: {True{} == Char.is_eq(c, ';') : Bool} - {Ref.step(Ref.Num{hex, n}, c, k) == Ref.code(n) ++ k(Ref.Txt{}) : String} - -# LAW: inside a number, anything else ends the reference and is read in text -# HTTP-REF-1 -law ref_num_other: - for +hex: Bool - for +n: U32 - for +c: Char - for k: Ref.Scan -> String - for e1: {False{} == U32.is_lt(Ref.digit(hex, c), 16) : Bool} - for e2: {False{} == Char.is_eq(c, ';') : Bool} - {Ref.step(Ref.Num{hex, n}, c, k) == Ref.code(n) ++ Ref.step(Ref.Txt{}, c, k) - : String} - -# LAW: inside a name, `;` ends the reference with it -# HTTP-REF-1 -law ref_name_semi: - for +s: String - for +len: Nat - for +c: Char - for k: Ref.Scan -> String - for e: {True{} == Char.is_eq(c, ';') : Bool} - {Ref.step(Ref.Name{s, len}, c, k) == Ref.named(s ++ ";") ++ k(Ref.Txt{}) - : String} - -# LAW: inside a name of fewer than 32 characters, a name character joins it -# HTTP-REF-1 -law ref_name_join: - for +s: String - for +len: Nat - for +c: Char - for k: Ref.Scan -> String - for e1: {False{} == Char.is_eq(c, ';') : Bool} - for e2: {True{} == Bool.and(Ref.name.char(c), Nat.is_lt(len, 32n)) : Bool} - {Ref.step(Ref.Name{s, len}, c, k) == k(Ref.Name{s ++ SCon{c, ""}, 1n+len}) - : String} - -# LAW: inside a name, anything else ends the reference and is read in text -# HTTP-REF-1 -law ref_name_stop: - for +s: String - for +len: Nat - for +c: Char - for k: Ref.Scan -> String - for e1: {False{} == Char.is_eq(c, ';') : Bool} - for e2: {False{} == Bool.and(Ref.name.char(c), Nat.is_lt(len, 32n)) : Bool} - {Ref.step(Ref.Name{s, len}, c, k) == Ref.named(s) ++ Ref.step(Ref.Txt{}, c, k) - : String} - -# LAW: text ends with nothing open -# HTTP-REF-1 -law ref_end_text: - for st: Ref.Scan - for e: {Ref.Txt{} == st : Ref.Scan} - {Ref.end(st) == "" : String} - -# LAW: an `&` at the end is text -# HTTP-REF-1 -law ref_end_amp: - for st: Ref.Scan - for e: {Ref.Amp{} == st : Ref.Scan} - {Ref.end(st) == "&" : String} - -# LAW: an `&#` at the end is text -# HTTP-REF-1 -law ref_end_hash: - for st: Ref.Scan - for e: {Ref.Hash{} == st : Ref.Scan} - {Ref.end(st) == "&#" : String} - -# LAW: an `&#x` at the end is text -# HTTP-REF-1 -law ref_end_hashx: - for x: Char - {Ref.end(Ref.HashX{x}) == SCon{'&', SCon{'#', SCon{x, ""}}} : String} - -# LAW: a number at the end ends its reference -# HTTP-REF-1 -law ref_end_num: - for hex: Bool - for +n: U32 - {Ref.end(Ref.Num{hex, n}) == Ref.code(n) : String} - -# LAW: a name at the end ends its reference -# HTTP-REF-1 -law ref_end_name: - for +s: String - for len: Nat - {Ref.end(Ref.Name{s, len}) == Ref.named(s) : String} - -# LAW (§13.2.5.73): a name character is anything but TAB, LF, FF, space, `<`, -# `&`, `#` and `;` -# HTTP-REF-1 -law ref_name_char: - for +c: Char - {Ref.name.char(c) == Bool.not(Bool.or(Char.is_eq(c, '\t'), - Bool.or(Char.is_eq(c, '\n'), Bool.or(Char.is_eq(c, '\u{C}'), - Bool.or(Char.is_eq(c, ' '), Bool.or(Char.is_eq(c, '<'), - Bool.or(Char.is_eq(c, '&'), Bool.or(Char.is_eq(c, '#'), - Char.is_eq(c, ';'))))))))) : Bool} - -# LAW: a decimal digit `0`-`9` is worth 0 to 9 -# HTTP-REF-1 -law ref_digit_dec: - for +u: U32 - for e: {True{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} - {Ref.digit(False{}, Chr{u}) == U32.sub(u, 48) : U32} - -# LAW: any other character is no decimal digit -# HTTP-REF-1 -law ref_digit_dec_none: - for +u: U32 - for e: {False{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} - {Ref.digit(False{}, Chr{u}) == 16 : U32} - -# LAW: a hex digit `0`-`9` is worth 0 to 9 -# HTTP-REF-1 -law ref_digit_hex_dec: - for +u: U32 - for e: {True{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} - {Ref.digit(True{}, Chr{u}) == U32.sub(u, 48) : U32} - -# LAW: a hex digit `A`-`F` is worth 10 to 15 -# HTTP-REF-1 -law ref_digit_hex_upper: - for +u: U32 - for e1: {False{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} - for e2: {True{} == Bool.and(U32.is_le(65, u), U32.is_le(u, 70)) : Bool} - {Ref.digit(True{}, Chr{u}) == U32.sub(u, 55) : U32} - -# LAW: a hex digit `a`-`f` is worth 10 to 15 -# HTTP-REF-1 -law ref_digit_hex_lower: - for +u: U32 - for e1: {False{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} - for e2: {False{} == Bool.and(U32.is_le(65, u), U32.is_le(u, 70)) : Bool} - for e3: {True{} == Bool.and(U32.is_le(97, u), U32.is_le(u, 102)) : Bool} - {Ref.digit(True{}, Chr{u}) == U32.sub(u, 87) : U32} - -# LAW: any other character is no hex digit -# HTTP-REF-1 -law ref_digit_hex_none: - for +u: U32 - for e1: {False{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} - for e2: {False{} == Bool.and(U32.is_le(65, u), U32.is_le(u, 70)) : Bool} - for e3: {False{} == Bool.and(U32.is_le(97, u), U32.is_le(u, 102)) : Bool} - {Ref.digit(True{}, Chr{u}) == 16 : U32} - -# LAW: a digit grows a number as n times the base plus the digit, held at -# 0x110000 -# HTTP-REF-1 -law ref_grow: - for +hex: Bool - for +n: U32 - for +d: U32 - {Ref.grow(hex, n, d) - == U32.min(U32.add(U32.mul(n, Bool.pick(U32, hex, 16, 10)), d), 1114112) - : U32} - -# LAW (§13.2.5.80): a number is read from 0 on -# HTTP-REF-2 -law ref_code: - for +n: U32 - {Ref.code(n) == Ref.code.zero(n, U32.is_eq(n, 0)) : String} - -# LAW (§13.2.5.80): 0 is U+FFFD -# HTTP-REF-2 -law ref_code_zero: - for +n: U32 - for e: {True{} == U32.is_eq(n, 0) : Bool} - {Ref.code(n) == SCon{Chr{65533}, ""} : String} - -# LAW (§13.2.5.80): 0x0D is a carriage return -# HTTP-REF-2 -law ref_code_cr: - for +n: U32 - for e1: {False{} == U32.is_eq(n, 0) : Bool} - for e2: {True{} == U32.is_eq(n, 13) : Bool} - {Ref.code(n) == "\r" : String} - -# LAW (§13.2.5.80): 0x80 to 0x9F are the table's -# HTTP-REF-2 -law ref_code_c1: - for +n: U32 - for e1: {False{} == U32.is_eq(n, 0) : Bool} - for e2: {False{} == U32.is_eq(n, 13) : Bool} - for e3: {True{} == Bool.and(U32.is_le(128, n), U32.is_le(n, 159)) : Bool} - {Ref.code(n) - == SCon{Chr{Ref.nth(Ref.w1252(), U32.to_nat(U32.sub(n, 128)))}, ""} - : String} - -# LAW (§13.2.5.80): a surrogate or a number past U+10FFFF is U+FFFD -# HTTP-REF-2 -law ref_code_bad: - for +n: U32 - for e1: {False{} == U32.is_eq(n, 0) : Bool} - for e2: {False{} == U32.is_eq(n, 13) : Bool} - for e3: {False{} == Bool.and(U32.is_le(128, n), U32.is_le(n, 159)) : Bool} - for e4: {True{} == Bool.or(Bool.and(U32.is_le(55296, n), - U32.is_le(n, 57343)), U32.is_lt(1114111, n)) : Bool} - {Ref.code(n) == SCon{Chr{65533}, ""} : String} - -# LAW: a control or noncharacter Python drops is nothing -# HTTP-REF-2 -law ref_code_dropped: - for +n: U32 - for e1: {False{} == U32.is_eq(n, 0) : Bool} - for e2: {False{} == U32.is_eq(n, 13) : Bool} - for e3: {False{} == Bool.and(U32.is_le(128, n), U32.is_le(n, 159)) : Bool} - for e4: {False{} == Bool.or(Bool.and(U32.is_le(55296, n), - U32.is_le(n, 57343)), U32.is_lt(1114111, n)) : Bool} - for e5: {True{} == Ref.dropped(n) : Bool} - {Ref.code(n) == "" : String} - -# LAW: every other number is its character -# HTTP-REF-2 -law ref_code_kept: - for +n: U32 - for e1: {False{} == U32.is_eq(n, 0) : Bool} - for e2: {False{} == U32.is_eq(n, 13) : Bool} - for e3: {False{} == Bool.and(U32.is_le(128, n), U32.is_le(n, 159)) : Bool} - for e4: {False{} == Bool.or(Bool.and(U32.is_le(55296, n), - U32.is_le(n, 57343)), U32.is_lt(1114111, n)) : Bool} - for e5: {False{} == Ref.dropped(n) : Bool} - {Ref.code(n) == SCon{Chr{n}, ""} : String} - -# LAW: what Python drops: 0x01 to 0x08, 0x0B, 0x0E to 0x1F, 0x7F to 0x9F, -# 0xFDD0 to 0xFDEF, and low 16 bits 0xFFFE or 0xFFFF -# HTTP-REF-2 -law ref_dropped: - for +n: U32 - {Ref.dropped(n) == Bool.or(Bool.and(U32.is_le(1, n), U32.is_le(n, 8)), - Bool.or(U32.is_eq(n, 11), - Bool.or(Bool.and(U32.is_le(14, n), U32.is_le(n, 31)), - Bool.or(Bool.and(U32.is_le(127, n), U32.is_le(n, 159)), - Bool.or(Bool.and(U32.is_le(64976, n), U32.is_le(n, 65007)), - U32.is_eq(U32.and(n, 65534), 65534)))))) : Bool} - -# LAW (§13.2.5.73): a name is read against the whole table, with no best yet -# HTTP-REF-3 -law ref_named: - for +s: String - {Ref.named(s) == Ref.named.of(Ref.best(Ents.table(), s, None{}), s) - : String} - -# LAW: with no name the text starts with, `&` and the text are written as -# they are -# HTTP-REF-3 -law ref_named_none: - for s: String - {Ref.named.of(None{}, s) == SCon{'&', s} : String} - -# LAW: with one, its characters, then the text after it as written -# HTTP-REF-3 -law ref_named_some: - for +name: String - for first: U32 - for second: U32 - for s: String - {Ref.named.of(Some{Ents.Ref{name, first, second}}, s) - == Ref.chars(first, second) ++ String.drop(s, String.length(name)) - : String} - -# LAW: after the last row, the best so far -# HTTP-REF-3 -law ref_best_nil: - for s: String - for sofar: Maybe<&2, Ents.Ref> - {Ref.best([], s, sofar) == sofar : Maybe<&2, Ents.Ref>} - -# LAW: the rows are read in order, each against the best so far -# HTTP-REF-3 -law ref_best_cons: - for r: Ents.Ref - for t: List<&2, Ents.Ref> - for +s: String - for +sofar: Maybe<&2, Ents.Ref> - {Ref.best(r <> t, s, sofar) == Ref.best(t, s, Ref.best.step(sofar, r, s)) - : Maybe<&2, Ents.Ref>} - -# LAW: a row is a candidate when the text starts with its name -# HTTP-REF-3 -law ref_step: - for +sofar: Maybe<&2, Ents.Ref> - for +name: String - for first: U32 - for second: U32 - for +s: String - {Ref.best.step(sofar, Ents.Ref{name, first, second}, s) - == Ref.best.hit(sofar, Ents.Ref{name, first, second}, name, - String.starts_with(s, name)) : Maybe<&2, Ents.Ref>} - -# LAW: a row whose name the text does not start with keeps the best -# HTTP-REF-3 -law ref_hit_miss: - for sofar: Maybe<&2, Ents.Ref> - for r: Ents.Ref - for name: String - {Ref.best.hit(sofar, r, name, False{}) == sofar : Maybe<&2, Ents.Ref>} - -# LAW: a candidate is compared by length with the best -# HTTP-REF-3 -law ref_hit_take: - for +sofar: Maybe<&2, Ents.Ref> - for +r: Ents.Ref - for +name: String - {Ref.best.hit(sofar, r, name, True{}) - == Ref.best.take(sofar, r, Nat.is_lt(Ref.best.len(sofar), - String.length(name))) : Maybe<&2, Ents.Ref>} - -# LAW: a longer candidate is the new best -# HTTP-REF-3 -law ref_take_longer: - for sofar: Maybe<&2, Ents.Ref> - for r: Ents.Ref - {Ref.best.take(sofar, r, True{}) == Some{r} : Maybe<&2, Ents.Ref>} - -# LAW: any other keeps the best -# HTTP-REF-3 -law ref_take_keep: - for sofar: Maybe<&2, Ents.Ref> - for r: Ents.Ref - {Ref.best.take(sofar, r, False{}) == sofar : Maybe<&2, Ents.Ref>} - -# LAW: no best yet is length 0 -# HTTP-REF-3 -law ref_len_none: - for m: Maybe<&2, Ents.Ref> - for e: {None{} == m : Maybe<&2, Ents.Ref>} - {Ref.best.len(m) == 0n : Nat} - -# LAW: a best is its name's length -# HTTP-REF-3 -law ref_len_some: - for +name: String - for first: U32 - for second: U32 - {Ref.best.len(Some{Ents.Ref{name, first, second}}) == String.length(name) - : Nat} - -# LAW: a row with one code point stands for that character -# HTTP-REF-3 -law ref_chars_one: - for first: U32 - for +second: U32 - for e: {True{} == U32.is_eq(second, 0) : Bool} - {Ref.chars(first, second) == SCon{Chr{first}, ""} : String} - -# LAW: a row with two stands for both, in order -# HTTP-REF-3 -law ref_chars_two: - for first: U32 - for +second: U32 - for e: {False{} == U32.is_eq(second, 0) : Bool} - {Ref.chars(first, second) == SCon{Chr{first}, SCon{Chr{second}, ""}} - : String} - -# --- cookies sent back (SPEC.md HTTP-CK) --- - -# LAW (RFC 6265 §5.2 step 1): a Set-Cookie value's name-value-pair is the -# text before its first `;`, cut at its first `=` -# HTTP-CK-1 -law ck_pair: - for line: String - {Echo.pair(line) == Echo.pair.cut(Qs.cut(Http.first(String.split(line, ';')))) - : Maybe<&2, Qs.Kv()>} - -# LAW (§5.2 step 2): a pair with no `=` sets nothing -# HTTP-CK-1 -law ck_pair_none: - for u: Maybe<&2, Qs.Kv()> - for e: {None{} == u : Maybe<&2, Qs.Kv()>} - {Echo.pair.cut(u) == None{} : Maybe<&2, Qs.Kv()>} - -# LAW (§5.2 steps 3, 4): the name and the value without leading and trailing -# WSP -# HTTP-CK-1 -law ck_pair_some: - for before: String - for after: String - {Echo.pair.cut(Some{(before, after)}) - == Echo.pair.named(Echo.wsp.trim(before), Echo.wsp.trim(after), - String.is_empty(Echo.wsp.trim(before))) : Maybe<&2, Qs.Kv()>} - -# LAW (§5.2 step 5): an empty name sets nothing -# HTTP-CK-1 -law ck_named_empty: - for name: String - for value: String - {Echo.pair.named(name, value, True{}) == None{} : Maybe<&2, Qs.Kv()>} - -# LAW: any other name sets the cookie -# HTTP-CK-1 -law ck_named_kept: - for name: String - for value: String - {Echo.pair.named(name, value, False{}) == Some{(name, value)} - : Maybe<&2, Qs.Kv()>} - -# LAW: trimming strips leading WSP, then (reversed) trailing WSP -# HTTP-CK-1 -law ck_trim: - for s: String - {Echo.wsp.trim(s) - == String.reverse(Echo.wsp.lead(String.reverse(Echo.wsp.lead(s)))) - : String} - -# LAW: the empty string has no WSP to strip -# HTTP-CK-1 -law ck_lead_nil: - for s: String - for e: {"" == s : String} - {Echo.wsp.lead(s) == "" : String} - -# LAW: a leading WSP character is dropped -# HTTP-CK-1 -law ck_lead_wsp: - for +h: Char - for +t: String - for e: {True{} == Echo.wsp(h) : Bool} - {Echo.wsp.lead(SCon{h, t}) == Echo.wsp.lead(t) : String} - -# LAW: stripping stops at the first other character -# HTTP-CK-1 -law ck_lead_other: - for +h: Char - for +t: String - for e: {False{} == Echo.wsp(h) : Bool} - {Echo.wsp.lead(SCon{h, t}) == SCon{h, t} : String} - -# LAW (RFC 5234 WSP): whitespace is SP or HTAB -# HTTP-CK-1 -law ck_wsp: - for +c: Char - {Echo.wsp(c) == Bool.or(Char.is_eq(c, ' '), Char.is_eq(c, '\t')) : Bool} - -# LAW: the header sent back is the store after the values, written out -# HTTP-CK-2 -law ck_echo: - for lines: List<&2, String> - {Echo.echo(lines) == Echo.line(Echo.store(lines, [])) : String} - -# LAW: with no more values, the store is as it is -# HTTP-CK-2 -law ck_store_nil: - for jar: List<&2, Qs.Kv()> - {Echo.store([], jar) == jar : List<&2, Qs.Kv()>} - -# LAW: values are stored in the order they came -# HTTP-CK-2 -law ck_store_cons: - for l: String - for t: List<&2, String> - for jar: List<&2, Qs.Kv()> - {Echo.store(l <> t, jar) == Echo.store(t, Echo.add(jar, Echo.pair(l))) - : List<&2, Qs.Kv()>} - -# LAW: a value that sets nothing leaves the store -# HTTP-CK-2 -law ck_add_none: - for jar: List<&2, Qs.Kv()> - {Echo.add(jar, None{}) == jar : List<&2, Qs.Kv()>} - -# LAW: a cookie is put in the store -# HTTP-CK-2 -law ck_add_some: - for jar: List<&2, Qs.Kv()> - for name: String - for value: String - {Echo.add(jar, Some{(name, value)}) == Echo.put(jar, name, value) - : List<&2, Qs.Kv()>} - -# LAW (§5.3): a cookie no stored one is named like goes last -# HTTP-CK-2 -law ck_put_nil: - for +name: String - for +value: String - {Echo.put([], name, value) == [(name, value)] : List<&2, Qs.Kv()>} - -# LAW (§5.3 step 11): a stored cookie of the same name takes the new value -# in its place -# HTTP-CK-2 -law ck_put_hit: - for +key: String - for old: String - for +t: List<&2, Qs.Kv()> - for +name: String - for +value: String - for e: {True{} == String.eq(key, name) : Bool} - {Echo.put((key, old) <> t, name, value) == (key, value) <> t - : List<&2, Qs.Kv()>} - -# LAW: any other stored cookie stays, and the rest of the store is looked at -# HTTP-CK-2 -law ck_put_miss: - for +key: String - for old: String - for +t: List<&2, Qs.Kv()> - for +name: String - for +value: String - for e: {False{} == String.eq(key, name) : Bool} - {Echo.put((key, old) <> t, name, value) == (key, old) <> Echo.put(t, name, - value) : List<&2, Qs.Kv()>} - -# LAW (§5.4): an empty store sends the empty value -# HTTP-CK-2 -law ck_line_nil: - for jar: List<&2, Qs.Kv()> - for e: {[] == jar : List<&2, Qs.Kv()>} - {Echo.line(jar) == "" : String} - -# LAW (§5.4 step 4): the first cookie, then the others -# HTTP-CK-2 -law ck_line_cons: - for name: String - for value: String - for t: List<&2, Qs.Kv()> - {Echo.line((name, value) <> t) == Echo.one(name, value) ++ Echo.more(t) - : String} - -# LAW: after the last cookie, nothing -# HTTP-CK-2 -law ck_more_nil: - for jar: List<&2, Qs.Kv()> - for e: {[] == jar : List<&2, Qs.Kv()>} - {Echo.more(jar) == "" : String} - -# LAW (§5.4 step 4.2): each cookie after the first follows `; ` -# HTTP-CK-2 -law ck_more_cons: - for name: String - for value: String - for t: List<&2, Qs.Kv()> - {Echo.more((name, value) <> t) == "; " ++ Echo.one(name, value) - ++ Echo.more(t) : String} - -# LAW (§5.4 step 4.1): a cookie is its name, `=` and its value -# HTTP-CK-2 -law ck_one: - for name: String - for value: String - {Echo.one(name, value) == name ++ "=" ++ value : String} - -# LAW: no header fields, no Set-Cookie values -# HTTP-CK-3 -law ck_values_nil: - for hs: List<&2, Http.Header> - for e: {[] == hs : List<&2, Http.Header>} - {Echo.values(hs) == [] : List<&2, String>} - -# LAW (RFC 9110 §5.1): a field named Set-Cookie in any case gives its value, -# in order -# HTTP-CK-3 -law ck_values_hit: - for +name: String - for value: String - for t: List<&2, Http.Header> - for e: {True{} == String.eq(String.to_lower(name), "set-cookie") : Bool} - {Echo.values(Http.H{name, value} <> t) == value <> Echo.values(t) - : List<&2, String>} - -# LAW: any other field gives nothing -# HTTP-CK-3 -law ck_values_miss: - for +name: String - for value: String - for t: List<&2, Http.Header> - for e: {False{} == String.eq(String.to_lower(name), "set-cookie") : Bool} - {Echo.values(Http.H{name, value} <> t) == Echo.values(t) - : List<&2, String>} - -# LAW: unescaping undoes html_escape: for every string, html_unescape of its -# escape is the string -# HTTP-REF-4 -law ref_round: - for s: String - {Ref.unescape(Html.escape(s)) == s : String} - -# --- bodies as octets (SPEC.md HTTP-BODY) --- - -# LAW: a received text's octets are its characters, one each -# HTTP-BODY-1 -law octs_len: - for s: String - {Http.octs.len(s) == String.length(s) : Nat} - -# LAW: the first n octets of a received text are its first n characters -# HTTP-BODY-1 -law octs_take: - for s: String - for +n: Nat - {Http.octs.take(s, n) == String.take(s, n) : String} - -# LAW: what follows the first n octets is what follows its first n -# characters -# HTTP-BODY-1 -law octs_drop: - for s: String - for +n: Nat - {Http.octs.drop(s, n) == String.drop(s, n) : String} - -# LAW (RFC 9112 §6.3 item 5): a body framed by a Content-Length of exactly -# its octets is those octets, whatever follows them -# HTTP-BODY-2 -law clen_prefix: - for st: U32 - for hs: List<&2, Http.Header> - for +b: String - for +r: String - {Http.parse.clen(st, hs, String.length(b), String.append(b, r)) - == Http.Reply{st, hs, b} - : Http.Reply} - -# LAW (RFC 9112 §7.1): a chunk of n octets other than 0 is its data, then -# the chunks after its CRLF -# HTTP-BODY-3 -law chunk_data: - for f: Nat - for +h: String - for +d: String - for +r: String - for e: {False{} == Nat.is_eq(Http.hexlen(h), 0n) : Bool} - for el: {String.length(d) == Http.hexlen(h) : Nat} - {Http.chunks(1n+f, Http.Cut{h, String.append(d, "\r\n" ++ r)}) - == String.append(d, Http.chunks(f, Http.divide(r, "\r\n"))) : String} - -# LAW (RFC 9112 §7.1): a chunk of size 0 ends the body -# HTTP-BODY-3 -law chunk_last: - for f: Nat - for +h: String - for after: String - for e: {True{} == Nat.is_eq(Http.hexlen(h), 0n) : Bool} - {Http.chunks(1n+f, Http.Cut{h, after}) == "" : String} - -# LAW: received octets as text are their UTF-8 decoding, one octet a byte -# HTTP-BODY-4 -law octets_text: - for o: String - {Qs.octets.text(o) == Qs.utf.dec(Qs.octets.toks(o), Qs.Idle{}) : String} - -# LAW: no octets are no pieces to decode -# HTTP-BODY-4 -law octets_toks_nil: - for s: String - for e: {SNil{} == s : String} - {Qs.octets.toks(s) == [] : List<&2, Qs.Tok>} - -# LAW: each octet is one byte for the decoder, in order -# HTTP-BODY-4 -law octets_toks_cons: - for +c: Char - for t: String - {Qs.octets.toks(SCon{c, t}) == Qs.B{Char.to_u32(c)} <> Qs.octets.toks(t) - : List<&2, Qs.Tok>} - -# LAW: header fields as text are each name and value decoded, in order -# HTTP-BODY-4 -law octets_headers_cons: - for name: String - for value: String - for t: List<&2, Http.Header> - {Qs.octets.headers(Http.H{name, value} <> t) - == Http.H{Qs.octets.text(name), Qs.octets.text(value)} - <> Qs.octets.headers(t) : List<&2, Http.Header>} - -# LAW: no header fields are no header fields -# HTTP-BODY-4 -law octets_headers_nil: - for hs: List<&2, Http.Header> - for e: {[] == hs : List<&2, Http.Header>} - {Qs.octets.headers(hs) == [] : List<&2, Http.Header>} - -# LAW: a request is handed on as text, each part decoded -# HTTP-SRV-2 -# HTTP-SRV-9 -law step_text: - for method: String - for target: String - for hs: List<&2, Http.Header> - for body: String - {Step.step.text(Http.Request{method, target, hs, body}) - == Http.Request{Qs.octets.text(method), Qs.octets.text(target), - Qs.octets.headers(hs), Qs.octets.text(body)} : Http.Request} - -# LAW: a bad request keeps its reason -# HTTP-SRV-1 -law step_text_bad: - for why: String - {Step.step.text(Http.Bad{why}) == Http.Bad{why} : Http.Request} - -# --- the concurrent server's replies and accepts (SPEC.md HTTP-SRV-4, 6 to 9) --- - -# LAW: an accept whose deadline passed (ETIMEDOUT) is not a lost connection -# HTTP-SRV-4 -law accept_idle: - for -A: Type - for +code: U32 - for e: {True{} == U32.is_eq(code, 110) : Bool} - for why: String - {Step.step.accepted(A, Fail{(code, why)}) == Step.Idle{} : Step.Accept} - -# LAW: any other failed accept is lost with the line `ezhttp: accept: ` -# HTTP-SRV-4 -law accept_missed: - for -A: Type - for +code: U32 - for e: {False{} == U32.is_eq(code, 110) : Bool} - for why: String - {Step.step.accepted(A, Fail{(code, why)}) - == Step.Missed{"ezhttp: accept: " ++ why} : Step.Accept} - -# LAW: an accept that brought a connection hands it on unchanged -# HTTP-SRV-4 -law accept_took: - for -A: Type - for value: A - {Step.step.accepted(A, Done{value}) == Step.Took{value} : Step.Accept} - -# LAW (RFC 9112 §4, §6.3): the head of a reply of n octets -# HTTP-SRV-6 -law respond_head: - for +status: U32 - for hs: List<&2, Http.Header> - for +n: Nat - {Http.respond.head(status, hs, n) - == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" - ++ "Connection: close\r\n" - ++ Http.headers.lines(hs) - ++ Http.clen.line(n, Nat.is_eq(n, 0n)) - ++ "\r\n" ++ "" : String} - -# LAW: a text reply is the head of its UTF-8 octets onto the body -# HTTP-SRV-6 -law respond_text_is: - for +status: U32 - for hs: List<&2, Http.Header> - for +body: String - {Http.respond(status, hs, body) - == Http.respond.head.onto(status, hs, Http.utf8.len(body), body) : String} - -# LAW: the head written onto what follows it is the head, then that -# HTTP-SRV-6 -law respond_head_onto: - for +status: U32 - for hs: List<&2, Http.Header> - for +n: Nat - for rest: String - {Http.respond.head.onto(status, hs, n, rest) - == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" - ++ "Connection: close\r\n" - ++ Http.headers.lines(hs) - ++ Http.clen.line(n, Nat.is_eq(n, 0n)) - ++ "\r\n" ++ rest : String} - -# LAW (RFC 9112 §7.1): a streamed reply with a body is chunked -# HTTP-SRV-7 -law stream_head: - for +status: U32 - for e: {False{} == Http.status.nocontent(status) : Bool} - for hs: List<&2, Http.Header> - {Http.respond.stream(status, hs) - == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" - ++ "Connection: close\r\n" ++ Http.headers.lines(hs) - ++ "Transfer-Encoding: chunked\r\n" ++ "\r\n" : String} - -# LAW (RFC 9112 §6.1): a streamed 204 or 304 sends no Transfer-Encoding -# HTTP-SRV-7 -law stream_head_bare: - for +status: U32 - for e: {True{} == Http.status.nocontent(status) : Bool} - for hs: List<&2, Http.Header> - {Http.respond.stream(status, hs) - == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" - ++ "Connection: close\r\n" ++ Http.headers.lines(hs) ++ "" - ++ "\r\n" : String} - -# LAW: a chunk's size line is its 8 hex digits, high first, then CRLF -# HTTP-SRV-7 -law chunk_line: - for +n: U32 - {Http.chunk.line(n) - == SCon{Http.hex.at(n, 28n), SCon{Http.hex.at(n, 24n), - SCon{Http.hex.at(n, 20n), SCon{Http.hex.at(n, 16n), - SCon{Http.hex.at(n, 12n), SCon{Http.hex.at(n, 8n), - SCon{Http.hex.at(n, 4n), SCon{Http.hex.at(n, 0n), "\r\n"}}}}}}}} - : String} - -# LAW: a hex digit is the 4 bits at its shift, 0-9 then lowercase a-f -# HTTP-SRV-7 -law hex_at: - for +n: U32 - for k: Nat - {Http.hex.at(n, k) == Http.hex.nib(U32.and(U32.shrn(n, k), 15)) : Char} - -# LAW: a value below 10 is its decimal digit, any other `a` onwards -# HTTP-SRV-7 -law hex_nib: - for +v: U32 - {Http.hex.nib(v) == Char.from_u32(Bool.pick(U32, U32.is_lt(v, 10), - U32.add(v, 48), U32.add(v, 87))) : Char} - -# LAW: an empty chunk is not written -# HTTP-SRV-7 -law chunk_empty: - for +n: U32 - for e: {True{} == U32.is_eq(n, 0) : Bool} - {Http.chunk.frame(n) == Http.NoChunk{} : Http.Chunk} - -# LAW: any other chunk is written after its size line -# HTTP-SRV-7 -law chunk_some: - for +n: U32 - for e: {False{} == U32.is_eq(n, 0) : Bool} - {Http.chunk.frame(n) == Http.Chunk{Http.chunk.line(n)} : Http.Chunk} - -# LAW (RFC 9112 §7.1): the body ends with the last chunk and no trailer -# HTTP-SRV-7 -law chunk_end: - for s: String - for e: {"" == s : String} - {Http.chunk.end() ++ s == "0\r\n\r\n" : String} - -# LAW (RFC 9110 §9.3.2): a reply to HEAD is written bare -# HTTP-SRV-8 -law bare_head: - for +status: U32 - {Step.step.bare("HEAD", status) == True{} : Bool} - -# LAW: a reply to any other method is bare exactly when its status is 204 -# or 304 -# HTTP-SRV-8 -law bare_other: - for +method: String - for e: {False{} == String.eq(method, "HEAD") : Bool} - for +status: U32 - {Step.step.bare(method, status) == Http.status.nocontent(status) : Bool} - -# LAW: a 204 or 304 is exactly what nocontent names -# HTTP-SRV-8 -law nocontent_is: - for +code: U32 - {Http.status.nocontent(code) - == Bool.or(U32.is_eq(code, 204), U32.is_eq(code, 304)) : Bool} - -# LAW: the handler gets the body octets of the request as they arrived -# HTTP-SRV-9 -law step_octets: - for method: String - for target: String - for hs: List<&2, Http.Header> - for body: String - {Step.step.octets(Http.Request{method, target, hs, body}) == body : String} +import ./laws/AbsoluteFormTargetRejected.bend as AbsoluteFormTargetRejected +import ./laws/AbsoluteLocationsResolve.bend as AbsoluteLocationsResolve +import ./laws/BadRequestAnswers400.bend as BadRequestAnswers400 +import ./laws/BadRequestShowIsMarked.bend as BadRequestShowIsMarked +import ./laws/BareRepliesWriteHeadOnly.bend as BareRepliesWriteHeadOnly +import ./laws/Base64EmptyIsEmpty.bend as Base64EmptyIsEmpty +import ./laws/Base64EncodesF.bend as Base64EncodesF +import ./laws/Base64EncodesFo.bend as Base64EncodesFo +import ./laws/Base64EncodesFoo.bend as Base64EncodesFoo +import ./laws/Base64EncodesUserPass.bend as Base64EncodesUserPass +import ./laws/Base64PadsFinalQuantum.bend as Base64PadsFinalQuantum +import ./laws/BasicIsBase64OfUserPassword.bend as BasicIsBase64OfUserPassword +import ./laws/BearerIsAuthorization.bend as BearerIsAuthorization +import ./laws/BearerTokenAcceptsPlain.bend as BearerTokenAcceptsPlain +import ./laws/BearerTokenRejectsNewline.bend as BearerTokenRejectsNewline +import ./laws/BearerTokenRejectsSpace.bend as BearerTokenRejectsSpace +import ./laws/BearerTokenRejectsTab.bend as BearerTokenRejectsTab +import ./laws/CacheDirectiveNamesIgnoreCase.bend as CacheDirectiveNamesIgnoreCase +import ./laws/CacheIgnoresUnknownDirective.bend as CacheIgnoresUnknownDirective +import ./laws/CacheMaxAgeIgnoresJunk.bend as CacheMaxAgeIgnoresJunk +import ./laws/CacheReadsKnownDirectives.bend as CacheReadsKnownDirectives +import ./laws/CacheRecordsExpires.bend as CacheRecordsExpires +import ./laws/CacheRecordsMaxAge.bend as CacheRecordsMaxAge +import ./laws/CacheRecordsNoCache.bend as CacheRecordsNoCache +import ./laws/CacheRecordsPrivate.bend as CacheRecordsPrivate +import ./laws/ChunkDataUnchanged.bend as ChunkDataUnchanged +import ./laws/ChunkSizeHexIgnoresCase.bend as ChunkSizeHexIgnoresCase +import ./laws/ChunkSizeIgnoresExtension.bend as ChunkSizeIgnoresExtension +import ./laws/ChunkSizeReadsHexSkipsExtension.bend as ChunkSizeReadsHexSkipsExtension +import ./laws/ChunkedBodyJoinsChunks.bend as ChunkedBodyJoinsChunks +import ./laws/ChunkedNameIgnoresCase.bend as ChunkedNameIgnoresCase +import ./laws/ChunksStopWithoutFuel.bend as ChunksStopWithoutFuel +import ./laws/ContentLengthFramesBody.bend as ContentLengthFramesBody +import ./laws/ContentLengthReadsDecimal.bend as ContentLengthReadsDecimal +import ./laws/ContentLengthRejectsJunk.bend as ContentLengthRejectsJunk +import ./laws/ContentLengthTakesPrefix.bend as ContentLengthTakesPrefix +import ./laws/ContinuationLineRejected.bend as ContinuationLineRejected +import ./laws/CookieAliveUnderMaxAge.bend as CookieAliveUnderMaxAge +import ./laws/CookieAttributesJoinWithSemicolons.bend as CookieAttributesJoinWithSemicolons +import ./laws/CookieDefaultPathFallsBackToRoot.bend as CookieDefaultPathFallsBackToRoot +import ./laws/CookieDefaultPathIsDirectory.bend as CookieDefaultPathIsDirectory +import ./laws/CookieDefaultPathNeedsTwoSlashes.bend as CookieDefaultPathNeedsTwoSlashes +import ./laws/CookieDefaultPathOmitsTrailingSlash.bend as CookieDefaultPathOmitsTrailingSlash +import ./laws/CookieDomainLowercased.bend as CookieDomainLowercased +import ./laws/CookieDomainMatchesDotSuffix.bend as CookieDomainMatchesDotSuffix +import ./laws/CookieDomainNeedsDotBeforeSuffix.bend as CookieDomainNeedsDotBeforeSuffix +import ./laws/CookieHeaderEchoesStore.bend as CookieHeaderEchoesStore +import ./laws/CookieHostOnlyNotSentElsewhere.bend as CookieHostOnlyNotSentElsewhere +import ./laws/CookieHostOnlySentToHost.bend as CookieHostOnlySentToHost +import ./laws/CookieHostOnlyStoresHost.bend as CookieHostOnlyStoresHost +import ./laws/CookieIdenticalPathsMatch.bend as CookieIdenticalPathsMatch +import ./laws/CookieInsecureSentOnCleartext.bend as CookieInsecureSentOnCleartext +import ./laws/CookieIpHostMatchesExactly.bend as CookieIpHostMatchesExactly +import ./laws/CookieIpHostNoSuffixMatch.bend as CookieIpHostNoSuffixMatch +import ./laws/CookieLongerPathFails.bend as CookieLongerPathFails +import ./laws/CookieMaxAgeIgnoresJunk.bend as CookieMaxAgeIgnoresJunk +import ./laws/CookieMaxAgeOneStaleAtOne.bend as CookieMaxAgeOneStaleAtOne +import ./laws/CookieMaxAgeSetsAge.bend as CookieMaxAgeSetsAge +import ./laws/CookieMaxAgeZeroNotAlive.bend as CookieMaxAgeZeroNotAlive +import ./laws/CookieNameIsNotIpv4.bend as CookieNameIsNotIpv4 +import ./laws/CookieNoDomainNotSentElsewhere.bend as CookieNoDomainNotSentElsewhere +import ./laws/CookiePathMatchesSlashPrefix.bend as CookiePathMatchesSlashPrefix +import ./laws/CookiePathNeedsSegmentBoundary.bend as CookiePathNeedsSegmentBoundary +import ./laws/CookieRecognizesIpv4Literal.bend as CookieRecognizesIpv4Literal +import ./laws/CookieRejectsEmptyName.bend as CookieRejectsEmptyName +import ./laws/CookieRequestJoinsPairs.bend as CookieRequestJoinsPairs +import ./laws/CookieRootPathMatchesLonger.bend as CookieRootPathMatchesLonger +import ./laws/CookieSameSiteFoldsCase.bend as CookieSameSiteFoldsCase +import ./laws/CookieSameSiteNoneCanonical.bend as CookieSameSiteNoneCanonical +import ./laws/CookieSecureNeedsTls.bend as CookieSecureNeedsTls +import ./laws/CookieSecureSentOnTls.bend as CookieSecureSentOnTls +import ./laws/CookieSessionStaysAlive.bend as CookieSessionStaysAlive +import ./laws/CookieSlashEndedPathIsPrefix.bend as CookieSlashEndedPathIsPrefix +import ./laws/CookieUnknownSameSiteDropped.bend as CookieUnknownSameSiteDropped +import ./laws/CorsAllowListReflectsOrigin.bend as CorsAllowListReflectsOrigin +import ./laws/CorsAllowedPreflightIs200.bend as CorsAllowedPreflightIs200 +import ./laws/CorsBadPreflightIs400.bend as CorsBadPreflightIs400 +import ./laws/CorsCredentialsKeepExplicitOrigin.bend as CorsCredentialsKeepExplicitOrigin +import ./laws/CorsCredentialsReflectOrigin.bend as CorsCredentialsReflectOrigin +import ./laws/CorsGetIsNotPreflight.bend as CorsGetIsNotPreflight +import ./laws/CorsNoVaryForStar.bend as CorsNoVaryForStar +import ./laws/CorsNoVaryWhenRefused.bend as CorsNoVaryWhenRefused +import ./laws/CorsOptionsWithMethodIsPreflight.bend as CorsOptionsWithMethodIsPreflight +import ./laws/CorsPreflightAnswersStar.bend as CorsPreflightAnswersStar +import ./laws/CorsPreflightHeaderDenied.bend as CorsPreflightHeaderDenied +import ./laws/CorsPreflightHeadersIgnoreCase.bend as CorsPreflightHeadersIgnoreCase +import ./laws/CorsPreflightMethodDenied.bend as CorsPreflightMethodDenied +import ./laws/CorsPreflightOriginDenied.bend as CorsPreflightOriginDenied +import ./laws/CorsReadsOriginHeader.bend as CorsReadsOriginHeader +import ./laws/CorsReadsRequestHeaders.bend as CorsReadsRequestHeaders +import ./laws/CorsReadsRequestMethod.bend as CorsReadsRequestMethod +import ./laws/CorsRefusedLeavesReply.bend as CorsRefusedLeavesReply +import ./laws/CorsRefusesMissingOrigin.bend as CorsRefusesMissingOrigin +import ./laws/CorsSimpleAnswersStar.bend as CorsSimpleAnswersStar +import ./laws/CorsSimpleKeepsBody.bend as CorsSimpleKeepsBody +import ./laws/CorsStarStaysStar.bend as CorsStarStaysStar +import ./laws/CorsVaryOnSpecificOrigin.bend as CorsVaryOnSpecificOrigin +import ./laws/CutAtFirstEquals.bend as CutAtFirstEquals +import ./laws/DeadlineIsTimeout.bend as DeadlineIsTimeout +import ./laws/DechunkEmptyIsEmpty.bend as DechunkEmptyIsEmpty +import ./laws/DechunkIgnoresExtension.bend as DechunkIgnoresExtension +import ./laws/DechunkJoinsTwoChunks.bend as DechunkJoinsTwoChunks +import ./laws/DechunkLastChunkAloneIsEmpty.bend as DechunkLastChunkAloneIsEmpty +import ./laws/DechunkRfcExample.bend as DechunkRfcExample +import ./laws/DechunkStopsAtZero.bend as DechunkStopsAtZero +import ./laws/DeleteIsIdempotent.bend as DeleteIsIdempotent +import ./laws/DeleteNotSafe.bend as DeleteNotSafe +import ./laws/DeleteRequestLine.bend as DeleteRequestLine +import ./laws/DotSegmentsRemoved.bend as DotSegmentsRemoved +import ./laws/EqualContentLengthsAreOne.bend as EqualContentLengthsAreOne +import ./laws/ErrnoNamesAreLinux.bend as ErrnoNamesAreLinux +import ./laws/ExactContentLengthBodyKept.bend as ExactContentLengthBodyKept +import ./laws/ExchangeCarriesMethod.bend as ExchangeCarriesMethod +import ./laws/FailedEffectsLoseConnection.bend as FailedEffectsLoseConnection +import ./laws/FailuresReportedByPhase.bend as FailuresReportedByPhase +import ./laws/FieldLookupFindsFirst.bend as FieldLookupFindsFirst +import ./laws/FieldLookupIgnoresCase.bend as FieldLookupIgnoresCase +import ./laws/FieldNamesIgnoreCase.bend as FieldNamesIgnoreCase +import ./laws/FieldValueTrimsWhitespace.bend as FieldValueTrimsWhitespace +import ./laws/FirstIsFirstWord.bend as FirstIsFirstWord +import ./laws/FormBodyLayout.bend as FormBodyLayout +import ./laws/FormBoundaryRules.bend as FormBoundaryRules +import ./laws/FormNamesEscaped.bend as FormNamesEscaped +import ./laws/FormPartHeaders.bend as FormPartHeaders +import ./laws/FormRefusesBadRequests.bend as FormRefusesBadRequests +import ./laws/FreshUnderMaxAge.bend as FreshUnderMaxAge +import ./laws/GetAsteriskTargetRejected.bend as GetAsteriskTargetRejected +import ./laws/GetIsIdempotent.bend as GetIsIdempotent +import ./laws/GetIsSafe.bend as GetIsSafe +import ./laws/GetOmitsContentLength.bend as GetOmitsContentLength +import ./laws/GetRequestLine.bend as GetRequestLine +import ./laws/HandlerGetsRequestText.bend as HandlerGetsRequestText +import ./laws/HeadAndNoContentHaveNoBody.bend as HeadAndNoContentHaveNoBody +import ./laws/HeadIsIdempotent.bend as HeadIsIdempotent +import ./laws/HeadIsSafe.bend as HeadIsSafe +import ./laws/HeadPlannedBeforeBody.bend as HeadPlannedBeforeBody +import ./laws/HeadReplyDropsBody.bend as HeadReplyDropsBody +import ./laws/HeadRequestLine.bend as HeadRequestLine +import ./laws/HtmlEscapeCharByChar.bend as HtmlEscapeCharByChar +import ./laws/HtmlEscapeIsSafe.bend as HtmlEscapeIsSafe +import ./laws/HtmlEscapesFiveChars.bend as HtmlEscapesFiveChars +import ./laws/HtmlKeepsOtherChars.bend as HtmlKeepsOtherChars +import ./laws/HttpsDefaultsTo443.bend as HttpsDefaultsTo443 +import ./laws/InterimResponsesSkipped.bend as InterimResponsesSkipped +import ./laws/InterimStatusIs1xx.bend as InterimStatusIs1xx +import ./laws/JoinedPiecesAreText.bend as JoinedPiecesAreText +import ./laws/JsonRejectsNonJson.bend as JsonRejectsNonJson +import ./laws/JsonStringIsTextBody.bend as JsonStringIsTextBody +import ./laws/JsonStringParsesBack.bend as JsonStringParsesBack +import ./laws/LocationResolvedBySort.bend as LocationResolvedBySort +import ./laws/LocationSplitsAtQuery.bend as LocationSplitsAtQuery +import ./laws/LowercaseGetNotSafe.bend as LowercaseGetNotSafe +import ./laws/MaxAgeOneFreshThenStale.bend as MaxAgeOneFreshThenStale +import ./laws/MaxAgeOverridesExpires.bend as MaxAgeOverridesExpires +import ./laws/MaxAgeZeroIsStale.bend as MaxAgeZeroIsStale +import ./laws/MethodTokensAreCaseSensitive.bend as MethodTokensAreCaseSensitive +import ./laws/MissingFieldIsEmpty.bend as MissingFieldIsEmpty +import ./laws/NamedReferencesLongestMatch.bend as NamedReferencesLongestMatch +import ./laws/NoCacheNeverFresh.bend as NoCacheNeverFresh +import ./laws/NoCacheNotFresh.bend as NoCacheNotFresh +import ./laws/NoMaxAgeNotFresh.bend as NoMaxAgeNotFresh +import ./laws/NoStoreNeverFresh.bend as NoStoreNeverFresh +import ./laws/NoStoreNotFresh.bend as NoStoreNotFresh +import ./laws/NonHeadReplyKeepsBody.bend as NonHeadReplyKeepsBody +import ./laws/NonHexChunkSizeTorn.bend as NonHexChunkSizeTorn +import ./laws/NumericReferencesDecode.bend as NumericReferencesDecode +import ./laws/ObsFoldRejected.bend as ObsFoldRejected +import ./laws/OctetBodiesSentUnchanged.bend as OctetBodiesSentUnchanged +import ./laws/OctetsDecodeAsUtf8.bend as OctetsDecodeAsUtf8 +import ./laws/OneOctetPerChar.bend as OneOctetPerChar +import ./laws/OnlyGetFollows.bend as OnlyGetFollows +import ./laws/OptionsAsteriskNamesServer.bend as OptionsAsteriskNamesServer +import ./laws/OptionsIsIdempotent.bend as OptionsIsIdempotent +import ./laws/OptionsIsSafe.bend as OptionsIsSafe +import ./laws/OptionsRequestLine.bend as OptionsRequestLine +import ./laws/OriginFormKeepsQuery.bend as OriginFormKeepsQuery +import ./laws/OtherSchemesDefaultTo80.bend as OtherSchemesDefaultTo80 +import ./laws/OtherStatusesStopChain.bend as OtherStatusesStopChain +import ./laws/ParseQuerySplitsPairs.bend as ParseQuerySplitsPairs +import ./laws/PatchNotIdempotent.bend as PatchNotIdempotent +import ./laws/PathFormKeepsSlash.bend as PathFormKeepsSlash +import ./laws/PhraseTableLookup.bend as PhraseTableLookup +import ./laws/PostNotIdempotent.bend as PostNotIdempotent +import ./laws/PostNotSafe.bend as PostNotSafe +import ./laws/PostRequestLine.bend as PostRequestLine +import ./laws/PostWritesContentLength.bend as PostWritesContentLength +import ./laws/PutIsIdempotent.bend as PutIsIdempotent +import ./laws/PutNotSafe.bend as PutNotSafe +import ./laws/PutRequestLine.bend as PutRequestLine +import ./laws/QueryGetLastWins.bend as QueryGetLastWins +import ./laws/QuoteEscapesUtf8Bytes.bend as QuoteEscapesUtf8Bytes +import ./laws/QuoteKeepsUnreserved.bend as QuoteKeepsUnreserved +import ./laws/QuoteOutputIsFramed.bend as QuoteOutputIsFramed +import ./laws/QuotePlusWritesSpaces.bend as QuotePlusWritesSpaces +import ./laws/ReadStateMatchesWholeText.bend as ReadStateMatchesWholeText +import ./laws/ReadsJudgedByCount.bend as ReadsJudgedByCount +import ./laws/Reason200IsOk.bend as Reason200IsOk +import ./laws/Reason201IsCreated.bend as Reason201IsCreated +import ./laws/Reason204IsNoContent.bend as Reason204IsNoContent +import ./laws/Reason400IsBadRequest.bend as Reason400IsBadRequest +import ./laws/Reason404IsNotFound.bend as Reason404IsNotFound +import ./laws/Reason405IsMethodNotAllowed.bend as Reason405IsMethodNotAllowed +import ./laws/Reason500IsInternalServerError.bend as Reason500IsInternalServerError +import ./laws/ReasonPhraseFromFinalHead.bend as ReasonPhraseFromFinalHead +import ./laws/ReasonPhrasesFollowRfc.bend as ReasonPhrasesFollowRfc +import ./laws/RedirectsFollowLocation.bend as RedirectsFollowLocation +import ./laws/RelativeLocationsMerge.bend as RelativeLocationsMerge +import ./laws/RelativeTargetRejected.bend as RelativeTargetRejected +import ./laws/ReplyHeadFramesBody.bend as ReplyHeadFramesBody +import ./laws/RequestBodyOfContentLengthKept.bend as RequestBodyOfContentLengthKept +import ./laws/RequestDisagreeingContentLengthsRejected.bend as RequestDisagreeingContentLengthsRejected +import ./laws/RequestEqualContentLengthsFrameOneBody.bend as RequestEqualContentLengthsFrameOneBody +import ./laws/RequestHeaderFieldsOrdered.bend as RequestHeaderFieldsOrdered +import ./laws/RequestKeepsFieldNameSpelling.bend as RequestKeepsFieldNameSpelling +import ./laws/RequestLineAndHost.bend as RequestLineAndHost +import ./laws/RequestLineOpensWithMethod.bend as RequestLineOpensWithMethod +import ./laws/RequestNeedsBlankLine.bend as RequestNeedsBlankLine +import ./laws/RequestNeedsRequestLine.bend as RequestNeedsRequestLine +import ./laws/RequestWithBothFramingsRejected.bend as RequestWithBothFramingsRejected +import ./laws/Respond200KeepsBody.bend as Respond200KeepsBody +import ./laws/Respond204DropsBody.bend as Respond204DropsBody +import ./laws/Respond304DropsBody.bend as Respond304DropsBody +import ./laws/RespondOpensWithVersion.bend as RespondOpensWithVersion +import ./laws/ResponseCompleteByFraming.bend as ResponseCompleteByFraming +import ./laws/ResponseDisagreeingContentLengthsTorn.bend as ResponseDisagreeingContentLengthsTorn +import ./laws/ResponseNeedsBlankLine.bend as ResponseNeedsBlankLine +import ./laws/ResponseNeedsStatus.bend as ResponseNeedsStatus +import ./laws/ResponseWithBothFramingsTorn.bend as ResponseWithBothFramingsTorn +import ./laws/SchemeBeforeFirstColon.bend as SchemeBeforeFirstColon +import ./laws/SecondIsNextWord.bend as SecondIsNextWord +import ./laws/SetCookieFieldsSelected.bend as SetCookieFieldsSelected +import ./laws/SetCookieParsesBack.bend as SetCookieParsesBack +import ./laws/SetCookieReadsPair.bend as SetCookieReadsPair +import ./laws/ShortRequestBodyIsBad.bend as ShortRequestBodyIsBad +import ./laws/ShowReplyOpensWithStatus.bend as ShowReplyOpensWithStatus +import ./laws/ShowTornIsMarked.bend as ShowTornIsMarked +import ./laws/SplitAtCharacter.bend as SplitAtCharacter +import ./laws/StaleAtMaxAge.bend as StaleAtMaxAge +import ./laws/StreamedRepliesAreChunked.bend as StreamedRepliesAreChunked +import ./laws/TakeDropSplitAtOneOffset.bend as TakeDropSplitAtOneOffset +import ./laws/TimeoutZeroIsOneMs.bend as TimeoutZeroIsOneMs +import ./laws/TimeoutsReadAsTimedOut.bend as TimeoutsReadAsTimedOut +import ./laws/UnescapeFindsReferences.bend as UnescapeFindsReferences +import ./laws/UnescapeUndoesEscape.bend as UnescapeUndoesEscape +import ./laws/UnframedBodyIsRest.bend as UnframedBodyIsRest +import ./laws/UnknownStatusHasEmptyReason.bend as UnknownStatusHasEmptyReason +import ./laws/UnquotePlusReadsSpaces.bend as UnquotePlusReadsSpaces +import ./laws/UnquoteReadsEscapes.bend as UnquoteReadsEscapes +import ./laws/UrlBadPortUsesDefault.bend as UrlBadPortUsesDefault +import ./laws/UrlEmptyPath.bend as UrlEmptyPath +import ./laws/UrlFragmentHidesQuery.bend as UrlFragmentHidesQuery +import ./laws/UrlHttpDefaultPort.bend as UrlHttpDefaultPort +import ./laws/UrlHttpsDefaultPort.bend as UrlHttpsDefaultPort +import ./laws/UrlHttpsExplicitPort.bend as UrlHttpsExplicitPort +import ./laws/UrlIpv4Host.bend as UrlIpv4Host +import ./laws/UrlIpv4Port.bend as UrlIpv4Port +import ./laws/UrlNeedsSlashes.bend as UrlNeedsSlashes +import ./laws/UrlPathIsAbsolute.bend as UrlPathIsAbsolute +import ./laws/UrlQueryDropsFragment.bend as UrlQueryDropsFragment +import ./laws/UrlQueryKeepsQuestion.bend as UrlQueryKeepsQuestion +import ./laws/UrlQueryOnEmptyPath.bend as UrlQueryOnEmptyPath +import ./laws/UrlSchemeLowercased.bend as UrlSchemeLowercased +import ./laws/UrlShowBadIsMarked.bend as UrlShowBadIsMarked +import ./laws/UrlShowOpensWithScheme.bend as UrlShowOpensWithScheme +import ./laws/UrlTrailingSlash.bend as UrlTrailingSlash +import ./laws/UrlUserinfoLastAt.bend as UrlUserinfoLastAt +import ./laws/UrlUserinfoStripped.bend as UrlUserinfoStripped +import ./laws/UrlencodeJoinsPairs.bend as UrlencodeJoinsPairs +import ./laws/Utf8DecodesWithReplacement.bend as Utf8DecodesWithReplacement +import ./laws/WithServesGetOctetsAndContext.bend as WithServesGetOctetsAndContext diff --git a/PROOF.bend b/PROOF.bend index bcb31e6..89878e6 100644 --- a/PROOF.bend +++ b/PROOF.bend @@ -1,6496 +1,274 @@ -# ezhttp: the proofs. `bend PROOF.bend` is the gate. +# The proofs: proofs/.bend proves laws/.bend. The gate: +# `bend PROOF.bend` prints exactly `ALL PROOFS CHECK` as its first line. import Base -import ./src/http.bend as Http -import ./src/url.bend as Url -import ./src/body.bend as Body -import ./src/step.bend as Step -import ./src/pct.bend as Pct -import ./src/wirespec.bend as Spec -import ./src/html.bend as Html -import ./src/redirect.bend as Redirect -import ./src/form.bend as Form -import ./src/qs.bend as Qs -import ./src/unescape.bend as Ref -import ./src/entities.bend as Ents -import ./src/echo.bend as Echo -import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes import ./LAWS.bend as Laws - -# --- Base facts used by the framing proofs --- - -def eq.bool_cmp_self(b: Bool) -> {Bool.cmp(b, b) == EQ{} : Cmp}: - match b: - case False{}: - {==} - case True{}: - {==} - -def eq.word_cmp_self(n: Nat, w: Word(n)) -> {Word.cmp(n, w, w) == EQ{} : Cmp}: - match n: - case 0n: - match w: - case WNil{}: - {==} - case 1n+p: - match w: - case WCon{+b, +t}: - Equal.trans(Cmp, Word.cmp.fin(b, b, Word.cmp(p, t, t)), - Word.cmp.fin(b, b, EQ{}), EQ{}, - Equal.cong(Cmp, Cmp, c => Word.cmp.fin(b, b, c), - Word.cmp(p, t, t), EQ{}, eq.word_cmp_self(p, t)), - eq.bool_cmp_self(b)) - -def eq.u32_cmp_self(u: U32) -> {U32.cmp(u, u) == EQ{} : Cmp}: - match u: - case U32{+w}: - eq.word_cmp_self(32n, w) - -def eq.char_eq_self(c: Char) -> {Char.is_eq(c, c) == True{} : Bool}: - match c: - case Chr{+x}: - Equal.cong(Cmp, Bool, r => Cmp.is_eq(r), U32.cmp(x, x), EQ{}, - eq.u32_cmp_self(x)) - -def eq.bit.emp(b: Bool) -> Type: - match b: - case False{}: - Unit - case True{}: - Empty - -def eq.bit.clash(e: {False{} == True{} : Bool}) -> Empty: - %e : eq.bit.emp(_) - Unit{} - -def eq.bit.gone(a, -A: Kind(a), e: Empty) -> A: - match e: - -def eq.bit.absurd(a, -A: Kind(a), e: {False{} == True{} : Bool}) -> A: - eq.bit.gone(a, A, eq.bit.clash(e)) - -def eq.and.left(u: Bool, v: Bool, e: {Bool.and(u, v) == True{} : Bool}) - -> {u == True{} : Bool}: - match u: - case True{}: - {==} - case False{}: - e - -def eq.and.right(u: Bool, v: Bool, e: {Bool.and(u, v) == True{} : Bool}) - -> {v == True{} : Bool}: - match u: - case True{}: - e - case False{}: - eq.bit.absurd(&2, {v == True{} : Bool}, e) - -def eq.not.true(u: Bool, e: {Bool.not(u) == True{} : Bool}) - -> {u == False{} : Bool}: - match u: - case True{}: - eq.bit.absurd(&2, {True{} == False{} : Bool}, e) - case False{}: - {==} - -def eq.starts_nil(s: String) -> {String.starts_with(s, "") == True{} : Bool}: - match s: - case SNil{}: - {==} - case SCon{h, t}: - {==} - -def eq.starts_append(a: String, b: String) - -> {String.starts_with(String.append(a, b), a) == True{} : Bool}: - match a: - case SNil{}: - eq.starts_nil(b) - case SCon{+h, +t}: - Equal.trans(Bool, - String.starts_with.go.if(t, String.append(t, b), Char.is_eq(h, h)), - String.starts_with.go.if(t, String.append(t, b), True{}), - True{}, - Equal.cong(Bool, Bool, - x => String.starts_with.go.if(t, String.append(t, b), x), - Char.is_eq(h, h), True{}, eq.char_eq_self(h)), - eq.starts_append(t, b)) - -# LAW: one step of the UTF-8 partition -law utf8_split.arm: - for -h: Char - for -t: String - for -m: Nat - for b: Bool - for ih: {String.append(Http.octs.take(t, m), Http.octs.drop(t, m)) == t : String} - {String.append( - Http.octs.take.step(b, h, _u => Http.octs.take(t, m)), - Http.octs.drop.step(b, h, t, _u => Http.octs.drop(t, m))) == SCon{h, t} : String} - -def utf8_split.arm(h, t, m, b, ih): - match b: - case True{}: - {==} - case False{}: - Equal.cong(String, String, x => SCon{h, x}, - String.append(Http.octs.take(t, m), Http.octs.drop(t, m)), t, ih) - -def Laws.utf8_split(s, n): - match s: - case SNil{}: - {==} - case SCon{+h, +t}: - utf8_split.arm(h, t, Nat.sub(n, Http.octs.width(Char.to_u32(h))), - Nat.is_lt(n, Http.octs.width(Char.to_u32(h))), - Laws.utf8_split(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))))) - -law lt_zero: - for n: Nat - {Nat.is_lt(n, 0n) == False{} : Bool} - -def lt_zero(n): - match n: - case 0n: - {==} - case 1n+p: - {==} - -law lt_add: - for a: Nat - for +b: Nat - {Nat.is_lt(Nat.add(a, b), a) == False{} : Bool} - -def lt_add(a, b): - match a: - case 0n: - lt_zero(b) - case 1n+p: - lt_add(p, b) - -law lt_self: - for n: Nat - {Nat.is_lt(n, n) == False{} : Bool} - -def lt_self(n): - match n: - case 0n: - {==} - case 1n+p: - lt_self(p) - -law sub_zero: - for n: Nat - {Nat.sub(n, 0n) == n : Nat} - -def sub_zero(n): - match n: - case 0n: - {==} - case 1n+p: - {==} - -law sub_add: - for a: Nat - for +b: Nat - {Nat.sub(Nat.add(a, b), a) == b : Nat} - -def sub_add(a, b): - match a: - case 0n: - sub_zero(b) - case 1n+p: - sub_add(p, b) - -law take_all: - for +s: String - {Http.octs.take(s, Http.octs.len(s)) == s : String} - -def take_all(s): - match s: - case SNil{}: - {==} - case SCon{+h, +t}: - Equal.trans(String, - Http.octs.take(SCon{h, t}, Http.octs.len(SCon{h, t})), - SCon{h, Http.octs.take(t, Nat.sub( - Nat.add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t)), - Http.octs.width(Char.to_u32(h))))}, - SCon{h, t}, - Equal.cong(Bool, String, - b => Http.octs.take.step(b, h, _u => Http.octs.take(t, Nat.sub( - Nat.add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t)), - Http.octs.width(Char.to_u32(h))))), - Nat.is_lt(Nat.add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t)), - Http.octs.width(Char.to_u32(h))), - False{}, - lt_add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t))), - Equal.trans(String, - SCon{h, Http.octs.take(t, Nat.sub( - Nat.add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t)), - Http.octs.width(Char.to_u32(h))))}, - SCon{h, Http.octs.take(t, Http.octs.len(t))}, - SCon{h, t}, - Equal.cong(Nat, String, m => SCon{h, Http.octs.take(t, m)}, - Nat.sub(Nat.add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t)), - Http.octs.width(Char.to_u32(h))), - Http.octs.len(t), - sub_add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t))), - Equal.cong(String, String, x => SCon{h, x}, - Http.octs.take(t, Http.octs.len(t)), t, take_all(t)))) - -def Laws.clen_exact(st, hs, body): - Equal.trans(Http.Reply, - Http.parse.clen(st, hs, Http.octs.len(body), body), - Http.parse.short(st, hs, Http.octs.len(body), body, False{}), - Http.Reply{st, hs, body}, - Equal.cong(Bool, Http.Reply, - b => Http.parse.short(st, hs, Http.octs.len(body), body, b), - Nat.is_lt(Http.octs.len(body), Http.octs.len(body)), False{}, - lt_self(Http.octs.len(body))), - Equal.cong(String, Http.Reply, x => Http.Reply{st, hs, x}, - Http.octs.take(body, Http.octs.len(body)), body, take_all(body))) - -def Laws.clen_short(st, hs, n, body, e): - %e : {Http.parse.short(st, hs, n, body, _) - == Http.Torn{"the response body stopped before its Content-Length"} - : Http.Reply} - {==} - -def Laws.path_absolute(ps): - match ps: - case []: - {==} - case h <> t: - eq.starts_nil(String.join(t, "/")) - -law split_whole.step: - for +h: Char - for +t: String - for +c: Char - for e: {False{} == Char.is_eq(h, c) : Bool} - for ih: {String.split(t, c) == [t] : List<&2, String>} - {String.split(SCon{h, t}, c) == [SCon{h, t}] : List<&2, String>} - -def split_whole.step(h, t, c, e, ih): - %e : {String.split.fin(h, String.split(t, c), _) == [SCon{h, t}] - : List<&2, String>} - Equal.cong(List<&2, String>, List<&2, String>, - r => String.split.fin(h, r, False{}), - String.split(t, c), [t], ih) - -def Laws.split_whole(s, c, e): - match s: - case SNil{}: - {==} - case SCon{+h, +t}: - split_whole.step(h, t, c, - Equal.sym(Bool, Char.is_eq(h, c), False{}, - eq.not.true(Char.is_eq(h, c), - eq.and.left(Bool.not(Char.is_eq(h, c)), Laws.avoids(t, c), e))), - Laws.split_whole(t, c, - eq.and.right(Bool.not(Char.is_eq(h, c)), Laws.avoids(t, c), e))) - -law split_cut.step: - for +h: Char - for +t: String - for +c: Char - for r: String - for e: {False{} == Char.is_eq(h, c) : Bool} - for ih: {String.split(String.append(t, SCon{c, r}), c) == t <> String.split(r, c) - : List<&2, String>} - {String.split(String.append(SCon{h, t}, SCon{c, r}), c) - == SCon{h, t} <> String.split(r, c) : List<&2, String>} - -def split_cut.step(h, t, c, r, e, ih): - %e : {String.split.fin(h, String.split(String.append(t, SCon{c, r}), c), _) - == SCon{h, t} <> String.split(r, c) : List<&2, String>} - Equal.cong(List<&2, String>, List<&2, String>, - x => String.split.fin(h, x, False{}), - String.split(String.append(t, SCon{c, r}), c), t <> String.split(r, c), ih) - -def Laws.split_cut(s, c, r, e): - match s: - case SNil{}: - Equal.cong(Bool, List<&2, String>, - x => String.split.fin(c, String.split(r, c), x), - Char.is_eq(c, c), True{}, eq.char_eq_self(c)) - case SCon{+h, +t}: - split_cut.step(h, t, c, r, - Equal.sym(Bool, Char.is_eq(h, c), False{}, - eq.not.true(Char.is_eq(h, c), - eq.and.left(Bool.not(Char.is_eq(h, c)), Laws.avoids(t, c), e))), - Laws.split_cut(t, c, r, - eq.and.right(Bool.not(Char.is_eq(h, c)), Laws.avoids(t, c), e))) - -def Laws.dechunk_empty(): - {==} - -def Laws.chunks_empty_fuel(): - {==} - -def Laws.hexlen_ignores_extension(s, ext, e): - Equal.trans(Nat, - Http.hexlen(String.append(s, SCon{';', ext})), - Http.hex.go(String.trim(s), 0n), - Http.hexlen(s), - Equal.cong(List<&2, String>, Nat, - ps => Http.hex.go(String.trim(Http.first(ps)), 0n), - String.split(String.append(s, SCon{';', ext}), ';'), - s <> String.split(ext, ';'), - Laws.split_cut(s, ';', ext, e)), - Equal.sym(Nat, Http.hexlen(s), Http.hex.go(String.trim(s), 0n), - Equal.cong(List<&2, String>, Nat, - ps => Http.hex.go(String.trim(Http.first(ps)), 0n), - String.split(s, ';'), [s], Laws.split_whole(s, ';', e)))) - -def Laws.first_is_first_word(w, r, e): - Equal.cong(List<&2, String>, String, ps => Http.first(ps), - String.split(String.append(w, SCon{' ', r}), ' '), - w <> String.split(r, ' '), Laws.split_cut(w, ' ', r, e)) - -def Laws.second_is_next_word(w, r, e): - Equal.cong(List<&2, String>, String, ps => Http.second(ps), - String.split(String.append(w, SCon{' ', r}), ' '), - w <> String.split(r, ' '), Laws.split_cut(w, ' ', r, e)) - -def Laws.request_names_method(method, host, path, hs, body): - eq.starts_append(method, - " " ++ path ++ " HTTP/1.1\r\n" - ++ Http.headers.lines(Http.request.fields(host, hs, body)) - ++ "\r\n" ++ body) - -def Laws.get_req_names_path(host, path, hs): - eq.starts_append(path, - " HTTP/1.1\r\n" ++ Http.headers.lines(Http.request.fields(host, hs, "")) - ++ "\r\n") - -def Laws.head_req_names_path(host, path, hs): - eq.starts_append(path, - " HTTP/1.1\r\n" ++ Http.headers.lines(Http.request.fields(host, hs, "")) - ++ "\r\n") - -def Laws.post_req_names_path(host, path, hs, body): - +payload = body - eq.starts_append(path, - " HTTP/1.1\r\n" - ++ Http.headers.lines(Http.request.fields(host, hs, payload)) - ++ "\r\n" ++ payload) - -def Laws.parse_needs_blank_line(_raw, e): - %e : {Http.parse.cut(_) - == Http.Torn{"the response head has no blank line after it"} - : Http.Reply} - {==} - -def Laws.parse_splits_at_blank_line(_raw, before, after, e): - %e : {Http.parse.cut(_) == Http.parse.head(String.lines(before), after) - : Http.Reply} - {==} - -def Laws.unframed_is_rest(_st, _hs, _body): - {==} - -def Laws.head_hit_ignores_case(_h, _name): - {==} - -def Laws.parse_needs_status(ls, body, e): - %e : {Http.parse.status(_, ls, body) - == Http.Torn{"the response has no status line"} - : Http.Reply} - {==} - -def Laws.show_torn_is_marked(why): - eq.starts_nil(why) - -def Laws.show_reply_opens_with_status(status, _hs, body): - eq.starts_append(U32.show(status), " " ++ body) - -def Laws.respond_opens_with_version(status, hs, body): - eq.starts_append("HTTP/1.1 ", - U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" - ++ "Connection: close\r\n" ++ Http.headers.lines(hs) - ++ Http.clen.line(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)) - ++ "\r\n" ++ body) - -def Laws.reason_200(): - {==} - -def Laws.ask_needs_blank_line(_raw, e): - %e : {Http.ask.cut(_) - == Http.Bad{"the request head has no blank line after it"} - : Http.Request} - {==} - -def Laws.ask_needs_request_line(_body): - {==} - -def Laws.ask_clen_exact(method, target, hs, body): - Equal.trans(Http.Request, - Http.ask.clen(method, target, hs, Http.octs.len(body), body), - Http.ask.short(method, target, hs, Http.octs.len(body), body, False{}), - Http.Request{method, target, hs, body}, - Equal.cong(Bool, Http.Request, - b => Http.ask.short(method, target, hs, Http.octs.len(body), body, b), - Nat.is_lt(Http.octs.len(body), Http.octs.len(body)), False{}, - lt_self(Http.octs.len(body))), - Equal.cong(String, Http.Request, - x => Http.Request{method, target, hs, x}, - Http.octs.take(body, Http.octs.len(body)), body, take_all(body))) - -def Laws.ask_clen_short(method, target, hs, n, body, e): - %e : {Http.ask.short(method, target, hs, n, body, _) - == Http.Bad{"the request body stopped before its Content-Length"} - : Http.Request} - {==} - -def Laws.ask_show_bad_is_marked(why): - eq.starts_nil(why) - -def Laws.dflt_plain(_scheme, e): - %e : {Bool.pick(U32, _, 443, 80) == 80 : U32} - {==} - -def Laws.dflt_https(): - {==} - -def Laws.parse_needs_slashes(scheme, rest, e): - %e : {Url.parse.body(scheme, rest, _) - == Url.Bad{scheme ++ ":" ++ rest ++ " has no //"} - : Url.Loc} - {==} - -def Laws.parse_scheme_first(_url, h, t, e): - %e : {Url.parse.scheme(_) - == Url.parse.rest(String.to_lower(h), String.join(t, ":")) : Url.Loc} - {==} - -def Laws.url_show_bad_is_marked(why): - eq.starts_nil(why) - -def Laws.url_show_opens_with_scheme(scheme, host, port, path): - eq.starts_append(scheme, " " ++ host ++ " " ++ U32.show(port) ++ " " ++ path) - -def Laws.bearer_is_authorization(_token): - {==} - -def Laws.url_empty_path(): - {==} - -def Laws.url_ipv4_host(): - {==} - -def Laws.url_ipv4_port(): - {==} - -def Laws.url_userinfo_stripped(): - {==} - -def Laws.url_query_drops_fragment(): - {==} - -def Laws.url_query_on_empty_path(): - {==} - -def Laws.url_scheme_lowercased(): - {==} - -def Laws.url_https_default_port(): - {==} - -def Laws.url_https_explicit_port(): - {==} - -def Laws.respond_writes_reason_and_close(_status, _hs, _body): - {==} - -def Laws.put_req_names_path(host, path, hs, body): - +payload = body - eq.starts_append(path, - " HTTP/1.1\r\n" - ++ Http.headers.lines(Http.request.fields(host, hs, payload)) - ++ "\r\n" ++ payload) - -def Laws.delete_req_names_path(host, path, hs, body): - +payload = body - eq.starts_append(path, - " HTTP/1.1\r\n" - ++ Http.headers.lines(Http.request.fields(host, hs, payload)) - ++ "\r\n" ++ payload) - -def Laws.options_req_names_path(host, path, hs): - eq.starts_append(path, - " HTTP/1.1\r\n" ++ Http.headers.lines(Http.request.fields(host, hs, "")) - ++ "\r\n") - -def Laws.method_tokens_are_case_sensitive(): - {==} - -def Laws.method_safe_get(): - {==} - -def Laws.method_safe_get_lower(): - {==} - -def Laws.method_safe_post(): - {==} - -def Laws.method_idempotent_put(): - {==} - -def Laws.method_idempotent_delete(): - {==} - -def Laws.method_idempotent_options(): - {==} - -def Laws.method_idempotent_post(): - {==} - -def Laws.reason_400(): - {==} - -def Laws.reason_404(): - {==} - -def Laws.reason_500(): - {==} - -def Laws.head_reply_drops_body(): - {==} - -def Laws.get_reply_keeps_body(): - {==} - -def Laws.head_line_trims(): - {==} - -def Laws.head_line_rejects_obs_fold(): - {==} - -def Laws.head_line_rejects_continuation(): - {==} - -def Laws.ask_rejects_relative_target(): - {==} - -def Laws.ask_origin_form_keeps_query(): - {==} - -def Laws.parse_rejects_cl_and_te(): - {==} - -def Laws.ask_rejects_disagreeing_cl(): - {==} - -def Laws.parse_rejects_non_hex_chunk(): - {==} - -def Laws.dechunk_two_chunks(): - {==} - -def Laws.dechunk_wiki(): - {==} - -def Laws.parse_clen_takes_prefix(): - {==} - -def Laws.parse_chunked_body(): - {==} - -def Laws.token_ok_plain(): - {==} - -def Laws.token_ok_space(): - {==} - -def Laws.token_ok_tab(): - {==} - -def Laws.b64_empty(): - {==} - -def Laws.b64_f(): - {==} - -def Laws.b64_fo(): - {==} - -def Laws.b64_foo(): - {==} - -def Laws.basic_user_pass(): - {==} - -def Laws.cookie_line_attributes(): - {==} - -def Laws.cookie_parse_round_trip(): - {==} - -def Laws.cookie_rejects_empty_name(): - {==} - -def Laws.cookie_request_joins(): - {==} - -def Laws.cookie_default_path_dir(): - {==} - -def Laws.cookie_default_path_root(): - {==} - -def Laws.cookie_default_path_trailing(): - {==} - -def Laws.cookie_receive_host_only(): - {==} - -def Laws.cookie_domain_suffix(): - {==} - -def Laws.cookie_domain_not_suffix(): - {==} - -def Laws.cookie_domain_ip_suffix(): - {==} - -def Laws.cookie_domain_ip_exact(): - {==} - -def Laws.cookie_path_prefix(): - {==} - -def Laws.cookie_path_not_prefix(): - {==} - -def Laws.cookie_secure_needs_tls(): - {==} - -def Laws.cookie_secure_on_tls(): - {==} - -def Laws.cookie_max_age_zero(): - {==} - -def Laws.cookie_max_age_open(): - {==} - -def Laws.cookie_session_alive(): - {==} - -def Laws.cache_directive_case(): - {==} - -def Laws.cache_directives(): - {==} - -def Laws.fresh_max_age_zero(): - {==} - -def Laws.fresh_max_age_open(): - {==} - -def Laws.fresh_max_age_limit(): - {==} - -def Laws.fresh_no_cache(): - {==} - -def Laws.fresh_no_store(): - {==} - -def Laws.fresh_public_only(): - {==} - -def Laws.cache_note_expires(): - {==} - -def Laws.fresh_max_age_overrides_expires(): - {==} - -def Laws.cache_max_age_present(): - {==} - -def Laws.cors_credentials_reflect_origin(): - {==} - -def Laws.cors_star_without_credentials(): - {==} - -def Laws.cors_explicit_origin(): - {==} - -def Laws.cors_refused_origin(): - {==} - -def Laws.cors_vary_specific(): - {==} - -def Laws.cors_vary_star(): - {==} - -def Laws.cors_vary_empty(): - {==} - -def Laws.cors_preflight_allowed(): - {==} - -def Laws.cors_preflight_method_denied(): - {==} - -def Laws.cors_simple_reflects(): - {==} - -def Laws.cors_simple_star(): - {==} - -def Laws.cors_simple_refused(): - {==} - -def Laws.cors_on_preflight(): - {==} - -def Laws.cors_origin_header(): - {==} - -def Laws.cors_request_method_header(): - {==} - -def Laws.cors_request_headers_header(): - {==} - -def Laws.url_http_default_port(): - {==} - -def Laws.url_query_keeps_question(): - {==} - -def Laws.url_fragment_hides_query(): - {==} - -def Laws.url_userinfo_last_at(): - {==} - -def Laws.url_trailing_slash(): - {==} - -def Laws.url_bad_port_uses_default(): - {==} - -def Laws.post_writes_content_length(): - {==} - -def Laws.get_omits_content_length(): - {==} - -def Laws.reason_201(): - {==} - -def Laws.reason_204(): - {==} - -def Laws.reason_405(): - {==} - -def Laws.reason_unknown_empty(): - {==} - -def Laws.respond_204_drops_body(): - {==} - -def Laws.respond_304_drops_body(): - {==} - -def Laws.respond_200_keeps_body(): - {==} - -def Laws.method_safe_head(): - {==} - -def Laws.method_safe_options(): - {==} - -def Laws.method_safe_put(): - {==} - -def Laws.method_safe_delete(): - {==} - -def Laws.method_idempotent_get(): - {==} - -def Laws.method_idempotent_head(): - {==} - -def Laws.method_idempotent_patch(): - {==} - -def Laws.headers_find_ignores_case(): - {==} - -def Laws.headers_find_missing(): - {==} - -def Laws.headers_find_first(): - {==} - -def Laws.ask_keeps_field_name_spelling(): - {==} - -def Laws.parse_equal_content_lengths(): - {==} - -def Laws.ask_rejects_cl_and_te(): - {==} - -def Laws.parse_rejects_disagreeing_cl(): - {==} - -def Laws.ask_equal_content_lengths(): - {==} - -def Laws.parse_chunked_ignores_case(): - {==} - -def Laws.dechunk_ignores_extension(): - {==} - -def Laws.dechunk_stops_at_zero(): - {==} - -def Laws.dechunk_only_last(): - {==} - -def Laws.hexlen_extension_and_case(): - {==} - -def Laws.hexlen_alpha(): - {==} - -def Laws.digits_read_decimal(): - {==} - -def Laws.digits_read_rejects_junk(): - {==} - -def Laws.ask_options_asterisk(): - {==} - -def Laws.ask_get_asterisk_rejected(): - {==} - -def Laws.ask_rejects_absolute_form(): - {==} - -def Laws.b64_user_pass(): - {==} - -def Laws.token_ok_newline(): - {==} - -def Laws.cookie_samesite_strict(): - {==} - -def Laws.cookie_samesite_none(): - {==} - -def Laws.cookie_samesite_unknown(): - {==} - -def Laws.cookie_max_age_ignores_junk(): - {==} - -def Laws.cookie_domain_lowercased(): - {==} - -def Laws.cookie_path_identical(): - {==} - -def Laws.cookie_path_root(): - {==} - -def Laws.cookie_path_longer_cookie(): - {==} - -def Laws.cookie_path_slash_prefix(): - {==} - -def Laws.cookie_default_path_one_slash(): - {==} - -def Laws.cookie_ipv4_literal(): - {==} - -def Laws.cookie_ipv4_name(): - {==} - -def Laws.cookie_send_host_only(): - {==} - -def Laws.cookie_send_other_host(): - {==} - -def Laws.cookie_send_unset_domain(): - {==} - -def Laws.cookie_insecure_on_tcp(): - {==} - -def Laws.cookie_max_age_one(): - {==} - -def Laws.cache_ignores_s_maxage(): - {==} - -def Laws.cache_max_age_ignores_junk(): - {==} - -def Laws.cache_no_cache_alone(): - {==} - -def Laws.cache_private_alone(): - {==} - -def Laws.fresh_max_age_one(): - {==} - -def Laws.cors_preflight_header_case(): - {==} - -def Laws.cors_preflight_origin_denied(): - {==} - -def Laws.cors_preflight_header_denied(): - {==} - -def Laws.cors_credentials_explicit_origin(): - {==} - -def Laws.cors_get_is_not_preflight(): - {==} - -def Laws.json_string_is_text_body(): - {==} - -def Laws.json_parse_string(): - {==} - -def Laws.json_parse_rejects(): - {==} - -def Laws.cors_preflight_star(): - {==} - -# --- the server's decisions --- - -def Laws.step_bad(_why): - {==} - -def Laws.step_call(_method, _target, _hs, _body): - {==} - -def Laws.shape_head(_status, _hs, _body): - {==} - -def Laws.shape_other(_method, e, reply): - match reply: - case Http.Torn{why}: - %e : {Http.reply.for.pick(Http.Torn{why}, _) == Http.Torn{why} - : Http.Reply} - {==} - case Http.Reply{status, hs, body}: - %e : {Http.reply.for.pick(Http.Reply{status, hs, body}, _) - == Http.Reply{status, hs, body} : Http.Reply} - {==} - -def Laws.respond_204(_hs, _body): - {==} - -def Laws.respond_304(_hs, _body): - {==} - -def Laws.respond_other(status, e, hs, body): - %e : {Http.respond.of.body(status, hs, body, _) - == Http.respond(status, hs, body) : String} - {==} - -def Laws.lost_failed(_a, _phase, _code, _why): - {==} - -def Laws.lost_kept(_a, _phase, _value): - {==} - -def Laws.lost_send(_code, _why, _left): - {==} - -def Laws.lost_sent(_done): - {==} - -# --- percent-encoding (SPEC.md HTTP-URL) --- - -def Laws.quote_nil(_slash): - {==} - -def Laws.quote_cons(_slash, _c, _t): - {==} - -def Laws.one_unreserved(slash, c, e, rest): - %e : {Pct.one.of(c, rest, Bool.or(_, Bool.and(U32.is_eq(c, 47), slash))) - == SCon{Chr{c}, rest} : String} - {==} - -def Laws.one_escaped(_slash, c, e, rest): - %e : {Pct.one.of(c, rest, _) == Pct.pct(Pct.utf8(c), rest) : String} - {==} - -def Laws.pct_nil(_rest): - {==} - -def Laws.pct_cons(_b, _bs, _rest): - {==} - -def Laws.hex_digit(n, e): - %e : {Chr{Bool.pick(U32, _, U32.add(48, n), U32.add(55, n))} - == Chr{U32.add(48, n)} : Char} - {==} - -def Laws.hex_letter(n, e): - %e : {Chr{Bool.pick(U32, _, U32.add(48, n), U32.add(55, n))} - == Chr{U32.add(55, n)} : Char} - {==} - -def Laws.utf8_one(c, e): - %e : {Pct.utf8.one(c, _) == [c] : List<&2, U32>} - {==} - -def Laws.utf8_two(c, e1, e2): - %e1 : {Pct.utf8.one(c, _) == [U32.or(192, U32.shrn(c, 6n)), - U32.or(128, U32.and(c, 63))] : List<&2, U32>} - %e2 : {Pct.utf8.two(c, _) == [U32.or(192, U32.shrn(c, 6n)), - U32.or(128, U32.and(c, 63))] : List<&2, U32>} - {==} - -def Laws.utf8_three(c, e1, e2, e3): - %e1 : {Pct.utf8.one(c, _) == [U32.or(224, U32.shrn(c, 12n)), - U32.or(128, U32.and(U32.shrn(c, 6n), 63)), - U32.or(128, U32.and(c, 63))] : List<&2, U32>} - %e2 : {Pct.utf8.two(c, _) == [U32.or(224, U32.shrn(c, 12n)), - U32.or(128, U32.and(U32.shrn(c, 6n), 63)), - U32.or(128, U32.and(c, 63))] : List<&2, U32>} - %e3 : {Pct.utf8.three(c, _) == [U32.or(224, U32.shrn(c, 12n)), - U32.or(128, U32.and(U32.shrn(c, 6n), 63)), - U32.or(128, U32.and(c, 63))] : List<&2, U32>} - {==} - -def Laws.utf8_four(c, e1, e2, e3): - %e1 : {Pct.utf8.one(c, _) == [U32.or(240, U32.shrn(c, 18n)), - U32.or(128, U32.and(U32.shrn(c, 12n), 63)), - U32.or(128, U32.and(U32.shrn(c, 6n), 63)), - U32.or(128, U32.and(c, 63))] : List<&2, U32>} - %e2 : {Pct.utf8.two(c, _) == [U32.or(240, U32.shrn(c, 18n)), - U32.or(128, U32.and(U32.shrn(c, 12n), 63)), - U32.or(128, U32.and(U32.shrn(c, 6n), 63)), - U32.or(128, U32.and(c, 63))] : List<&2, U32>} - %e3 : {Pct.utf8.three(c, _) == [U32.or(240, U32.shrn(c, 18n)), - U32.or(128, U32.and(U32.shrn(c, 12n), 63)), - U32.or(128, U32.and(U32.shrn(c, 6n), 63)), - U32.or(128, U32.and(c, 63))] : List<&2, U32>} - {==} - -def Laws.path_keeps_slash(_rest): - {==} - -def Laws.component_escapes_slash(_rest): - {==} - -def Laws.component_is_path_elsewhere(c, e, rest): - %e : {Pct.one.of(c, rest, Bool.or(Pct.unreserved(c), Bool.and(_, False{}))) - == Pct.one.of(c, rest, Bool.or(Pct.unreserved(c), Bool.and(_, True{}))) - : String} - {==} - -def Laws.frame_kept(slash, c, e, rest): - %e : {Laws.framed(slash, Pct.one.of(c, rest, _), 0n) - == Laws.framed(slash, rest, 0n) : Bool} - %e : {Bool.and(Bool.or(_, U32.is_eq(c, 37)), - Laws.framed(slash, rest, - Bool.pick(Nat, _, 0n, Bool.pick(Nat, U32.is_eq(c, 37), 2n, 0n)))) - == Laws.framed(slash, rest, 0n) : Bool} - {==} - -# LAW: one %XX triple whose nibbles are hex digits stays in the frame -law frame_bytes.step: - for +slash: Bool - for +b: U32 - for t: List<&2, U32> - for +rest: String - for e1: {True{} == Laws.hexdig(Pct.hex(U32.shrn(b, 4n))) : Bool} - for e2: {True{} == Laws.hexdig(Pct.hex(U32.and(b, 15))) : Bool} - for ih: {Laws.framed(slash, Pct.pct(t, rest), 0n) - == Laws.framed(slash, rest, 0n) : Bool} - {Laws.framed(slash, Pct.pct(b <> t, rest), 0n) - == Laws.framed(slash, rest, 0n) : Bool} - -def frame_bytes.step(slash, b, t, rest, e1, e2, ih): - %e1 : {Bool.and(_, Bool.and(Laws.hexdig(Pct.hex(U32.and(b, 15))), - Laws.framed(slash, Pct.pct(t, rest), 0n))) - == Laws.framed(slash, rest, 0n) : Bool} - %e2 : {Bool.and(_, Laws.framed(slash, Pct.pct(t, rest), 0n)) - == Laws.framed(slash, rest, 0n) : Bool} - ih - -def Laws.frame_bytes(slash, bs, e, rest): - match bs: - case []: - {==} - case +b <> +t: - frame_bytes.step(slash, b, t, rest, - Equal.sym(Bool, Laws.hexdig(Pct.hex(U32.shrn(b, 4n))), True{}, - eq.and.left(Laws.hexdig(Pct.hex(U32.shrn(b, 4n))), - Bool.and(Laws.hexdig(Pct.hex(U32.and(b, 15))), Laws.hexes(t)), e)), - Equal.sym(Bool, Laws.hexdig(Pct.hex(U32.and(b, 15))), True{}, - eq.and.left(Laws.hexdig(Pct.hex(U32.and(b, 15))), Laws.hexes(t), - eq.and.right(Laws.hexdig(Pct.hex(U32.shrn(b, 4n))), - Bool.and(Laws.hexdig(Pct.hex(U32.and(b, 15))), Laws.hexes(t)), - e))), - Laws.frame_bytes(slash, t, - eq.and.right(Laws.hexdig(Pct.hex(U32.and(b, 15))), Laws.hexes(t), - eq.and.right(Laws.hexdig(Pct.hex(U32.shrn(b, 4n))), - Bool.and(Laws.hexdig(Pct.hex(U32.and(b, 15))), Laws.hexes(t)), - e)), - rest)) - -# LAW: one character stays in the frame, kept or escaped -law frame_char: - for +slash: Bool - for +c: U32 - for +k: Bool - for +ek: {k == Pct.safe(slash, c) : Bool} - for +h: {Bool.or(k, Laws.hexes(Pct.utf8(c))) == True{} : Bool} - for +rest: String - {Laws.framed(slash, Pct.one(slash, c, rest), 0n) - == Laws.framed(slash, rest, 0n) : Bool} - -def frame_char(slash, c, k, ek, h, rest): - match k: - case True{}: - Laws.frame_kept(slash, c, ek, rest) - case False{}: - %ek : {Laws.framed(slash, Pct.one.of(c, rest, _), 0n) - == Laws.framed(slash, rest, 0n) : Bool} - Laws.frame_bytes(slash, Pct.utf8(c), h, rest) - -def Laws.frame_quote(slash, str, e): - match str: - case SNil{}: - {==} - case SCon{Chr{+c}, +t}: - Equal.trans(Bool, - Laws.framed(slash, Pct.one(slash, c, Pct.quote(slash, t)), 0n), - Laws.framed(slash, Pct.quote(slash, t), 0n), - True{}, - frame_char(slash, c, Pct.safe(slash, c), {==}, - eq.and.left(Bool.or(Pct.safe(slash, c), Laws.hexes(Pct.utf8(c))), - Laws.nibbles(slash, t), e), - Pct.quote(slash, t)), - Laws.frame_quote(slash, t, - eq.and.right(Bool.or(Pct.safe(slash, c), Laws.hexes(Pct.utf8(c))), - Laws.nibbles(slash, t), e))) - -# --- the client's timeout (SPEC.md HTTP-CLI) --- - -def Laws.why_late(_why): - {==} - -def Laws.why_other(_n, e, why): - %e : {Spec.wire.why.of(why, _) == why : String} - {==} - -def Laws.why_none(_why): - {==} - -def Laws.why_late_timed_out(why): - eq.starts_append("timed out: ", why) - -# --- redirects (SPEC.md HTTP-REDIR) --- - -def Laws.redir_go(_status, e, _n, url, loc, next, et): - %e : {Redirect.redirect.on(url, loc, _) == Redirect.Go{next} : Redirect.Hop} - %et : {Redirect.redirect.hop(_) == Redirect.Go{next} : Redirect.Hop} - {==} - -# a response whose Location has no target stops, redirect or not -law redir_no_target.on: - for url: String - for +loc: String - for moved: Bool - for et: {None{} == Redirect.redirect.target(url, loc) : Maybe<&2, String>} - {Redirect.redirect.on(url, loc, moved) == Redirect.Stop{} : Redirect.Hop} - -def redir_no_target.on(_url, _loc, moved, et): - match moved: - case True{}: - %et : {Redirect.redirect.hop(_) == Redirect.Stop{} : Redirect.Hop} - {==} - case False{}: - {==} - -def Laws.redir_no_target(status, left, url, loc, et): - match left: - case 0n: - {==} - case 1n+p: - redir_no_target.on(url, loc, Redirect.redirect.is(status), et) - -def Laws.redir_plan_location(_left, _url, _status, _hs): - {==} - -def Laws.redir_other(_status, e, left, url, loc): - match left: - case 0n: - {==} - case 1n+p: - %e : {Redirect.redirect.on(url, loc, _) == Redirect.Stop{} - : Redirect.Hop} - {==} - -def Laws.redir_zero(_url, _status, _loc): - {==} - -def Laws.target_absolute(url, loc, e): - %e : {Redirect.redirect.target.of(url, loc, _) == Some{loc} - : Maybe<&2, String>} - {==} - -# --- HTML escaping (SPEC.md HTTP-HTML) --- - -def Laws.html_escape_onto(_s): - {==} - -def Laws.html_nil(_rest): - {==} - -def Laws.html_cons(_c, _t, _rest): - {==} - -def Laws.html_amp(_rest): - {==} - -def Laws.html_lt(_rest): - {==} - -def Laws.html_gt(_rest): - {==} - -def Laws.html_dq(_rest): - {==} - -def Laws.html_sq(_rest): - {==} - -def Laws.html_other(c, e1, e2, e3, e4, e5, rest): - %e1 : {Html.one.amp(c, rest, _) == SCon{c, rest} : String} - %e2 : {Html.one.lt(c, rest, _) == SCon{c, rest} : String} - %e3 : {Html.one.gt(c, rest, _) == SCon{c, rest} : String} - %e4 : {Html.one.dq(c, rest, _) == SCon{c, rest} : String} - %e5 : {Html.one.sq(c, rest, _) == SCon{c, rest} : String} - {==} - -# LAW: a character none of the five stays in the frame -law frame.sq: - for +c: Char - for rest: String - for k: Bool - for ek: {k == Char.is_eq(c, '\'') : Bool} - for ea: {False{} == Char.is_eq(c, '&') : Bool} - for el: {False{} == Char.is_eq(c, '<') : Bool} - for eg: {False{} == Char.is_eq(c, '>') : Bool} - for ed: {False{} == Char.is_eq(c, '"') : Bool} - {Laws.html.framed(Html.one.sq(c, rest, k), Laws.Out{}) - == Laws.html.framed(rest, Laws.Out{}) : Bool} - -def frame.sq(c, rest, k, ek, ea, el, eg, ed): - match k: - case True{}: - {==} - case False{}: - %ea : {Bool.and(Laws.ent.out.ok(Char.is_eq(c, '<'), Char.is_eq(c, '>'), - Char.is_eq(c, '"'), Char.is_eq(c, '\'')), - Laws.html.framed(rest, Laws.ent.out.next(_))) - == Laws.html.framed(rest, Laws.Out{}) : Bool} - %el : {Bool.and(Laws.ent.out.ok(_, Char.is_eq(c, '>'), - Char.is_eq(c, '"'), Char.is_eq(c, '\'')), - Laws.html.framed(rest, Laws.Out{})) - == Laws.html.framed(rest, Laws.Out{}) : Bool} - %eg : {Bool.and(Laws.ent.out.ok(False{}, _, - Char.is_eq(c, '"'), Char.is_eq(c, '\'')), - Laws.html.framed(rest, Laws.Out{})) - == Laws.html.framed(rest, Laws.Out{}) : Bool} - %ed : {Bool.and(Laws.ent.out.ok(False{}, False{}, _, - Char.is_eq(c, '\'')), - Laws.html.framed(rest, Laws.Out{})) - == Laws.html.framed(rest, Laws.Out{}) : Bool} - %ek : {Bool.and(Laws.ent.out.ok(False{}, False{}, False{}, _), - Laws.html.framed(rest, Laws.Out{})) - == Laws.html.framed(rest, Laws.Out{}) : Bool} - {==} - -# LAW: a character that is not `&`, `<` or `>` stays in the frame -law frame.dq: - for +c: Char - for rest: String - for k: Bool - for ek: {k == Char.is_eq(c, '"') : Bool} - for ea: {False{} == Char.is_eq(c, '&') : Bool} - for el: {False{} == Char.is_eq(c, '<') : Bool} - for eg: {False{} == Char.is_eq(c, '>') : Bool} - {Laws.html.framed(Html.one.dq(c, rest, k), Laws.Out{}) - == Laws.html.framed(rest, Laws.Out{}) : Bool} - -def frame.dq(c, rest, k, ek, ea, el, eg): - match k: - case True{}: - {==} - case False{}: - frame.sq(c, rest, Char.is_eq(c, '\''), {==}, ea, el, eg, ek) - -# LAW: a character that is not `&` or `<` stays in the frame -law frame.gt: - for +c: Char - for rest: String - for k: Bool - for ek: {k == Char.is_eq(c, '>') : Bool} - for ea: {False{} == Char.is_eq(c, '&') : Bool} - for el: {False{} == Char.is_eq(c, '<') : Bool} - {Laws.html.framed(Html.one.gt(c, rest, k), Laws.Out{}) - == Laws.html.framed(rest, Laws.Out{}) : Bool} - -def frame.gt(c, rest, k, ek, ea, el): - match k: - case True{}: - {==} - case False{}: - frame.dq(c, rest, Char.is_eq(c, '"'), {==}, ea, el, ek) - -# LAW: a character that is not `&` stays in the frame -law frame.lt: - for +c: Char - for rest: String - for k: Bool - for ek: {k == Char.is_eq(c, '<') : Bool} - for ea: {False{} == Char.is_eq(c, '&') : Bool} - {Laws.html.framed(Html.one.lt(c, rest, k), Laws.Out{}) - == Laws.html.framed(rest, Laws.Out{}) : Bool} - -def frame.lt(c, rest, k, ek, ea): - match k: - case True{}: - {==} - case False{}: - frame.gt(c, rest, Char.is_eq(c, '>'), {==}, ea, ek) - -# LAW: one escaped character stays in the frame -law frame.amp: - for +c: Char - for rest: String - for k: Bool - for ek: {k == Char.is_eq(c, '&') : Bool} - {Laws.html.framed(Html.one.amp(c, rest, k), Laws.Out{}) - == Laws.html.framed(rest, Laws.Out{}) : Bool} - -def frame.amp(c, rest, k, ek): - match k: - case True{}: - {==} - case False{}: - frame.lt(c, rest, Char.is_eq(c, '<'), {==}, ek) - -# LAW: an escaped string stays in the frame, whatever follows it -law frame.onto: - for s: String - for +rest: String - {Laws.html.framed(Html.escape.onto(s, rest), Laws.Out{}) - == Laws.html.framed(rest, Laws.Out{}) : Bool} - -def frame.onto(s, rest): - match s: - case SNil{}: - {==} - case SCon{+c, +t}: - Equal.trans(Bool, - Laws.html.framed(Html.one(c, Html.escape.onto(t, rest)), Laws.Out{}), - Laws.html.framed(Html.escape.onto(t, rest), Laws.Out{}), - Laws.html.framed(rest, Laws.Out{}), - frame.amp(c, Html.escape.onto(t, rest), Char.is_eq(c, '&'), {==}), - frame.onto(t, rest)) - -def Laws.html_frame(s): - frame.onto(s, "") - -# --- reason-phrases (SPEC.md HTTP-SRV-5) --- - -def Laws.reason_phrases(_code): - {==} - -def Laws.reason_in_nil(_code): - {==} - -def Laws.reason_in_hit(code, _c, text, t, e): - %e : {Http.reason.pick(text, _, _u => Http.reason.in(t, code)) == text - : String} - {==} - -def Laws.reason_in_miss(code, _c, text, t, e): - %e : {Http.reason.pick(text, _, _u => Http.reason.in(t, code)) - == Http.reason.in(t, code) : String} - {==} - -# --- the client's requests (SPEC.md HTTP-CLI-4 to 6) --- - -def Laws.host_http_default(_host): - {==} - -def Laws.host_https_default(_host): - {==} - -def Laws.host_http_other(host, port, e): - %e : {Http.authority.of(host, port, _) == host ++ ":" ++ U32.show(port) - : String} - {==} - -def Laws.host_https_other(host, port, e): - %e : {Http.authority.of(host, port, _) == host ++ ":" ++ U32.show(port) - : String} - {==} - -def Laws.request_at(_method, _secure, _host, _port, _path, _hs, _body): - {==} - -def Laws.exchange_bad(_method, _why, _hs, _body): - {==} - -def Laws.exchange_loc(_method, _scheme, _host, _port, _path, _hs, _body): - {==} - -# --- the request's header fields (SPEC.md HTTP-CLI-8) --- - -# a string prefix both sides share does not change starts_with -def eq.starts_cancel(+a: String, +x: String, +y: String) - -> {String.starts_with.go(String.append(a, y), String.append(a, x)) - == String.starts_with.go(y, x) : Bool}: - match a: - case SNil{}: - {==} - case SCon{h, t}: - %Equal.sym(Bool, Char.is_eq(h, h), True{}, eq.char_eq_self(h)) - : {String.starts_with.go.if(String.append(t, y), String.append(t, x), _) - == String.starts_with.go(y, x) : Bool} - eq.starts_cancel(t, x, y) - -def Laws.exchange_method(_method, _scheme, _host, _port, _path, _hs, _body): - {==} - -def Laws.request_line(method, secure, host, port, path, hs, body): - +rest = Http.headers.lines( - Http.request.fields(Http.authority(secure, host, port), hs, body)) - ++ "\r\n" ++ body - Equal.trans(Bool, - String.starts_with.go(method ++ " " ++ path ++ " HTTP/1.1\r\n", - method ++ " " ++ path ++ " HTTP/1.1\r\n" ++ rest), - String.starts_with.go(" " ++ path ++ " HTTP/1.1\r\n", - " " ++ path ++ " HTTP/1.1\r\n" ++ rest), - True{}, - eq.starts_cancel(method, " " ++ path ++ " HTTP/1.1\r\n" ++ rest, - " " ++ path ++ " HTTP/1.1\r\n"), - eq.starts_cancel(path, " HTTP/1.1\r\n" ++ rest, " HTTP/1.1\r\n")) - -def Laws.field_host(_name, e): - %e : {Http.field.of(_) == Http.FHost{} : Http.Field} - {==} - -def Laws.field_agent(_name, e): - %e : {Http.field.of(_) == Http.FAgent{} : Http.Field} - {==} - -def Laws.field_accept(_name, e): - %e : {Http.field.of(_) == Http.FAccept{} : Http.Field} - {==} - -def Laws.field_conn(_name, e): - %e : {Http.field.of(_) == Http.FConn{} : Http.Field} - {==} - -def Laws.field_clen(_name, e): - %e : {Http.field.of(_) == Http.FClen{} : Http.Field} - {==} - -def Laws.field_other(name, eh, ea, ec, en, el): - %eh : {Http.field.of.host(_, String.to_lower(name)) == Http.FOther{} - : Http.Field} - %ea : {Http.field.of.agent(_, String.to_lower(name)) == Http.FOther{} - : Http.Field} - %ec : {Http.field.of.accept(_, String.to_lower(name)) == Http.FOther{} - : Http.Field} - %en : {Http.field.of.conn(_, String.to_lower(name)) == Http.FOther{} - : Http.Field} - %el : {Http.field.of.clen(_) == Http.FOther{} : Http.Field} - {==} - -def Laws.request_text(_method, _host, _path, _hs, _body): - {==} - -# two fields field.same calls the same are the same -def hd.same_sound(+a: Http.Field, +b: Http.Field, - +e: {True{} == Http.field.same(a, b) : Bool}) -> {a == b : Http.Field}: - match a: - case Http.FHost{}: - match b: - case Http.FHost{}: - {==} - case Http.FAgent{}: - eq.bit.absurd(&2, {Http.FHost{} == Http.FAgent{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAccept{}: - eq.bit.absurd(&2, {Http.FHost{} == Http.FAccept{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FConn{}: - eq.bit.absurd(&2, {Http.FHost{} == Http.FConn{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FClen{}: - eq.bit.absurd(&2, {Http.FHost{} == Http.FClen{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FOther{}: - eq.bit.absurd(&2, {Http.FHost{} == Http.FOther{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAgent{}: - match b: - case Http.FHost{}: - eq.bit.absurd(&2, {Http.FAgent{} == Http.FHost{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAgent{}: - {==} - case Http.FAccept{}: - eq.bit.absurd(&2, {Http.FAgent{} == Http.FAccept{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FConn{}: - eq.bit.absurd(&2, {Http.FAgent{} == Http.FConn{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FClen{}: - eq.bit.absurd(&2, {Http.FAgent{} == Http.FClen{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FOther{}: - eq.bit.absurd(&2, {Http.FAgent{} == Http.FOther{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAccept{}: - match b: - case Http.FHost{}: - eq.bit.absurd(&2, {Http.FAccept{} == Http.FHost{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAgent{}: - eq.bit.absurd(&2, {Http.FAccept{} == Http.FAgent{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAccept{}: - {==} - case Http.FConn{}: - eq.bit.absurd(&2, {Http.FAccept{} == Http.FConn{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FClen{}: - eq.bit.absurd(&2, {Http.FAccept{} == Http.FClen{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FOther{}: - eq.bit.absurd(&2, {Http.FAccept{} == Http.FOther{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FConn{}: - match b: - case Http.FHost{}: - eq.bit.absurd(&2, {Http.FConn{} == Http.FHost{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAgent{}: - eq.bit.absurd(&2, {Http.FConn{} == Http.FAgent{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAccept{}: - eq.bit.absurd(&2, {Http.FConn{} == Http.FAccept{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FConn{}: - {==} - case Http.FClen{}: - eq.bit.absurd(&2, {Http.FConn{} == Http.FClen{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FOther{}: - eq.bit.absurd(&2, {Http.FConn{} == Http.FOther{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FClen{}: - match b: - case Http.FHost{}: - eq.bit.absurd(&2, {Http.FClen{} == Http.FHost{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAgent{}: - eq.bit.absurd(&2, {Http.FClen{} == Http.FAgent{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAccept{}: - eq.bit.absurd(&2, {Http.FClen{} == Http.FAccept{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FConn{}: - eq.bit.absurd(&2, {Http.FClen{} == Http.FConn{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FClen{}: - {==} - case Http.FOther{}: - eq.bit.absurd(&2, {Http.FClen{} == Http.FOther{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FOther{}: - match b: - case Http.FHost{}: - eq.bit.absurd(&2, {Http.FOther{} == Http.FHost{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAgent{}: - eq.bit.absurd(&2, {Http.FOther{} == Http.FAgent{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FAccept{}: - eq.bit.absurd(&2, {Http.FOther{} == Http.FAccept{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FConn{}: - eq.bit.absurd(&2, {Http.FOther{} == Http.FConn{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FClen{}: - eq.bit.absurd(&2, {Http.FOther{} == Http.FClen{} : Http.Field}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.FOther{}: - {==} - -# a list's tail after an empty append is the list -def hd.app_nil(+xs: List<&2, - Http.Header>) -> {List.append(&2, Http.Header, xs, []) == xs : List<&2, - Http.Header>}: - match xs: - case []: - {==} - case h <> t: - Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, x => h <> x, - List.append(&2, Http.Header, t, []), t, - hd.app_nil(t)) - -# one step of of_app: the head kept or not -def hd.of_app.put(+f: Http.Field, +k: Bool, +h: Http.Header, +t: List<&2, - Http.Header>, +b: List<&2, Http.Header>, - +ih: {Http.fields.of(f, List.append(&2, Http.Header, t, b)) - == List.append(&2, Http.Header, Http.fields.of(f, t), - Http.fields.of(f, b)) : List<&2, Http.Header>}) - -> {Http.fields.put(k, h, - Http.fields.of(f, List.append(&2, Http.Header, t, b))) - == List.append(&2, Http.Header, - Http.fields.put(k, h, Http.fields.of(f, t)), - Http.fields.of(f, b)) : List<&2, Http.Header>}: - match k: - case True{}: - Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, x => h <> x, - Http.fields.of(f, List.append(&2, Http.Header, t, b)), - List.append(&2, Http.Header, Http.fields.of(f, t), - Http.fields.of(f, b)), ih) - case False{}: - ih - -# the fields of a name in an append are the ones in each part -def hd.of_app(+f: Http.Field, +a: List<&2, Http.Header>, +b: List<&2, - Http.Header>) - -> {Http.fields.of(f, List.append(&2, Http.Header, a, b)) - == List.append(&2, Http.Header, Http.fields.of(f, a), - Http.fields.of(f, b)) : List<&2, Http.Header>}: - match a: - case []: - {==} - case h <> t: - hd.of_app.put(f, Http.field.same(f, Http.header.field(h)), h, t, b, - hd.of_app(f, t, b)) - -# of_app, the other way round -def hd.of_app.r(+f: Http.Field, +a: List<&2, Http.Header>, +b: List<&2, - Http.Header>) - -> {List.append(&2, Http.Header, Http.fields.of(f, a), Http.fields.of(f, b)) - == Http.fields.of(f, List.append(&2, Http.Header, a, b)) : List<&2, - Http.Header>}: - Equal.sym(List<&2, Http.Header>, - Http.fields.of(f, List.append(&2, Http.Header, a, b)), - List.append(&2, Http.Header, Http.fields.of(f, a), Http.fields.of(f, b)), - hd.of_app(f, a, b)) - -# one step of idem -def hd.idem.put(+f: Http.Field, +k: Bool, +h: Http.Header, +t: List<&2, - Http.Header>, - +ek: {k == Http.field.same(f, Http.header.field(h)) : Bool}, - +ih: {Http.fields.of(f, Http.fields.of(f, t)) - == Http.fields.of(f, t) : List<&2, Http.Header>}) - -> {Http.fields.of(f, Http.fields.put(k, h, Http.fields.of(f, t))) - == Http.fields.put(k, h, Http.fields.of(f, t)) : List<&2, Http.Header>}: - match k: - case True{}: - %ek : {Http.fields.put(_, h, Http.fields.of(f, Http.fields.of(f, t))) - == h <> Http.fields.of(f, t) : List<&2, Http.Header>} - Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, x => h <> x, - Http.fields.of(f, Http.fields.of(f, t)), Http.fields.of(f, t), - ih) - case False{}: - ih - -# the fields of a name among the fields of that name are all of them -def hd.idem(+f: Http.Field, +hs: List<&2, Http.Header>) - -> {Http.fields.of(f, Http.fields.of(f, hs)) - == Http.fields.of(f, hs) : List<&2, Http.Header>}: - match hs: - case []: - {==} - case h <> t: - hd.idem.put(f, Http.field.same(f, Http.header.field(h)), h, t, {==}, - hd.idem(f, t)) - -# one step of disj -def hd.disj.put(+f: Http.Field, +g: Http.Field, - +efg: {False{} == Http.field.same(f, g) : Bool}, +k: Bool, +h: Http.Header, - +t: List<&2, Http.Header>, - +ek: {k == Http.field.same(g, Http.header.field(h)) : Bool}, - +ih: {Http.fields.of(f, Http.fields.of(g, t)) == [] : List<&2, Http.Header>}) - -> {Http.fields.of(f, Http.fields.put(k, h, Http.fields.of(g, t))) - == [] : List<&2, Http.Header>}: - match k: - case True{}: - %Equal.trans(Bool, False{}, Http.field.same(f, g), - Http.field.same(f, Http.header.field(h)), efg, - Equal.cong(Http.Field, Bool, y => Http.field.same(f, y), g, - Http.header.field(h), hd.same_sound(g, Http.header.field(h), ek))) - : {Http.fields.put(_, h, Http.fields.of(f, Http.fields.of(g, t))) - == [] : List<&2, Http.Header>} - ih - case False{}: - ih - -# the fields of one name among the fields of another are none -def hd.disj(+f: Http.Field, +g: Http.Field, - +efg: {False{} == Http.field.same(f, g) : Bool}, +hs: List<&2, Http.Header>) - -> {Http.fields.of(f, Http.fields.of(g, hs)) == [] : List<&2, Http.Header>}: - match hs: - case []: - {==} - case h <> t: - hd.disj.put(f, g, efg, Http.field.same(g, Http.header.field(h)), h, t, - {==}, hd.disj(f, g, efg, t)) - -# a slot holds none of a name when neither the caller's list nor the default -# does -def hd.or_gone(+f: Http.Field, +given: List<&2, Http.Header>, +d: List<&2, - Http.Header>, - +eg: {Http.fields.of(f, given) == [] : List<&2, Http.Header>}, - +ed: {Http.fields.of(f, d) == [] : List<&2, Http.Header>}) - -> {[] == Http.fields.of(f, Http.fields.or(given, d)) : List<&2, - Http.Header>}: - match given: - case []: - Equal.sym(List<&2, Http.Header>, Http.fields.of(f, d), [], ed) - case h <> t: - Equal.sym(List<&2, Http.Header>, Http.fields.of(f, h <> t), [], eg) - -# a slot holds itself when the caller's list and the default are all of a -# name -def hd.or_keep(+f: Http.Field, +given: List<&2, Http.Header>, +d: List<&2, - Http.Header>, - +eg: {Http.fields.of(f, given) == given : List<&2, Http.Header>}, - +ed: {Http.fields.of(f, d) == d : List<&2, Http.Header>}) - -> {Http.fields.or(given, d) - == Http.fields.of(f, Http.fields.or(given, d)) : List<&2, Http.Header>}: - match given: - case []: - Equal.sym(List<&2, Http.Header>, Http.fields.of(f, d), d, ed) - case h <> t: - Equal.sym(List<&2, Http.Header>, Http.fields.of(f, h <> t), h <> t, eg) - -# a computed Content-Length is no other field -def hd.clen_gone(+f: Http.Field, - +efc: {False{} == Http.field.same(f, Http.FClen{}) : Bool}, +n: Nat, - +e: Bool) -> {[] == Http.fields.of(f, Http.clen.field(n, e)) : List<&2, - Http.Header>}: - match e: - case True{}: - {==} - case False{}: - %efc : {[] - == Http.fields.put(_, Http.H{"Content-Length", Nat.show(n)}, []) - : List<&2, Http.Header>} - {==} - -# a computed Content-Length is a Content-Length field -def hd.clen_self(+n: Nat, +e: Bool) - -> {Http.clen.field(n, e) - == Http.fields.of(Http.FClen{}, Http.clen.field(n, e)) : List<&2, - Http.Header>}: - match e: - case True{}: - {==} - case False{}: - {==} - -# the fields of a name in a request's fields, slot by slot -def hd.of_fields(+f: Http.Field, +s1: List<&2, Http.Header>, +s2: List<&2, - Http.Header>, +s3: List<&2, Http.Header>, +o: List<&2, Http.Header>, - +c: List<&2, Http.Header>, - +r1: List<&2, Http.Header>, +r2: List<&2, Http.Header>, +r3: List<&2, - Http.Header>, +ro: List<&2, Http.Header>, +rc: List<&2, Http.Header>, - +e1: {r1 == Http.fields.of(f, s1) : List<&2, Http.Header>}, - +e2: {r2 == Http.fields.of(f, s2) : List<&2, Http.Header>}, - +e3: {r3 == Http.fields.of(f, s3) : List<&2, Http.Header>}, - +eo: {ro == Http.fields.of(f, o) : List<&2, Http.Header>}, - +ec: {rc == Http.fields.of(f, c) : List<&2, Http.Header>}) - -> {Http.fields.of(f, - List.append(&2, Http.Header, s1, - List.append(&2, Http.Header, s2, - List.append(&2, Http.Header, s3, Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, o, c))))) - == List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>}: - %hd.of_app.r(f, s1, - List.append(&2, Http.Header, s2, - List.append(&2, Http.Header, s3, Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, o, c)))) : {_ - == List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} - %hd.of_app.r(f, s2, - List.append(&2, Http.Header, s3, Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, o, c))) : {List.append(&2, Http.Header, - Http.fields.of(f, s1), _) - == List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} - %hd.of_app.r(f, s3, Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, o, c)) - : {List.append(&2, Http.Header, Http.fields.of(f, s1), - List.append(&2, Http.Header, Http.fields.of(f, s2), _)) - == List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} - %hd.of_app.r(f, o, c) - : {List.append(&2, Http.Header, Http.fields.of(f, s1), - List.append(&2, Http.Header, Http.fields.of(f, s2), - List.append(&2, Http.Header, Http.fields.of(f, s3), - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, _)))) - == List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} - %e1 : {List.append(&2, Http.Header, _, - List.append(&2, Http.Header, Http.fields.of(f, s2), - List.append(&2, Http.Header, Http.fields.of(f, s3), - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, Http.fields.of(f, o), Http.fields.of(f, c)))))) - == List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} - %e2 : {List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, _, - List.append(&2, Http.Header, Http.fields.of(f, s3), - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, Http.fields.of(f, o), Http.fields.of(f, c)))))) - == List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} - %e3 : {List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, _, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, Http.fields.of(f, o), Http.fields.of(f, c)))))) - == List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} - %eo : {List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, _, Http.fields.of(f, c)))))) - == List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} - %ec : {List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, List.append(&2, Http.Header, ro, _))))) - == List.append(&2, Http.Header, r1, - List.append(&2, Http.Header, r2, - List.append(&2, Http.Header, r3, - Http.fields.put(Http.field.same(f, - Http.header.field(Http.H{"Connection", "close"})), - Http.H{"Connection", "close"}, - List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} - {==} - -def Laws.fields_host_caller(host, hs, body, h, t, e): - Equal.trans(List<&2, Http.Header>, - Http.fields.of(Http.FHost{}, Http.request.fields(host, hs, body)), - List.append(&2, Http.Header, h <> t, []), h <> t, - hd.of_fields(Http.FHost{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), - h <> t, [], [], [], [], - Equal.trans(List<&2, Http.Header>, h <> t, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}]), - Http.fields.of(Http.FHost{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}])), - Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, - x => Http.fields.or(x, [Http.H{"Host", host}]), h <> t, - Http.fields.of(Http.FHost{}, hs), - Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FHost{}, hs), - h <> t, e)), - hd.or_keep(Http.FHost{}, Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}], hd.idem(Http.FHost{}, hs), {==})), - hd.or_gone(Http.FHost{}, Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}], - hd.disj(Http.FHost{}, Http.FAgent{}, {==}, hs), {==}), - hd.or_gone(Http.FHost{}, Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}], - hd.disj(Http.FHost{}, Http.FAccept{}, {==}, hs), {==}), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FHost{}, Http.fields.of(Http.FOther{}, hs)), [], - hd.disj(Http.FHost{}, Http.FOther{}, {==}, hs)), - hd.clen_gone(Http.FHost{}, {==}, Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), - hd.app_nil(h <> t)) - -def Laws.fields_host_dflt(host, hs, body, e): - hd.of_fields(Http.FHost{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), - [Http.H{"Host", host}], [], [], [], [], - Equal.trans(List<&2, Http.Header>, [Http.H{"Host", host}], - Http.fields.or(Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}]), - Http.fields.of(Http.FHost{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}])), - Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, - x => Http.fields.or(x, [Http.H{"Host", host}]), [], - Http.fields.of(Http.FHost{}, hs), - Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FHost{}, hs), - [], e)), - hd.or_keep(Http.FHost{}, Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}], hd.idem(Http.FHost{}, hs), {==})), - hd.or_gone(Http.FHost{}, Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}], - hd.disj(Http.FHost{}, Http.FAgent{}, {==}, hs), {==}), - hd.or_gone(Http.FHost{}, Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}], - hd.disj(Http.FHost{}, Http.FAccept{}, {==}, hs), {==}), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FHost{}, Http.fields.of(Http.FOther{}, hs)), [], - hd.disj(Http.FHost{}, Http.FOther{}, {==}, hs)), - hd.clen_gone(Http.FHost{}, {==}, Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))) - -def Laws.fields_agent_caller(host, hs, body, h, t, e): - Equal.trans(List<&2, Http.Header>, - Http.fields.of(Http.FAgent{}, Http.request.fields(host, hs, body)), - List.append(&2, Http.Header, h <> t, []), h <> t, - hd.of_fields(Http.FAgent{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), - [], h <> t, [], [], [], - hd.or_gone(Http.FAgent{}, Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}], - hd.disj(Http.FAgent{}, Http.FHost{}, {==}, hs), {==}), - Equal.trans(List<&2, Http.Header>, h <> t, - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.of(Http.FAgent{}, - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}])), - Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, - x => Http.fields.or(x, [Http.H{"User-Agent", "ezhttp"}]), h <> t, - Http.fields.of(Http.FAgent{}, hs), - Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FAgent{}, hs), - h <> t, e)), - hd.or_keep(Http.FAgent{}, Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}], hd.idem(Http.FAgent{}, hs), {==})), - hd.or_gone(Http.FAgent{}, Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}], - hd.disj(Http.FAgent{}, Http.FAccept{}, {==}, hs), {==}), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FAgent{}, Http.fields.of(Http.FOther{}, hs)), [], - hd.disj(Http.FAgent{}, Http.FOther{}, {==}, hs)), - hd.clen_gone(Http.FAgent{}, {==}, Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), - hd.app_nil(h <> t)) - -def Laws.fields_agent_dflt(host, hs, body, e): - hd.of_fields(Http.FAgent{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), - [], [Http.H{"User-Agent", "ezhttp"}], [], [], [], - hd.or_gone(Http.FAgent{}, Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}], - hd.disj(Http.FAgent{}, Http.FHost{}, {==}, hs), {==}), - Equal.trans(List<&2, Http.Header>, [Http.H{"User-Agent", "ezhttp"}], - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.of(Http.FAgent{}, - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}])), - Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, - x => Http.fields.or(x, [Http.H{"User-Agent", "ezhttp"}]), [], - Http.fields.of(Http.FAgent{}, hs), - Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FAgent{}, hs), - [], e)), - hd.or_keep(Http.FAgent{}, Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}], hd.idem(Http.FAgent{}, hs), {==})), - hd.or_gone(Http.FAgent{}, Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}], - hd.disj(Http.FAgent{}, Http.FAccept{}, {==}, hs), {==}), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FAgent{}, Http.fields.of(Http.FOther{}, hs)), [], - hd.disj(Http.FAgent{}, Http.FOther{}, {==}, hs)), - hd.clen_gone(Http.FAgent{}, {==}, Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))) - -def Laws.fields_accept_caller(host, hs, body, h, t, e): - Equal.trans(List<&2, Http.Header>, - Http.fields.of(Http.FAccept{}, Http.request.fields(host, hs, body)), - List.append(&2, Http.Header, h <> t, []), h <> t, - hd.of_fields(Http.FAccept{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), - [], [], h <> t, [], [], - hd.or_gone(Http.FAccept{}, Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}], - hd.disj(Http.FAccept{}, Http.FHost{}, {==}, hs), {==}), - hd.or_gone(Http.FAccept{}, Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}], - hd.disj(Http.FAccept{}, Http.FAgent{}, {==}, hs), {==}), - Equal.trans(List<&2, Http.Header>, h <> t, - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FAccept{}, - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}])), - Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, - x => Http.fields.or(x, [Http.H{"Accept", "*/*"}]), h <> t, - Http.fields.of(Http.FAccept{}, hs), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FAccept{}, hs), h <> t, e)), - hd.or_keep(Http.FAccept{}, Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}], hd.idem(Http.FAccept{}, hs), {==})), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FAccept{}, Http.fields.of(Http.FOther{}, hs)), [], - hd.disj(Http.FAccept{}, Http.FOther{}, {==}, hs)), - hd.clen_gone(Http.FAccept{}, {==}, Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), - hd.app_nil(h <> t)) - -def Laws.fields_accept_dflt(host, hs, body, e): - hd.of_fields(Http.FAccept{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), - [], [], [Http.H{"Accept", "*/*"}], [], [], - hd.or_gone(Http.FAccept{}, Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}], - hd.disj(Http.FAccept{}, Http.FHost{}, {==}, hs), {==}), - hd.or_gone(Http.FAccept{}, Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}], - hd.disj(Http.FAccept{}, Http.FAgent{}, {==}, hs), {==}), - Equal.trans(List<&2, Http.Header>, [Http.H{"Accept", "*/*"}], - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FAccept{}, - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}])), - Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, - x => Http.fields.or(x, [Http.H{"Accept", "*/*"}]), [], - Http.fields.of(Http.FAccept{}, hs), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FAccept{}, hs), [], e)), - hd.or_keep(Http.FAccept{}, Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}], hd.idem(Http.FAccept{}, hs), {==})), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FAccept{}, Http.fields.of(Http.FOther{}, hs)), [], - hd.disj(Http.FAccept{}, Http.FOther{}, {==}, hs)), - hd.clen_gone(Http.FAccept{}, {==}, Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))) - -def Laws.fields_conn(host, hs, body): - hd.of_fields(Http.FConn{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), - [], [], [], [], [], - hd.or_gone(Http.FConn{}, Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}], - hd.disj(Http.FConn{}, Http.FHost{}, {==}, hs), {==}), - hd.or_gone(Http.FConn{}, Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}], - hd.disj(Http.FConn{}, Http.FAgent{}, {==}, hs), {==}), - hd.or_gone(Http.FConn{}, Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}], - hd.disj(Http.FConn{}, Http.FAccept{}, {==}, hs), {==}), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FConn{}, Http.fields.of(Http.FOther{}, hs)), [], - hd.disj(Http.FConn{}, Http.FOther{}, {==}, hs)), - hd.clen_gone(Http.FConn{}, {==}, Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))) - -def Laws.fields_clen(host, hs, body): - hd.of_fields(Http.FClen{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), - [], [], [], [], - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)), - hd.or_gone(Http.FClen{}, Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}], - hd.disj(Http.FClen{}, Http.FHost{}, {==}, hs), {==}), - hd.or_gone(Http.FClen{}, Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}], - hd.disj(Http.FClen{}, Http.FAgent{}, {==}, hs), {==}), - hd.or_gone(Http.FClen{}, Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}], - hd.disj(Http.FClen{}, Http.FAccept{}, {==}, hs), {==}), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FClen{}, Http.fields.of(Http.FOther{}, hs)), [], - hd.disj(Http.FClen{}, Http.FOther{}, {==}, hs)), - hd.clen_self(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n))) - -def Laws.fields_other(host, hs, body): - Equal.trans(List<&2, Http.Header>, - Http.fields.of(Http.FOther{}, Http.request.fields(host, hs, body)), - List.append(&2, Http.Header, Http.fields.of(Http.FOther{}, hs), []), - Http.fields.of(Http.FOther{}, hs), - hd.of_fields(Http.FOther{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), - [], [], [], Http.fields.of(Http.FOther{}, hs), [], - hd.or_gone(Http.FOther{}, Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}], - hd.disj(Http.FOther{}, Http.FHost{}, {==}, hs), {==}), - hd.or_gone(Http.FOther{}, Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}], - hd.disj(Http.FOther{}, Http.FAgent{}, {==}, hs), {==}), - hd.or_gone(Http.FOther{}, Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}], - hd.disj(Http.FOther{}, Http.FAccept{}, {==}, hs), {==}), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FOther{}, Http.fields.of(Http.FOther{}, hs)), - Http.fields.of(Http.FOther{}, hs), - hd.idem(Http.FOther{}, hs)), - hd.clen_gone(Http.FOther{}, {==}, Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), - hd.app_nil(Http.fields.of(Http.FOther{}, hs))) - -def Laws.follows_get(follow): - match follow: - case True{}: - {==} - case False{}: - {==} - -def Laws.follows_other(_method, e, follow): - match follow: - case True{}: - %e : {_ == False{} : Bool} - {==} - case False{}: - {==} - -# string append is associative -def eq.app_assoc(+a: String, +b: String, +c: String) - -> {String.append(String.append(a, b), c) - == String.append(a, String.append(b, c)) : String}: - match a: - case SNil{}: - {==} - case SCon{h, t}: - Equal.cong(String, String, x => SCon{h, x}, - String.append(String.append(t, b), c), - String.append(t, String.append(b, c)), eq.app_assoc(t, b, c)) - -# the lines of an append are the lines of each part -def hd.lines_app(+a: List<&2, Http.Header>, +b: List<&2, Http.Header>) - -> {Http.headers.lines(List.append(&2, Http.Header, a, b)) - == String.append(Http.headers.lines(a), Http.headers.lines(b)) : String}: - match a: - case []: - {==} - case +h <> +t: - Equal.trans(String, - String.append(Http.header.line(h), - Http.headers.lines(List.append(&2, Http.Header, t, b))), - String.append(Http.header.line(h), - String.append(Http.headers.lines(t), Http.headers.lines(b))), - String.append(String.append(Http.header.line(h), - Http.headers.lines(t)), Http.headers.lines(b)), - Equal.cong(String, String, x => String.append(Http.header.line(h), x), - Http.headers.lines(List.append(&2, Http.Header, t, b)), - String.append(Http.headers.lines(t), Http.headers.lines(b)), - hd.lines_app(t, b)), - Equal.sym(String, - String.append(String.append(Http.header.line(h), - Http.headers.lines(t)), Http.headers.lines(b)), - String.append(Http.header.line(h), - String.append(Http.headers.lines(t), Http.headers.lines(b))), - eq.app_assoc(Http.header.line(h), Http.headers.lines(t), - Http.headers.lines(b)))) - -# with none of Host, User-Agent, Accept from the caller and only other -# fields, the request's fields are the defaults, the caller's, Content-Length -def hd.plain_fields(+host: String, +hs: List<&2, Http.Header>, +body: String, - +eh: {Http.fields.of(Http.FHost{}, hs) == [] : List<&2, Http.Header>}, - +ea: {Http.fields.of(Http.FAgent{}, hs) == [] : List<&2, Http.Header>}, - +ec: {Http.fields.of(Http.FAccept{}, hs) == [] : List<&2, Http.Header>}, - +eo: {Http.fields.of(Http.FOther{}, hs) == hs : List<&2, Http.Header>}) - -> {Http.request.fields(host, hs, body) == Http.H{"Host", host} - <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} - <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))) : List<&2, Http.Header>}: - %Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FHost{}, hs), [], eh) - : {List.append(&2, Http.Header, Http.fields.or(_, [Http.H{"Host", host}]), - List.append(&2, Http.Header, - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - List.append(&2, Http.Header, - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))))) == Http.H{"Host", host} - <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} - <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))) : List<&2, Http.Header>} - %Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FAgent{}, hs), [], ea) - : {List.append(&2, Http.Header, Http.fields.or([], [Http.H{"Host", host}]), - List.append(&2, Http.Header, - Http.fields.or(_, [Http.H{"User-Agent", "ezhttp"}]), - List.append(&2, Http.Header, - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))))) == Http.H{"Host", host} - <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} - <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))) : List<&2, Http.Header>} - %Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FAccept{}, hs), [], ec) - : {List.append(&2, Http.Header, Http.fields.or([], [Http.H{"Host", host}]), - List.append(&2, Http.Header, - Http.fields.or([], [Http.H{"User-Agent", "ezhttp"}]), - List.append(&2, Http.Header, - Http.fields.or(_, [Http.H{"Accept", "*/*"}]), - Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, Http.fields.of(Http.FOther{}, hs), - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))))) == Http.H{"Host", host} - <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} - <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))) : List<&2, Http.Header>} - %Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FOther{}, hs), hs, eo) - : {List.append(&2, Http.Header, Http.fields.or([], [Http.H{"Host", host}]), - List.append(&2, Http.Header, - Http.fields.or([], [Http.H{"User-Agent", "ezhttp"}]), - List.append(&2, Http.Header, - Http.fields.or([], [Http.H{"Accept", "*/*"}]), - Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, _, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))))) == Http.H{"Host", host} - <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} - <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))) : List<&2, Http.Header>} - {==} - -# the plain request's text after `Host: ` -def hd.plain_tail(+host: String, +hs: List<&2, Http.Header>, +body: String) - -> {String.append(String.append(String.append(host, "\r\n"), - Http.headers.lines(Http.H{"User-Agent", "ezhttp"} - <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))))), "\r\n" ++ body) - == String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" - ++ "Accept: */*\r\n" ++ "Connection: close\r\n" - ++ String.append(Http.headers.lines(hs), - String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))) : String}: - Equal.trans(String, - String.append(String.append(String.append(host, "\r\n"), - Http.headers.lines(Http.H{"User-Agent", "ezhttp"} - <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))))), "\r\n" ++ body), - String.append(String.append(host, "\r\n"), - String.append(Http.headers.lines(Http.H{"User-Agent", "ezhttp"} - <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body)), - String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" ++ "Accept: */*\r\n" - ++ "Connection: close\r\n" - ++ String.append(Http.headers.lines(hs), - String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))), - eq.app_assoc(String.append(host, "\r\n"), - Http.headers.lines(Http.H{"User-Agent", "ezhttp"} - <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body), - Equal.trans(String, - String.append(String.append(host, "\r\n"), - String.append(Http.headers.lines(Http.H{"User-Agent", "ezhttp"} - <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body)), - String.append(host, - String.append("\r\n", - String.append(Http.headers.lines(Http.H{"User-Agent", "ezhttp"} - <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body))), - String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" - ++ "Accept: */*\r\n" ++ "Connection: close\r\n" - ++ String.append(Http.headers.lines(hs), - String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))), - eq.app_assoc(host, "\r\n", - String.append(Http.headers.lines(Http.H{"User-Agent", "ezhttp"} - <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body)), - Equal.cong(String, String, - x => String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" - ++ "Accept: */*\r\n" ++ "Connection: close\r\n" ++ x), - String.append(Http.headers.lines(List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body), - String.append(Http.headers.lines(hs), - String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body)), - Equal.trans(String, - String.append(Http.headers.lines(List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body), - String.append(String.append(Http.headers.lines(hs), - Http.headers.lines(Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body), - String.append(Http.headers.lines(hs), - String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body)), - Equal.cong(String, String, y => String.append(y, "\r\n" ++ body), - Http.headers.lines(List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))), - String.append(Http.headers.lines(hs), - Http.headers.lines(Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))), - hd.lines_app(hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))), - eq.app_assoc(Http.headers.lines(hs), - Http.headers.lines(Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))))) - -def Laws.request_plain(method, host, path, hs, body, eh, ea, ec, eo): - Equal.trans(String, - Http.request(method, host, path, hs, body), - method ++ " " ++ path ++ " HTTP/1.1\r\n" - ++ Http.headers.lines(Http.H{"Host", host} - <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} - <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n)))) ++ "\r\n" ++ body, - method ++ " " ++ path ++ " HTTP/1.1\r\n" ++ "Host: " - ++ String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" - ++ "Accept: */*\r\n" ++ "Connection: close\r\n" - ++ String.append(Http.headers.lines(hs), - String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))), - Equal.cong(List<&2, Http.Header>, String, - z => method ++ " " ++ path ++ " HTTP/1.1\r\n" ++ Http.headers.lines(z) - ++ "\r\n" ++ body, - Http.request.fields(host, hs, body), Http.H{"Host", host} - <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} - <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), - hd.plain_fields(host, hs, body, eh, ea, ec, eo)), - Equal.cong(String, String, - x => method ++ " " ++ path ++ " HTTP/1.1\r\n" ++ "Host: " ++ x, - String.append(String.append(String.append(host, "\r\n"), - Http.headers.lines(Http.H{"User-Agent", "ezhttp"} - <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} - <> List.append(&2, Http.Header, hs, - Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))))), "\r\n" ++ body), - String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" - ++ "Accept: */*\r\n" ++ "Connection: close\r\n" - ++ String.append(Http.headers.lines(hs), - String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), - Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))), - hd.plain_tail(host, hs, body))) - -def Laws.whole_open(_f, _asked): - {==} - -def Laws.whole_head(f, asked, before, after, e): - %e : {Http.whole.step(asked, String.lines(before), after, _, - cut => Http.whole.go(f, asked, cut)) - == Http.whole.of(Http.frame(Bool.or(asked, Http.status.bare(Http.status.of(String.lines(before)))), - Http.head.go(String.lines(before), "transfer-encoding"), - Http.head.values(String.lines(before), "content-length")), after) : Bool} - {==} - -def Laws.status_of(_ls): - {==} - -def Laws.status_bare_some(_n): - {==} - -def Laws.status_bare_none(_m, e): - %e : {Http.status.bare(_) == False{} : Bool} - {==} - -def Laws.frame_bare(_te, _lens): - {==} - -def Laws.frame_chunked(_te, lens, e): - %e : {Http.frame.te(lens, _) == Http.Chunked{} : Http.Frame} - {==} - -def Laws.frame_sized(_te, lens, e1, e2, n, e3): - %e1 : {Http.frame.te(lens, _) == Http.Sized{n} : Http.Frame} - %e2 : {Http.frame.agree(lens, _) == Http.Sized{n} : Http.Frame} - %e3 : {Http.frame.sized(_) == Http.Sized{n} : Http.Frame} - {==} - -def Laws.frame_disagree(_te, lens, e1, e2): - %e1 : {Http.frame.te(lens, _) == Http.Closed{} : Http.Frame} - %e2 : {Http.frame.agree(lens, _) == Http.Closed{} : Http.Frame} - {==} - -def Laws.frame_unsized(_te, lens, e1, e2, e3): - %e1 : {Http.frame.te(lens, _) == Http.Closed{} : Http.Frame} - %e2 : {Http.frame.agree(lens, _) == Http.Closed{} : Http.Frame} - %e3 : {Http.frame.sized(_) == Http.Closed{} : Http.Frame} - {==} - -# --- counting octets without walking all of them (utf8.has) --- - -law lt_over: - for k: Nat - for w: Nat - for +l: Nat - for e: {True{} == Nat.is_lt(k, w) : Bool} - {Nat.is_lt(Nat.add(w, l), k) == False{} : Bool} - -def lt_over(k, w, l, e): - match k w: - case 0n 0n: - eq.bit.absurd(&2, {Nat.is_lt(Nat.add(0n, l), 0n) == False{} : Bool}, - Equal.sym(Bool, True{}, False{}, e)) - case 0n 1n+p: - {==} - case 1n+q 0n: - eq.bit.absurd(&2, {Nat.is_lt(Nat.add(0n, l), 1n+q) == False{} : Bool}, - Equal.sym(Bool, True{}, False{}, e)) - case 1n+q 1n+p: - lt_over(q, p, l, e) - -law lt_shift: - for k: Nat - for w: Nat - for +l: Nat - for e: {False{} == Nat.is_lt(k, w) : Bool} - {Nat.is_lt(Nat.add(w, l), k) == Nat.is_lt(l, Nat.sub(k, w)) : Bool} - -def lt_shift(k, w, l, e): - match k w: - case 0n 0n: - {==} - case 1n+q 0n: - {==} - case 0n 1n+p: - eq.bit.absurd(&2, {Nat.is_lt(Nat.add(1n+p, l), 0n) - == Nat.is_lt(l, Nat.sub(0n, 1n+p)) : Bool}, e) - case 1n+q 1n+p: - lt_shift(q, p, l, e) - -law has_len.arm: - for n: Nat - for w: Nat - for +l: Nat - for r: Bool - for b: Bool - for e: {b == Nat.is_lt(n, w) : Bool} - for ih: {r == Bool.not(Nat.is_lt(l, Nat.sub(n, w))) : Bool} - {Http.octs.has.step(b, _u => r) == Bool.not(Nat.is_lt(Nat.add(w, l), n)) - : Bool} - -def has_len.arm(n, w, l, r, b, e, ih): - match b: - case True{}: - Equal.cong(Bool, Bool, x => Bool.not(x), False{}, - Nat.is_lt(Nat.add(w, l), n), - Equal.sym(Bool, Nat.is_lt(Nat.add(w, l), n), False{}, - lt_over(n, w, l, e))) - case False{}: - Equal.trans(Bool, r, Bool.not(Nat.is_lt(l, Nat.sub(n, w))), - Bool.not(Nat.is_lt(Nat.add(w, l), n)), ih, - Equal.cong(Bool, Bool, x => Bool.not(x), - Nat.is_lt(l, Nat.sub(n, w)), Nat.is_lt(Nat.add(w, l), n), - Equal.sym(Bool, Nat.is_lt(Nat.add(w, l), n), - Nat.is_lt(l, Nat.sub(n, w)), lt_shift(n, w, l, e)))) - -law has_len: - for s: String - for +n: Nat - {Http.octs.has(s, n) == Bool.not(Nat.is_lt(Http.octs.len(s), n)) : Bool} - -def has_len(s, n): - match s: - case SNil{}: - {==} - case SCon{+h, +t}: - +w = Http.octs.width(Char.to_u32(h)) - has_len.arm(n, w, Http.octs.len(t), Http.octs.has(t, Nat.sub(n, w)), - Nat.is_lt(n, w), {==}, has_len(t, Nat.sub(n, w))) - -def Laws.whole_bare(_body): - {==} - -def Laws.whole_sized(_n, _body): - {==} - -def Laws.whole_chunked(_body): - {==} - -def Laws.whole_closed(_body): - {==} - -def Laws.ended_fuel(_c): - {==} - -def Laws.ended_open(_f): - {==} - -def Laws.ended_bad(f, before, after, e): - %e : {Http.chunks.ended.hex(before, after, _, - cut => Http.chunks.ended(f, cut)) == True{} : Bool} - {==} - -def Laws.ended_last(f, before, after, e1, e2): - %e1 : {Http.chunks.ended.hex(before, after, _, - cut => Http.chunks.ended(f, cut)) == Http.chunks.trailer(after) : Bool} - %e2 : {Http.chunks.ended.size(after, Http.hexlen(before), _, - cut => Http.chunks.ended(f, cut)) == Http.chunks.trailer(after) : Bool} - {==} - -def Laws.ended_next(f, before, after, e1, e2, e3): - %e1 : {Http.chunks.ended.hex(before, after, _, - cut => Http.chunks.ended(f, cut)) == Http.chunks.ended(f, Http.divide(Http.octs.drop(after, - Nat.add(Http.hexlen(before), 2n)), "\r\n")) : Bool} - %e2 : {Http.chunks.ended.size(after, Http.hexlen(before), _, - cut => Http.chunks.ended(f, cut)) == Http.chunks.ended(f, Http.divide(Http.octs.drop(after, - Nat.add(Http.hexlen(before), 2n)), "\r\n")) : Bool} - %e3 : {Http.chunks.ended.data(after, Http.hexlen(before), _, - cut => Http.chunks.ended(f, cut)) == Http.chunks.ended(f, Http.divide(Http.octs.drop(after, - Nat.add(Http.hexlen(before), 2n)), "\r\n")) : Bool} - {==} - -def Laws.ended_short(f, before, after, e1, e2, e3): - %e1 : {Http.chunks.ended.hex(before, after, _, - cut => Http.chunks.ended(f, cut)) == False{} : Bool} - %e2 : {Http.chunks.ended.size(after, Http.hexlen(before), _, - cut => Http.chunks.ended(f, cut)) == False{} : Bool} - %e3 : {Http.chunks.ended.data(after, Http.hexlen(before), _, - cut => Http.chunks.ended(f, cut)) == False{} : Bool} - {==} - -def Laws.trailer_ended(_after): - {==} - -def Laws.chunk_ready(after, n): - Equal.cong(Bool, Bool, - x => Bool.and(x, String.starts_with(Http.octs.drop(after, n), "\r\n")), - Http.octs.has(after, Nat.add(n, 2n)), - Bool.not(Nat.is_lt(Http.octs.len(after), Nat.add(n, 2n))), - has_len(after, Nat.add(n, 2n))) - -def eq.drop_zero(s: String) -> {String.drop(s, 0n) == s : String}: - match s: - case SNil{}: - {==} - case SCon{h, t}: - {==} - -# --- the chunk walk, read by read (SPEC.md HTTP-CLI-12) --- - -# what a skip leaves: the text after the octets it walked off -def sk.text(s: Http.Skip) -> String: - match s: - case Http.Short{o}: - "" - case Http.Over{l, r}: - r - -# whether a text is at least the skipped octets and j more -def sk.has(s: Http.Skip, +j: Nat) -> Bool: - match s: - case Http.Short{o}: - Http.octs.has("", Nat.add(o, j)) - case Http.Over{l, r}: - Http.octs.has(r, Nat.add(l, j)) - -# the text after the skipped octets and j more -def sk.drop(s: Http.Skip, +j: Nat) -> String: - match s: - case Http.Short{o}: - "" - case Http.Over{l, r}: - Http.octs.drop(r, Nat.add(l, j)) - -# a skip carried on into more text -def sk.then(s: Http.Skip, b: String) -> Http.Skip: - match s: - case Http.Short{o}: - Http.octs.skip(b, o) - case Http.Over{l, r}: - Http.Over{l, r ++ b} - -law width_pos: - for +u: U32 - {Nat.is_lt(0n, Http.octs.width(u)) == True{} : Bool} - -def width_pos(_u): - {==} - -law has_zero: - for s: String - {Http.octs.has(s, 0n) == True{} : Bool} - -def has_zero(s): - match s: - case SNil{}: - {==} - case SCon{+h, t}: - +w = Http.octs.width(Char.to_u32(h)) - %Equal.sym(Bool, Nat.is_lt(0n, w), True{}, width_pos(Char.to_u32(h))) - : {Http.octs.has.step(_, _u => Http.octs.has(t, Nat.sub(0n, w))) - == True{} : Bool} - {==} - -law drop_zero8: - for s: String - {Http.octs.drop(s, 0n) == s : String} - -def drop_zero8(s): - match s: - case SNil{}: - {==} - case SCon{+h, +t}: - +w = Http.octs.width(Char.to_u32(h)) - %Equal.sym(Bool, Nat.is_lt(0n, w), True{}, width_pos(Char.to_u32(h))) - : {Http.octs.drop.step(_, h, t, _u => Http.octs.drop(t, Nat.sub(0n, w))) - == SCon{h, t} : String} - {==} - -law lt_grow: - for n: Nat - for w: Nat - for +j: Nat - for e: {False{} == Nat.is_lt(n, w) : Bool} - {Nat.is_lt(Nat.add(n, j), w) == False{} : Bool} - -def lt_grow(n, w, j, e): - match n w: - case 0n 0n: - lt_zero(j) - case 1n+q 0n: - {==} - case 0n 1n+p: - eq.bit.absurd(&2, {Nat.is_lt(Nat.add(0n, j), 1n+p) == False{} : Bool}, e) - case 1n+q 1n+p: - lt_grow(q, p, j, e) - -law sub_grow: - for n: Nat - for w: Nat - for +j: Nat - for e: {False{} == Nat.is_lt(n, w) : Bool} - {Nat.sub(Nat.add(n, j), w) == Nat.add(Nat.sub(n, w), j) : Nat} - -def sub_grow(n, w, j, e): - match n w: - case 0n 0n: - sub_zero(j) - case 1n+q 0n: - {==} - case 0n 1n+p: - eq.bit.absurd(&2, {Nat.sub(Nat.add(0n, j), 1n+p) - == Nat.add(Nat.sub(0n, 1n+p), j) : Nat}, e) - case 1n+q 1n+p: - sub_grow(q, p, j, e) - -law skip_text.arm: - for -h: Char - for -t: String - for -n: Nat - for -w: Nat - for b: Bool - for ih: {Http.octs.drop(t, Nat.sub(n, w)) - == sk.text(Http.octs.skip(t, Nat.sub(n, w))) : String} - {Http.octs.drop.step(b, h, t, _u => Http.octs.drop(t, Nat.sub(n, w))) - == sk.text(Http.octs.skip.step(b, n, h, t, - _u => Http.octs.skip(t, Nat.sub(n, w)))) : String} - -def skip_text.arm(_h, _t, _n, _w, b, ih): - match b: - case True{}: - {==} - case False{}: - ih - -law skip_text: - for s: String - for +n: Nat - {Http.octs.drop(s, n) == sk.text(Http.octs.skip(s, n)) : String} - -def skip_text(s, n): - match s: - case SNil{}: - {==} - case SCon{+h, +t}: - +w = Http.octs.width(Char.to_u32(h)) - skip_text.arm(h, t, n, w, Nat.is_lt(n, w), skip_text(t, Nat.sub(n, w))) - -law skip_has.arm: - for +h: Char - for +t: String - for +n: Nat - for +j: Nat - for b: Bool - for +e: {b == Nat.is_lt(n, Http.octs.width(Char.to_u32(h))) : Bool} - for ih: {Http.octs.has(t, Nat.add(Nat.sub(n, Http.octs.width(Char.to_u32(h))), j)) - == sk.has(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : Bool} - {Http.octs.has(SCon{h, t}, Nat.add(n, j)) - == sk.has(Http.octs.skip.step(b, n, h, t, - _u => Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))))), j) : Bool} - -def skip_has.arm(h, t, n, j, b, e, ih): - match b: - case True{}: - {==} - case False{}: - %Equal.sym(Bool, Nat.is_lt(Nat.add(n, j), Http.octs.width(Char.to_u32(h))), False{}, lt_grow(n, - Http.octs.width(Char.to_u32(h)), j, e)) - : {Http.octs.has.step(_, - _u => Http.octs.has(t, Nat.sub(Nat.add(n, j), Http.octs.width(Char.to_u32(h))))) - == sk.has(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : Bool} - %Equal.sym(Nat, Nat.sub(Nat.add(n, j), Http.octs.width(Char.to_u32(h))), Nat.add(Nat.sub(n, - Http.octs.width(Char.to_u32(h))), j), - sub_grow(n, Http.octs.width(Char.to_u32(h)), j, e)) - : {Http.octs.has(t, _) - == sk.has(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : Bool} - ih - -law skip_has: - for s: String - for +n: Nat - for +j: Nat - {Http.octs.has(s, Nat.add(n, j)) == sk.has(Http.octs.skip(s, n), j) : Bool} - -def skip_has(s, n, j): - match s: - case SNil{}: - {==} - case SCon{+h, +t}: - skip_has.arm(h, t, n, j, Nat.is_lt(n, Http.octs.width(Char.to_u32(h))), {==}, - skip_has(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))), j)) - -law skip_drop.arm: - for +h: Char - for +t: String - for +n: Nat - for +j: Nat - for b: Bool - for +e: {b == Nat.is_lt(n, Http.octs.width(Char.to_u32(h))) : Bool} - for ih: {Http.octs.drop(t, Nat.add(Nat.sub(n, Http.octs.width(Char.to_u32(h))), j)) - == sk.drop(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : String} - {Http.octs.drop(SCon{h, t}, Nat.add(n, j)) - == sk.drop(Http.octs.skip.step(b, n, h, t, - _u => Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))))), j) : String} - -def skip_drop.arm(h, t, n, j, b, e, ih): - match b: - case True{}: - {==} - case False{}: - %Equal.sym(Bool, Nat.is_lt(Nat.add(n, j), Http.octs.width(Char.to_u32(h))), False{}, lt_grow(n, - Http.octs.width(Char.to_u32(h)), j, e)) - : {Http.octs.drop.step(_, h, t, - _u => Http.octs.drop(t, Nat.sub(Nat.add(n, j), Http.octs.width(Char.to_u32(h))))) - == sk.drop(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : String} - %Equal.sym(Nat, Nat.sub(Nat.add(n, j), Http.octs.width(Char.to_u32(h))), Nat.add(Nat.sub(n, - Http.octs.width(Char.to_u32(h))), j), - sub_grow(n, Http.octs.width(Char.to_u32(h)), j, e)) - : {Http.octs.drop(t, _) - == sk.drop(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : String} - ih - -law skip_drop: - for s: String - for +n: Nat - for +j: Nat - {Http.octs.drop(s, Nat.add(n, j)) == sk.drop(Http.octs.skip(s, n), j) - : String} - -def skip_drop(s, n, j): - match s: - case SNil{}: - {==} - case SCon{+h, +t}: - skip_drop.arm(h, t, n, j, Nat.is_lt(n, Http.octs.width(Char.to_u32(h))), {==}, - skip_drop(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))), j)) - -law skip_app.arm: - for +h: Char - for +t: String - for +b: String - for +n: Nat - for c: Bool - for ih: {Http.octs.skip(t ++ b, Nat.sub(n, Http.octs.width(Char.to_u32(h)))) - == sk.then(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), b) : Http.Skip} - {Http.octs.skip.step(c, n, h, t ++ b, - _u => Http.octs.skip(t ++ b, Nat.sub(n, Http.octs.width(Char.to_u32(h))))) - == sk.then(Http.octs.skip.step(c, n, h, t, - _u => Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))))), b) : Http.Skip} - -def skip_app.arm(_h, _t, _b, _n, c, ih): - match c: - case True{}: - {==} - case False{}: - ih - -law skip_app: - for s: String - for +b: String - for +n: Nat - {Http.octs.skip(s ++ b, n) == sk.then(Http.octs.skip(s, n), b) : Http.Skip} - -def skip_app(s, b, n): - match s: - case SNil{}: - {==} - case SCon{+h, +t}: - skip_app.arm(h, t, b, n, Nat.is_lt(n, Http.octs.width(Char.to_u32(h))), skip_app(t, b, Nat.sub(n, - Http.octs.width(Char.to_u32(h))))) - -law has_app.arm: - for +h: Char - for +t: String - for +b: String - for +k: Nat - for c: Bool - for e: {True{} == Http.octs.has.step(c, _u => Http.octs.has(t, Nat.sub(k, Http.octs.width(Char.to_u32(h))))) - : Bool} - for ih: @e2: {True{} == Http.octs.has(t, Nat.sub(k, Http.octs.width(Char.to_u32(h)))) : Bool} -> - {Http.octs.has(t ++ b, Nat.sub(k, Http.octs.width(Char.to_u32(h)))) == True{} : Bool} - {Http.octs.has.step(c, _u => Http.octs.has(t ++ b, Nat.sub(k, Http.octs.width(Char.to_u32(h))))) - == True{} : Bool} - -def has_app.arm(_h, _t, _b, _k, c, e, ih): - match c: - case True{}: - {==} - case False{}: - ih(e) - -law has_app: - for s: String - for +b: String - for +k: Nat - for e: {True{} == Http.octs.has(s, k) : Bool} - {Http.octs.has(s ++ b, k) == True{} : Bool} - -def has_app(s, b, k, e): - match s k: - case SNil{} 0n: - has_zero(b) - case SNil{} 1n+p: - eq.bit.absurd(&2, {Http.octs.has(b, 1n+p) == True{} : Bool}, - Equal.sym(Bool, True{}, False{}, e)) - case SCon{+h, +t} _: - has_app.arm(h, t, b, k, Nat.is_lt(k, Http.octs.width(Char.to_u32(h))), e, - e2 => has_app(t, b, Nat.sub(k, Http.octs.width(Char.to_u32(h))), e2)) - -law drop_app.arm: - for +h: Char - for +t: String - for +b: String - for +k: Nat - for c: Bool - for e: {True{} == Http.octs.has.step(c, _u => Http.octs.has(t, Nat.sub(k, Http.octs.width(Char.to_u32(h))))) - : Bool} - for ih: @e2: {True{} == Http.octs.has(t, Nat.sub(k, Http.octs.width(Char.to_u32(h)))) : Bool} -> - {Http.octs.drop(t ++ b, Nat.sub(k, Http.octs.width(Char.to_u32(h)))) == Http.octs.drop(t, Nat.sub(k, - Http.octs.width(Char.to_u32(h)))) ++ b - : String} - {Http.octs.drop.step(c, h, t ++ b, _u => Http.octs.drop(t ++ b, Nat.sub(k, Http.octs.width(Char.to_u32(h))))) - == Http.octs.drop.step(c, h, t, _u => Http.octs.drop(t, Nat.sub(k, Http.octs.width(Char.to_u32(h))))) ++ b - : String} - -def drop_app.arm(_h, _t, _b, _k, c, e, ih): - match c: - case True{}: - {==} - case False{}: - ih(e) - -law drop_app: - for s: String - for +b: String - for +k: Nat - for e: {True{} == Http.octs.has(s, k) : Bool} - {Http.octs.drop(s ++ b, k) == Http.octs.drop(s, k) ++ b : String} - -def drop_app(s, b, k, e): - match s k: - case SNil{} 0n: - drop_zero8(b) - case SNil{} 1n+p: - eq.bit.absurd(&2, {Http.octs.drop(b, 1n+p) == b : String}, - Equal.sym(Bool, True{}, False{}, e)) - case SCon{+h, +t} _: - drop_app.arm(h, t, b, k, Nat.is_lt(k, Http.octs.width(Char.to_u32(h))), e, - e2 => drop_app(t, b, Nat.sub(k, Http.octs.width(Char.to_u32(h))), e2)) - -law sw_app.arm: - for +xt: String - for +yt: String - for +b: String - for c: Bool - for e: {True{} == String.starts_with.go.if(yt, xt, c) : Bool} - for ih: @e2: {True{} == String.starts_with.go(yt, xt) : Bool} -> - {String.starts_with.go(yt, xt ++ b) == True{} : Bool} - {String.starts_with.go.if(yt, xt ++ b, c) == True{} : Bool} - -def sw_app.arm(_xt, _yt, _b, c, e, ih): - match c: - case True{}: - ih(e) - case False{}: - eq.bit.absurd(&2, {False{} == True{} : Bool}, - Equal.sym(Bool, True{}, False{}, e)) - -law sw_app: - for s: String - for p: String - for +b: String - for e: {True{} == String.starts_with(s, p) : Bool} - {String.starts_with(s ++ b, p) == True{} : Bool} - -def sw_app(s, p, b, e): - match s p: - case _ SNil{}: - {==} - case SNil{} SCon{y, yt}: - eq.bit.absurd(&2, {String.starts_with.go(SCon{y, yt}, b) == True{} : Bool}, - Equal.sym(Bool, True{}, False{}, e)) - case SCon{+x, +xt} SCon{+y, +yt}: - sw_app.arm(xt, yt, b, Char.is_eq(x, y), e, e2 => sw_app(xt, yt, b, e2)) - -law contains_nil: - for s: String - {String.contains(s, "") == True{} : Bool} - -def contains_nil(s): - match s: - case SNil{}: - {==} - case SCon{h, t}: - {==} - -law contains_if: - for +t: String - for +p: String - for c: Bool - for e: {String.contains(t, p) == True{} : Bool} - {String.contains.if(t, p, c) == True{} : Bool} - -def contains_if(_t, _p, c, e): - match c: - case True{}: - {==} - case False{}: - e - -law contains_app.arm: - for +h: Char - for +t: String - for +p: String - for +b: String - for c: Bool - for +ec: {c == String.starts_with(SCon{h, t}, p) : Bool} - for e: {True{} == String.contains.if(t, p, c) : Bool} - for ih: @e2: {True{} == String.contains(t, p) : Bool} -> - {String.contains(t ++ b, p) == True{} : Bool} - {String.contains.if(t ++ b, p, String.starts_with(SCon{h, t ++ b}, p)) - == True{} : Bool} - -def contains_app.arm(h, t, p, b, c, ec, e, ih): - match c: - case True{}: - %Equal.sym(Bool, String.starts_with(SCon{h, t} ++ b, p), True{}, - sw_app(SCon{h, t}, p, b, ec)) - : {String.contains.if(t ++ b, p, _) == True{} : Bool} - {==} - case False{}: - contains_if(t ++ b, p, String.starts_with(SCon{h, t ++ b}, p), ih(e)) - -law contains_app: - for s: String - for +p: String - for +b: String - for e: {True{} == String.contains(s, p) : Bool} - {String.contains(s ++ b, p) == True{} : Bool} - -def contains_app(s, p, b, e): - match s p: - case SNil{} SNil{}: - contains_nil(b) - case SNil{} SCon{y, yt}: - eq.bit.absurd(&2, {String.contains(b, SCon{y, yt}) == True{} : Bool}, - Equal.sym(Bool, True{}, False{}, e)) - case SCon{+h, +t} _: - contains_app.arm(h, t, p, b, String.starts_with(SCon{h, t}, p), {==}, e, - e2 => contains_app(t, p, b, e2)) - -law or_true: - for x: Bool - {Bool.or(x, True{}) == True{} : Bool} - -def or_true(x): - match x: - case True{}: - {==} - case False{}: - {==} - -law or_app: - for x: Bool - for y: Bool - for +x2: Bool - for +y2: Bool - for e: {True{} == Bool.or(x, y) : Bool} - for fx: @ex: {True{} == x : Bool} -> {x2 == True{} : Bool} - for fy: @ey: {True{} == y : Bool} -> {y2 == True{} : Bool} - {Bool.or(x2, y2) == True{} : Bool} - -def or_app(x, _y, x2, y2, e, fx, fy): - match x: - case True{}: - %Equal.sym(Bool, x2, True{}, fx({==})) : {Bool.or(_, y2) == True{} : Bool} - {==} - case False{}: - %Equal.sym(Bool, y2, True{}, fy(e)) - : {Bool.or(x2, _) == True{} : Bool} - or_true(x2) - -law trailer_app: - for +after: String - for +b: String - for e: {True{} == Http.chunks.trailer(after) : Bool} - {Http.chunks.trailer(after ++ b) == True{} : Bool} - -def trailer_app(after, b, e): - or_app(String.starts_with(after, "\r\n"), String.contains(after, "\r\n\r\n"), - String.starts_with(after ++ b, "\r\n"), - String.contains(after ++ b, "\r\n\r\n"), e, - ex => sw_app(after, "\r\n", b, ex), - ey => contains_app(after, "\r\n\r\n", b, ey)) - -# whether a divide found its pattern -def cut.is(c: Http.Cut) -> Bool: - match c: - case Http.NoCut{}: - False{} - case Http.Cut{x, y}: - True{} - -# a divide's second half with more text after it -def cut.app(c: Http.Cut, b: String) -> Http.Cut: - match c: - case Http.NoCut{}: - Http.NoCut{} - case Http.Cut{x, y}: - Http.Cut{x, y ++ b} - -law cut_is_push: - for h: Char - for r: Http.Cut - {cut.is(Http.divide.push(h, r)) == cut.is(r) : Bool} - -def cut_is_push(_h, r): - match r: - case Http.NoCut{}: - {==} - case Http.Cut{x, y}: - {==} - -law cut_app_push: - for h: Char - for r: Http.Cut - for +b: String - {cut.app(Http.divide.push(h, r), b) == Http.divide.push(h, cut.app(r, b)) - : Http.Cut} - -def cut_app_push(_h, r, _b): - match r: - case Http.NoCut{}: - {==} - case Http.Cut{x, y}: - {==} - -law len_app: - for a: String - for +b: String - {String.length(a ++ b) == Nat.add(String.length(a), String.length(b)) : Nat} - -def len_app(a, b): - match a: - case SNil{}: - {==} - case SCon{+h, +t}: - Equal.cong(Nat, Nat, x => 1n+x, String.length(t ++ b), - Nat.add(String.length(t), String.length(b)), len_app(t, b)) - -law lt_succ: - for l: Nat - for p: Nat - for e: {False{} == Nat.is_lt(l, p) : Bool} - {Nat.is_lt(1n+l, p) == False{} : Bool} - -def lt_succ(l, p, e): - match l p: - case _ 0n: - {==} - case 0n 1n+q: - eq.bit.absurd(&2, {Nat.is_lt(1n, 1n+q) == False{} : Bool}, e) - case 1n+k 1n+q: - lt_succ(k, q, e) - -law sw_len.arm: - for +xt: String - for +yt: String - for c: Bool - for e: {True{} == String.starts_with.go.if(yt, xt, c) : Bool} - for ih: @e2: {True{} == String.starts_with.go(yt, xt) : Bool} -> - {Nat.is_lt(String.length(xt), String.length(yt)) == False{} : Bool} - {Nat.is_lt(String.length(xt), String.length(yt)) == False{} : Bool} - -def sw_len.arm(xt, yt, c, e, ih): - match c: - case True{}: - ih(e) - case False{}: - eq.bit.absurd(&2, {Nat.is_lt(String.length(xt), String.length(yt)) - == False{} : Bool}, Equal.sym(Bool, True{}, False{}, e)) - -law sw_len: - for s: String - for p: String - for e: {True{} == String.starts_with(s, p) : Bool} - {Nat.is_lt(String.length(s), String.length(p)) == False{} : Bool} - -def sw_len(s, p, e): - match s p: - case _ SNil{}: - lt_zero(String.length(s)) - case SNil{} SCon{y, yt}: - eq.bit.absurd(&2, {Nat.is_lt(0n, 1n+String.length(yt)) == False{} : Bool}, - Equal.sym(Bool, True{}, False{}, e)) - case SCon{+x, +xt} SCon{+y, +yt}: - sw_len.arm(xt, yt, Char.is_eq(x, y), e, e2 => sw_len(xt, yt, e2)) - -law sw_long.arm: - for +xt: String - for +yt: String - for +b: String - for c: Bool - for ih: {String.starts_with.go(yt, xt ++ b) == String.starts_with.go(yt, xt) - : Bool} - {String.starts_with.go.if(yt, xt ++ b, c) - == String.starts_with.go.if(yt, xt, c) : Bool} - -def sw_long.arm(_xt, _yt, _b, c, ih): - match c: - case True{}: - ih - case False{}: - {==} - -law sw_long: - for s: String - for p: String - for +b: String - for e: {False{} == Nat.is_lt(String.length(s), String.length(p)) : Bool} - {String.starts_with(s ++ b, p) == String.starts_with(s, p) : Bool} - -def sw_long(s, p, b, e): - match s p: - case _ SNil{}: - {==} - case SNil{} SCon{y, yt}: - eq.bit.absurd(&2, {String.starts_with(b, SCon{y, yt}) - == String.starts_with(SNil{}, SCon{y, yt}) : Bool}, e) - case SCon{+x, +xt} SCon{+y, +yt}: - sw_long.arm(xt, yt, b, Char.is_eq(x, y), sw_long(xt, yt, b, e)) - -law sdrop_app.arm: - for +xt: String - for +yt: String - for +b: String - for c: Bool - for e: {True{} == String.starts_with.go.if(yt, xt, c) : Bool} - for ih: @e2: {True{} == String.starts_with.go(yt, xt) : Bool} -> - {String.drop(xt ++ b, String.length(yt)) - == String.drop(xt, String.length(yt)) ++ b : String} - {String.drop(xt ++ b, String.length(yt)) - == String.drop(xt, String.length(yt)) ++ b : String} - -def sdrop_app.arm(xt, yt, b, c, e, ih): - match c: - case True{}: - ih(e) - case False{}: - eq.bit.absurd(&2, {String.drop(xt ++ b, String.length(yt)) - == String.drop(xt, String.length(yt)) ++ b : String}, - Equal.sym(Bool, True{}, False{}, e)) - -law sdrop_app: - for s: String - for p: String - for +b: String - for e: {True{} == String.starts_with(s, p) : Bool} - {String.drop(s ++ b, String.length(p)) == String.drop(s, String.length(p)) ++ b - : String} - -def sdrop_app(s, p, b, e): - match s p: - case SNil{} SNil{}: - eq.drop_zero(b) - case SCon{x, xt} SNil{}: - {==} - case SNil{} SCon{y, yt}: - eq.bit.absurd(&2, {String.drop(b, 1n+String.length(yt)) - == String.drop(SNil{}, 1n+String.length(yt)) ++ b : String}, - Equal.sym(Bool, True{}, False{}, e)) - case SCon{+x, +xt} SCon{+y, +yt}: - sdrop_app.arm(xt, yt, b, Char.is_eq(x, y), e, e2 => sdrop_app(xt, yt, b, e2)) - -law cut_len.arm: - for +h: Char - for +t: String - for +p: String - for c: Bool - for +ec: {c == String.starts_with(SCon{h, t}, p) : Bool} - for e: {True{} == cut.is(Http.divide.step(c, h, t, String.length(p), - _u => Http.divide.go(t, p, String.length(p)))) : Bool} - for ih: @e2: {True{} == cut.is(Http.divide.go(t, p, String.length(p))) : Bool} -> - {Nat.is_lt(String.length(t), String.length(p)) == False{} : Bool} - {Nat.is_lt(1n+String.length(t), String.length(p)) == False{} : Bool} - -def cut_len.arm(h, t, p, c, ec, e, ih): - match c: - case True{}: - sw_len(SCon{h, t}, p, ec) - case False{}: - +ih2 = ih(Equal.trans(Bool, True{}, - cut.is(Http.divide.push(h, Http.divide.go(t, p, String.length(p)))), - cut.is(Http.divide.go(t, p, String.length(p))), e, - cut_is_push(h, Http.divide.go(t, p, String.length(p))))) - lt_succ(String.length(t), String.length(p), - Equal.sym(Bool, Nat.is_lt(String.length(t), String.length(p)), False{}, - ih2)) - -law cut_len: - for s: String - for +p: String - for e: {True{} == cut.is(Http.divide.go(s, p, String.length(p))) : Bool} - {Nat.is_lt(String.length(s), String.length(p)) == False{} : Bool} - -def cut_len(s, p, e): - match s: - case SNil{}: - eq.bit.absurd(&2, {Nat.is_lt(0n, String.length(p)) == False{} : Bool}, - Equal.sym(Bool, True{}, False{}, e)) - case SCon{+h, +t}: - cut_len.arm(h, t, p, String.starts_with(SCon{h, t}, p), {==}, e, - e2 => cut_len(t, p, e2)) - -law divide_app.arm: - for +h: Char - for +t: String - for +p: String - for +b: String - for c: Bool - for +ec: {c == String.starts_with(SCon{h, t}, p) : Bool} - for e: {True{} == cut.is(Http.divide.step(c, h, t, String.length(p), - _u => Http.divide.go(t, p, String.length(p)))) : Bool} - for ih: @e2: {True{} == cut.is(Http.divide.go(t, p, String.length(p))) : Bool} -> - {Http.divide.go(t ++ b, p, String.length(p)) - == cut.app(Http.divide.go(t, p, String.length(p)), b) : Http.Cut} - {Http.divide.go(SCon{h, t ++ b}, p, String.length(p)) - == cut.app(Http.divide.step(c, h, t, String.length(p), - _u => Http.divide.go(t, p, String.length(p))), b) : Http.Cut} - -def divide_app.arm(h, t, p, b, c, ec, e, ih): - match c: - case True{}: - %Equal.sym(Bool, String.starts_with(SCon{h, t} ++ b, p), True{}, - sw_app(SCon{h, t}, p, b, ec)) - : {Http.divide.step(_, h, t ++ b, String.length(p), - _u => Http.divide.go(t ++ b, p, String.length(p))) - == Http.Cut{"", String.drop(SCon{h, t}, String.length(p)) ++ b} - : Http.Cut} - Equal.cong(String, Http.Cut, x => Http.Cut{"", x}, - String.drop(SCon{h, t} ++ b, String.length(p)), - String.drop(SCon{h, t}, String.length(p)) ++ b, - sdrop_app(SCon{h, t}, p, b, ec)) - case False{}: - +e2 = Equal.trans(Bool, True{}, - cut.is(Http.divide.push(h, Http.divide.go(t, p, String.length(p)))), - cut.is(Http.divide.go(t, p, String.length(p))), e, - cut_is_push(h, Http.divide.go(t, p, String.length(p)))) - +long = lt_succ(String.length(t), String.length(p), - Equal.sym(Bool, Nat.is_lt(String.length(t), String.length(p)), False{}, - cut_len(t, p, e2))) - +same = Equal.trans(Bool, String.starts_with(SCon{h, t} ++ b, p), - String.starts_with(SCon{h, t}, p), False{}, - sw_long(SCon{h, t}, p, b, Equal.sym(Bool, - Nat.is_lt(1n+String.length(t), String.length(p)), False{}, long)), - Equal.sym(Bool, False{}, String.starts_with(SCon{h, t}, p), ec)) - %Equal.sym(Bool, String.starts_with(SCon{h, t} ++ b, p), False{}, same) - : {Http.divide.step(_, h, t ++ b, String.length(p), - _u => Http.divide.go(t ++ b, p, String.length(p))) - == cut.app(Http.divide.push(h, Http.divide.go(t, p, String.length(p))), - b) : Http.Cut} - Equal.trans(Http.Cut, - Http.divide.push(h, Http.divide.go(t ++ b, p, String.length(p))), - Http.divide.push(h, cut.app(Http.divide.go(t, p, String.length(p)), b)), - cut.app(Http.divide.push(h, Http.divide.go(t, p, String.length(p))), b), - Equal.cong(Http.Cut, Http.Cut, r => Http.divide.push(h, r), - Http.divide.go(t ++ b, p, String.length(p)), - cut.app(Http.divide.go(t, p, String.length(p)), b), ih(e2)), - Equal.sym(Http.Cut, - cut.app(Http.divide.push(h, Http.divide.go(t, p, String.length(p))), b), - Http.divide.push(h, cut.app(Http.divide.go(t, p, String.length(p)), b)), - cut_app_push(h, Http.divide.go(t, p, String.length(p)), b))) - -law divide_app: - for s: String - for +p: String - for +b: String - for e: {True{} == cut.is(Http.divide.go(s, p, String.length(p))) : Bool} - {Http.divide.go(s ++ b, p, String.length(p)) - == cut.app(Http.divide.go(s, p, String.length(p)), b) : Http.Cut} - -def divide_app(s, p, b, e): - match s: - case SNil{}: - eq.bit.absurd(&2, {Http.divide.go(b, p, String.length(p)) - == cut.app(Http.NoCut{}, b) : Http.Cut}, - Equal.sym(Bool, True{}, False{}, e)) - case SCon{+h, +t}: - divide_app.arm(h, t, p, b, String.starts_with(SCon{h, t}, p), {==}, e, - e2 => divide_app(t, p, b, e2)) - -law lt0_add2: - for o: Nat - {Nat.is_lt(0n, Nat.add(o, 2n)) == True{} : Bool} - -def lt0_add2(o): - match o: - case 0n: - {==} - case 1n+p: - {==} - -law walk_done.crlf: - for +g: Nat - for +l: Nat - for +r: String - for +after: String - for +n: Nat - for rd: Bool - for +ed: {Http.octs.drop(after, Nat.add(n, 2n)) - == Http.octs.drop(r, Nat.add(l, 2n)) : String} - for ih: @s: String -> - {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) - : Bool} - {Http.walk.done(Http.walk.crlf(g, l, r, rd, s => Http.walk(g, s))) - == Http.chunks.ended.data(after, n, rd, cut => Http.chunks.ended(g, cut)) : Bool} - -def walk_done.crlf(g, l, r, after, n, rd, ed, ih): - match rd: - case True{}: - %Equal.sym(String, Http.octs.drop(after, Nat.add(n, 2n)), - Http.octs.drop(r, Nat.add(l, 2n)), ed) - : {Http.walk.done(Http.walk(g, Http.octs.drop(r, Nat.add(l, 2n)))) - == Http.chunks.ended(g, Http.divide(_, "\r\n")) : Bool} - ih(Http.octs.drop(r, Nat.add(l, 2n))) - case False{}: - {==} - -law walk_done.data.arm: - for +g: Nat - for +after: String - for +n: Nat - for sk: Http.Skip - for +eh: {Http.octs.has(after, Nat.add(n, 2n)) == sk.has(sk, 2n) : Bool} - for +et: {Http.octs.drop(after, n) == sk.text(sk) : String} - for +ed: {Http.octs.drop(after, Nat.add(n, 2n)) == sk.drop(sk, 2n) : String} - for ih: @s: String -> - {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) - : Bool} - {Http.walk.done(Http.walk.data(g, sk, s => Http.walk(g, s))) - == Http.chunks.ended.data(after, n, Http.chunks.ready(after, n), cut => Http.chunks.ended(g, cut)) - : Bool} - -def walk_done.data.arm(g, after, n, sk, eh, et, ed, ih): - match sk: - case Http.Short{+o}: - +hf = Equal.trans(Bool, Http.octs.has(after, Nat.add(n, 2n)), - Bool.not(Nat.is_lt(0n, Nat.add(o, 2n))), False{}, eh, - Equal.cong(Bool, Bool, x => Bool.not(x), Nat.is_lt(0n, Nat.add(o, 2n)), - True{}, lt0_add2(o))) - %Equal.sym(Bool, Http.octs.has(after, Nat.add(n, 2n)), False{}, hf) - : {False{} == Http.chunks.ended.data(after, n, - Bool.and(_, String.starts_with(Http.octs.drop(after, n), "\r\n")), - cut => Http.chunks.ended(g, cut)) : Bool} - {==} - case Http.Over{+l, +r}: - %Equal.sym(Bool, Http.octs.has(after, Nat.add(n, 2n)), - Http.octs.has(r, Nat.add(l, 2n)), eh) - : {Http.walk.done(Http.walk.crlf(g, l, r, Bool.and( - Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(r, "\r\n")), s => Http.walk(g, s))) - == Http.chunks.ended.data(after, n, - Bool.and(_, String.starts_with(Http.octs.drop(after, n), "\r\n")), - cut => Http.chunks.ended(g, cut)) : Bool} - %Equal.sym(String, Http.octs.drop(after, n), r, et) - : {Http.walk.done(Http.walk.crlf(g, l, r, Bool.and( - Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(r, "\r\n")), s => Http.walk(g, s))) - == Http.chunks.ended.data(after, n, - Bool.and(Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(_, "\r\n")), - cut => Http.chunks.ended(g, cut)) : Bool} - walk_done.crlf(g, l, r, after, n, Bool.and(Http.octs.has(r, Nat.add(l, 2n)), - String.starts_with(r, "\r\n")), ed, ih) - -law walk_done.data: - for +g: Nat - for +after: String - for +n: Nat - for sk: Http.Skip - for +esk: {sk == Http.octs.skip(after, n) : Http.Skip} - for ih: @s: String -> - {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) - : Bool} - {Http.walk.done(Http.walk.data(g, sk, s => Http.walk(g, s))) - == Http.chunks.ended.data(after, n, Http.chunks.ready(after, n), cut => Http.chunks.ended(g, cut)) - : Bool} - -def walk_done.data(g, after, n, sk, esk, ih): - +eh = Equal.trans(Bool, Http.octs.has(after, Nat.add(n, 2n)), - sk.has(Http.octs.skip(after, n), 2n), sk.has(sk, 2n), - skip_has(after, n, 2n), - Equal.cong(Http.Skip, Bool, x => sk.has(x, 2n), Http.octs.skip(after, n), sk, - Equal.sym(Http.Skip, sk, Http.octs.skip(after, n), esk))) - +et = Equal.trans(String, Http.octs.drop(after, n), - sk.text(Http.octs.skip(after, n)), sk.text(sk), - skip_text(after, n), - Equal.cong(Http.Skip, String, x => sk.text(x), Http.octs.skip(after, n), sk, - Equal.sym(Http.Skip, sk, Http.octs.skip(after, n), esk))) - +ed = Equal.trans(String, Http.octs.drop(after, Nat.add(n, 2n)), - sk.drop(Http.octs.skip(after, n), 2n), sk.drop(sk, 2n), - skip_drop(after, n, 2n), - Equal.cong(Http.Skip, String, x => sk.drop(x, 2n), Http.octs.skip(after, n), sk, - Equal.sym(Http.Skip, sk, Http.octs.skip(after, n), esk))) - walk_done.data.arm(g, after, n, sk, eh, et, ed, ih) - -law walk_done.trail: - for g: Nat - for rest: String - for t: Bool - {Http.walk.done(Http.walk.trail(g, rest, t)) == t : Bool} - -def walk_done.trail(_g, _rest, t): - match t: - case True{}: - {==} - case False{}: - {==} - -law walk_done.size: - for +g: Nat - for rest: String - for +after: String - for +n: Nat - for z: Bool - for ih: @s: String -> - {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) - : Bool} - {Http.walk.done(Http.walk.size(g, rest, after, n, z, s => Http.walk(g, s))) - == Http.chunks.ended.size(after, n, z, cut => Http.chunks.ended(g, cut)) : Bool} - -def walk_done.size(g, rest, after, n, z, ih): - match z: - case True{}: - walk_done.trail(g, rest, Http.chunks.trailer(after)) - case False{}: - walk_done.data(g, after, n, Http.octs.skip(after, n), {==}, ih) - -law walk_done.hex: - for +g: Nat - for rest: String - for +before: String - for +after: String - for ok: Bool - for ih: @s: String -> - {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) - : Bool} - {Http.walk.done(Http.walk.hex(g, rest, before, after, ok, s => Http.walk(g, s))) - == Http.chunks.ended.hex(before, after, ok, cut => Http.chunks.ended(g, cut)) : Bool} - -def walk_done.hex(g, rest, before, after, ok, ih): - match ok: - case False{}: - {==} - case True{}: - walk_done.size(g, rest, after, Http.hexlen(before), - Nat.is_eq(Http.hexlen(before), 0n), ih) - -law walk_done.at: - for +g: Nat - for rest: String - for c: Http.Cut - for ih: @s: String -> - {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) - : Bool} - {Http.walk.done(Http.walk.at(g, rest, c, s => Http.walk(g, s))) - == Http.chunks.ended.at(c, cut => Http.chunks.ended(g, cut)) : Bool} - -def walk_done.at(g, rest, c, ih): - match c: - case Http.NoCut{}: - {==} - case Http.Cut{+before, +after}: - walk_done.hex(g, rest, before, after, - Http.hex.well(Http.chunks.token(before)), ih) - -def Laws.walk_done(f, rest): - match f: - case 0n: - {==} - case 1n+f2: - +g = f2 - walk_done.at(g, rest, Http.divide(rest, "\r\n"), - s => Laws.walk_done(g, s)) - -law wm.ready: - for +g: Nat - for rest: String - for +b: String - for +l: Nat - for +r: String - for +erd: {Bool.and(Http.octs.has(r, Nat.add(l, 2n)), - String.starts_with(r, "\r\n")) == True{} : Bool} - for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.crlf(Nat.add(g, - String.length(b)), l, r ++ b, Bool.and( - Http.octs.has(r ++ b, Nat.add(l, 2n)), String.starts_with(r ++ b, "\r\n")), s => Http.walk(Nat.add(g, - String.length(b)), s)) - : Http.Walk} - for ih: @s: String -> - {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) - : Http.Walk} - {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk(g, Http.octs.drop(r, - Nat.add(l, 2n))), String.length(b), b) - : Http.Walk} - -def wm.ready(g, rest, b, l, r, erd, s1, ih): - +eh = Equal.sym(Bool, Http.octs.has(r, Nat.add(l, 2n)), True{}, - eq.and.left(Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(r, "\r\n"), erd)) - +es = Equal.sym(Bool, String.starts_with(r, "\r\n"), True{}, - eq.and.right(Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(r, "\r\n"), erd)) - +h2 = has_app(r, b, Nat.add(l, 2n), eh) - +s2 = sw_app(r, "\r\n", b, es) - +rd2 = Equal.trans(Bool, Bool.and(Http.octs.has(r ++ b, Nat.add(l, 2n)), - String.starts_with(r ++ b, "\r\n")), - Bool.and(True{}, String.starts_with(r ++ b, "\r\n")), True{}, - Equal.cong(Bool, Bool, x => Bool.and(x, String.starts_with(r ++ b, "\r\n")), - Http.octs.has(r ++ b, Nat.add(l, 2n)), True{}, h2), s2) - Equal.trans(Http.Walk, Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b), - Http.walk.crlf(Nat.add(g, String.length(b)), l, r ++ b, True{}, s => Http.walk(Nat.add(g, String.length(b)), s)), - Http.walk.more(Http.walk(g, Http.octs.drop(r, Nat.add(l, 2n))), String.length(b), b), - Equal.trans(Http.Walk, Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b), - Http.walk.crlf(Nat.add(g, String.length(b)), l, r ++ b, Bool.and( - Http.octs.has(r ++ b, Nat.add(l, 2n)), String.starts_with(r ++ b, "\r\n")), s => Http.walk(Nat.add(g, - String.length(b)), s)), - Http.walk.crlf(Nat.add(g, String.length(b)), l, r ++ b, True{}, s => Http.walk(Nat.add(g, - String.length(b)), s)), s1, - Equal.cong(Bool, Http.Walk, x => Http.walk.crlf(Nat.add(g, String.length(b)), l, r ++ b, x, - s => Http.walk(Nat.add(g, String.length(b)), s)), - Bool.and(Http.octs.has(r ++ b, Nat.add(l, 2n)), - String.starts_with(r ++ b, "\r\n")), True{}, rd2)), - Equal.trans(Http.Walk, - Http.walk(Nat.add(g, String.length(b)), Http.octs.drop(r ++ b, Nat.add(l, 2n))), - Http.walk(Nat.add(g, String.length(b)), Http.octs.drop(r, Nat.add(l, 2n)) ++ b), - Http.walk.more(Http.walk(g, Http.octs.drop(r, Nat.add(l, 2n))), String.length(b), b), - Equal.cong(String, Http.Walk, x => Http.walk(Nat.add(g, String.length(b)), x), - Http.octs.drop(r ++ b, Nat.add(l, 2n)), - Http.octs.drop(r, Nat.add(l, 2n)) ++ b, drop_app(r, b, Nat.add(l, 2n), eh)), - ih(Http.octs.drop(r, Nat.add(l, 2n))))) - -law wm.crlf: - for +g: Nat - for rest: String - for +b: String - for +l: Nat - for +r: String - for rd: Bool - for +erd: {rd == Bool.and(Http.octs.has(r, Nat.add(l, 2n)), - String.starts_with(r, "\r\n")) : Bool} - for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.data(Nat.add(g, - String.length(b)), Http.Over{l, r ++ b}, s => Http.walk(Nat.add(g, String.length(b)), s)) - : Http.Walk} - for ih: @s: String -> - {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) - : Http.Walk} - {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.crlf(g, l, r, rd, - s => Http.walk(g, s)), String.length(b), b) : Http.Walk} - -def wm.crlf(g, rest, b, l, r, rd, erd, s1, ih): - match rd: - case True{}: - wm.ready(g, rest, b, l, r, Equal.sym(Bool, True{}, Bool.and( - Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(r, "\r\n")), erd), s1, ih) - case False{}: - s1 - -law wm.data: - for +g: Nat - for rest: String - for +b: String - for sk: Http.Skip - for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.data(Nat.add(g, - String.length(b)), sk.then(sk, b), s => Http.walk(Nat.add(g, String.length(b)), s)) : Http.Walk} - for ih: @s: String -> - {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) - : Http.Walk} - {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.data(g, sk, - s => Http.walk(g, s)), String.length(b), b) : Http.Walk} - -def wm.data(g, rest, b, sk, s1, ih): - match sk: - case Http.Short{o}: - s1 - case Http.Over{+l, +r}: - wm.crlf(g, rest, b, l, r, Bool.and(Http.octs.has(r, Nat.add(l, 2n)), - String.starts_with(r, "\r\n")), {==}, s1, ih) - -law wm.trail: - for +g: Nat - for +rest: String - for +b: String - for +after: String - for t: Bool - for +et: {t == Http.chunks.trailer(after) : Bool} - for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.trail(Nat.add(g, - String.length(b)), rest ++ b, - Http.chunks.trailer(after ++ b)) : Http.Walk} - {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.trail(g, rest, t), - String.length(b), b) : Http.Walk} - -def wm.trail(g, rest, b, after, t, et, s1): - match t: - case True{}: - Equal.trans(Http.Walk, Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b), - Http.walk.trail(Nat.add(g, String.length(b)), rest ++ b, Http.chunks.trailer(after ++ b)), - Http.Ended{}, s1, - Equal.cong(Bool, Http.Walk, x => Http.walk.trail(Nat.add(g, String.length(b)), rest ++ b, x), - Http.chunks.trailer(after ++ b), True{}, trailer_app(after, b, et))) - case False{}: - {==} - -law wm.size: - for +g: Nat - for +rest: String - for +b: String - for +after: String - for +n: Nat - for z: Bool - for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.size(Nat.add(g, - String.length(b)), rest ++ b, after ++ b, n, z, s => Http.walk(Nat.add(g, String.length(b)), s)) - : Http.Walk} - for ih: @s: String -> - {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) - : Http.Walk} - {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.size(g, rest, after, n, - z, s => Http.walk(g, s)), String.length(b), b) - : Http.Walk} - -def wm.size(g, rest, b, after, n, z, s1, ih): - match z: - case True{}: - wm.trail(g, rest, b, after, Http.chunks.trailer(after), {==}, s1) - case False{}: - wm.data(g, rest, b, Http.octs.skip(after, n), - Equal.trans(Http.Walk, Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b), - Http.walk.data(Nat.add(g, String.length(b)), Http.octs.skip(after ++ b, n), - s => Http.walk(Nat.add(g, String.length(b)), s)), - Http.walk.data(Nat.add(g, String.length(b)), sk.then(Http.octs.skip(after, n), b), - s => Http.walk(Nat.add(g, String.length(b)), s)), s1, - Equal.cong(Http.Skip, Http.Walk, x => Http.walk.data(Nat.add(g, String.length(b)), x, - s => Http.walk(Nat.add(g, String.length(b)), s)), - Http.octs.skip(after ++ b, n), sk.then(Http.octs.skip(after, n), b), - skip_app(after, b, n))), ih) - -law wm.hex: - for +g: Nat - for +rest: String - for +b: String - for +before: String - for +after: String - for ok: Bool - for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.hex(Nat.add(g, - String.length(b)), rest ++ b, before, after ++ b, ok, s => Http.walk(Nat.add(g, String.length(b)), s)) - : Http.Walk} - for ih: @s: String -> - {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) - : Http.Walk} - {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.hex(g, rest, before, - after, ok, s => Http.walk(g, s)), String.length(b), b) - : Http.Walk} - -def wm.hex(g, rest, b, before, after, ok, s1, ih): - match ok: - case False{}: - s1 - case True{}: - wm.size(g, rest, b, after, Http.hexlen(before), - Nat.is_eq(Http.hexlen(before), 0n), s1, ih) - -law wm.at: - for +g: Nat - for +rest: String - for +b: String - for c: Http.Cut - for +ec: {c == Http.divide(rest, "\r\n") : Http.Cut} - for ih: @s: String -> - {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) - : Http.Walk} - {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.at(g, rest, c, - s => Http.walk(g, s)), String.length(b), b) : Http.Walk} - -def wm.at(g, rest, b, c, ec, ih): - match c: - case Http.NoCut{}: - {==} - case Http.Cut{+before, +after}: - +cut = Equal.cong(Http.Cut, Bool, x => cut.is(x), Http.Cut{before, after}, - Http.divide(rest, "\r\n"), ec) - +dv = Equal.trans(Http.Cut, Http.divide(rest ++ b, "\r\n"), - cut.app(Http.divide(rest, "\r\n"), b), Http.Cut{before, after ++ b}, - divide_app(rest, "\r\n", b, cut), - Equal.cong(Http.Cut, Http.Cut, x => cut.app(x, b), Http.divide(rest, "\r\n"), - Http.Cut{before, after}, - Equal.sym(Http.Cut, Http.Cut{before, after}, Http.divide(rest, "\r\n"), ec))) - wm.hex(g, rest, b, before, after, Http.hex.well(Http.chunks.token(before)), - Equal.cong(Http.Cut, Http.Walk, x => Http.walk.at(Nat.add(g, String.length(b)), rest ++ b, x, - s => Http.walk(Nat.add(g, String.length(b)), s)), - Http.divide(rest ++ b, "\r\n"), Http.Cut{before, after ++ b}, dv), ih) - -def Laws.walk_more(f, rest, b): - match f: - case 0n: - {==} - case 1n+f2: - +g = f2 - wm.at(g, rest, b, Http.divide(rest, "\r\n"), {==}, - s => Laws.walk_more(g, s, b)) - -law hd.body: - for f: Http.Frame - for +after: String - {Http.hear.done(Http.hear.body(f, after)) == Http.whole.of(f, after) : Bool} - -def hd.body(f, after): - match f: - case Http.Bare{}: - {==} - case Http.Sized{n}: - {==} - case Http.Chunked{}: - Laws.walk_done(String.length(after), after) - case Http.Closed{}: - {==} - -law hd.step: - for +g: Nat - for +asked: Bool - for +ls: List<&2, String> - for +after: String - for i: Bool - for ih: @s: String -> - {Http.hear.done(Http.hear.go(g, asked, s)) - == Http.whole.go(g, asked, Http.divide(s, "\r\n\r\n")) : Bool} - {Http.hear.done(Http.hear.step(asked, ls, after, i, s => Http.hear.go(g, asked, s))) - == Http.whole.step(asked, ls, after, i, cut => Http.whole.go(g, asked, cut)) : Bool} - -def hd.step(_g, asked, ls, after, i, ih): - match i: - case True{}: - ih(after) - case False{}: - hd.body(Http.frame(Bool.or(asked, Http.status.bare(Http.status.of(ls))), - Http.head.go(ls, "transfer-encoding"), - Http.head.values(ls, "content-length")), after) - -law hd.at: - for +g: Nat - for +asked: Bool - for raw: String - for c: Http.Cut - for ih: @s: String -> - {Http.hear.done(Http.hear.go(g, asked, s)) - == Http.whole.go(g, asked, Http.divide(s, "\r\n\r\n")) : Bool} - {Http.hear.done(Http.hear.at(g, asked, raw, c, s => Http.hear.go(g, asked, s))) - == Http.whole.at(asked, c, cut => Http.whole.go(g, asked, cut)) : Bool} - -def hd.at(g, asked, _raw, c, ih): - match c: - case Http.NoCut{}: - {==} - case Http.Cut{before, +after}: - +ls = String.lines(before) - hd.step(g, asked, ls, after, Http.status.interim(Http.status.of(ls)), ih) - -law hd: - for f: Nat - for +asked: Bool - for +raw: String - {Http.hear.done(Http.hear.go(f, asked, raw)) - == Http.whole.go(f, asked, Http.divide(raw, "\r\n\r\n")) : Bool} - -def hd(f, asked, raw): - match f: - case 0n: - {==} - case 1n+f2: - +g = f2 - hd.at(g, asked, raw, Http.divide(raw, "\r\n\r\n"), s => hd(g, asked, s)) - -def Laws.hear_done(method, raw): - hd(String.length(raw), String.eq(method, "HEAD"), raw) - -law add_assoc: - for x: Nat - for +y: Nat - for +z: Nat - {Nat.add(Nat.add(x, y), z) == Nat.add(x, Nat.add(y, z)) : Nat} - -def add_assoc(x, y, z): - match x: - case 0n: - {==} - case 1n+p: - Equal.cong(Nat, Nat, k => 1n+k, Nat.add(Nat.add(p, y), z), - Nat.add(p, Nat.add(y, z)), add_assoc(p, y, z)) - -law u8len_app: - for a: String - for +b: String - {Http.octs.len(a ++ b) == Nat.add(Http.octs.len(a), Http.octs.len(b)) : Nat} - -def u8len_app(a, b): - match a: - case SNil{}: - {==} - case SCon{+h, +t}: - +w = Http.octs.width(Char.to_u32(h)) - Equal.trans(Nat, Nat.add(w, Http.octs.len(t ++ b)), - Nat.add(w, Nat.add(Http.octs.len(t), Http.octs.len(b))), - Nat.add(Nat.add(w, Http.octs.len(t)), Http.octs.len(b)), - Equal.cong(Nat, Nat, k => Nat.add(w, k), Http.octs.len(t ++ b), - Nat.add(Http.octs.len(t), Http.octs.len(b)), u8len_app(t, b)), - Equal.sym(Nat, Nat.add(Nat.add(w, Http.octs.len(t)), Http.octs.len(b)), - Nat.add(w, Nat.add(Http.octs.len(t), Http.octs.len(b))), - add_assoc(w, Http.octs.len(t), Http.octs.len(b)))) - -law hm.body: - for f: Http.Frame - for +after: String - for +b: String - {Http.hear.body(f, after ++ b) == Http.hear.more(Http.hear.body(f, after), b) - : Http.Hear} - -def hm.body(f, after, b): - match f: - case Http.Bare{}: - {==} - case Http.Sized{+n}: - Equal.cong(Nat, Http.Hear, k => Http.Count{n, k}, Http.octs.len(after ++ b), - Nat.add(Http.octs.len(after), Http.octs.len(b)), u8len_app(after, b)) - case Http.Chunked{}: - Equal.cong(Http.Walk, Http.Hear, w => Http.Walking{w}, - Http.walk(String.length(after ++ b), after ++ b), - Http.walk.more(Http.walk(String.length(after), after), String.length(b), b), - Equal.trans(Http.Walk, Http.walk(String.length(after ++ b), after ++ b), - Http.walk(Nat.add(String.length(after), String.length(b)), after ++ b), - Http.walk.more(Http.walk(String.length(after), after), String.length(b), b), - Equal.cong(Nat, Http.Walk, k => Http.walk(k, after ++ b), - String.length(after ++ b), Nat.add(String.length(after), String.length(b)), - len_app(after, b)), - Laws.walk_more(String.length(after), after, b))) - case Http.Closed{}: - {==} - -law hm.step: - for +g: Nat - for +asked: Bool - for +raw: String - for +b: String - for +ls: List<&2, String> - for +after: String - for i: Bool - for +s1: {Http.hear.go(Nat.add(1n+g, String.length(b)), asked, raw ++ b) == Http.hear.step(asked, ls, - after ++ b, i, s => Http.hear.go(Nat.add(g, String.length(b)), asked, s)) : Http.Hear} - for ih: @s: String -> - {Http.hear.go(Nat.add(g, String.length(b)), asked, s ++ b) - == Http.hear.more(Http.hear.go(g, asked, s), b) : Http.Hear} - {Http.hear.go(Nat.add(1n+g, String.length(b)), asked, raw ++ b) == Http.hear.more(Http.hear.step(asked, ls, - after, i, s => Http.hear.go(g, asked, s)), b) - : Http.Hear} - -def hm.step(g, asked, raw, b, ls, after, i, s1, ih): - match i: - case True{}: - Equal.trans(Http.Hear, Http.hear.go(Nat.add(1n+g, String.length(b)), asked, raw ++ b), - Http.hear.go(Nat.add(g, String.length(b)), asked, after ++ b), - Http.hear.more(Http.hear.go(g, asked, after), b), s1, ih(after)) - case False{}: - +fr = Http.frame(Bool.or(asked, Http.status.bare(Http.status.of(ls))), - Http.head.go(ls, "transfer-encoding"), - Http.head.values(ls, "content-length")) - Equal.trans(Http.Hear, Http.hear.go(Nat.add(1n+g, String.length(b)), asked, raw ++ b), - Http.hear.body(fr, after ++ b), - Http.hear.more(Http.hear.body(fr, after), b), s1, hm.body(fr, after, b)) - -law hm.at: - for +g: Nat - for +asked: Bool - for +raw: String - for +b: String - for c: Http.Cut - for +ec: {c == Http.divide(raw, "\r\n\r\n") : Http.Cut} - for ih: @s: String -> - {Http.hear.go(Nat.add(g, String.length(b)), asked, s ++ b) - == Http.hear.more(Http.hear.go(g, asked, s), b) : Http.Hear} - {Http.hear.go(Nat.add(1n+g, String.length(b)), asked, raw ++ b) == Http.hear.more(Http.hear.at(g, asked, - raw, c, s => Http.hear.go(g, asked, s)), b) : Http.Hear} - -def hm.at(g, asked, raw, b, c, ec, ih): - match c: - case Http.NoCut{}: - {==} - case Http.Cut{+before, +after}: - +cut = Equal.cong(Http.Cut, Bool, x => cut.is(x), Http.Cut{before, after}, - Http.divide(raw, "\r\n\r\n"), ec) - +dv = Equal.trans(Http.Cut, Http.divide(raw ++ b, "\r\n\r\n"), - cut.app(Http.divide(raw, "\r\n\r\n"), b), Http.Cut{before, after ++ b}, - divide_app(raw, "\r\n\r\n", b, cut), - Equal.cong(Http.Cut, Http.Cut, x => cut.app(x, b), Http.divide(raw, "\r\n\r\n"), - Http.Cut{before, after}, - Equal.sym(Http.Cut, Http.Cut{before, after}, Http.divide(raw, "\r\n\r\n"), ec))) - +ls = String.lines(before) - hm.step(g, asked, raw, b, ls, after, Http.status.interim(Http.status.of(ls)), - Equal.cong(Http.Cut, Http.Hear, - x => Http.hear.at(Nat.add(g, String.length(b)), asked, raw ++ b, x, s => Http.hear.go(Nat.add(g, - String.length(b)), asked, s)), - Http.divide(raw ++ b, "\r\n\r\n"), Http.Cut{before, after ++ b}, dv), ih) - -law hm: - for f: Nat - for +asked: Bool - for +raw: String - for +b: String - {Http.hear.go(Nat.add(f, String.length(b)), asked, raw ++ b) - == Http.hear.more(Http.hear.go(f, asked, raw), b) : Http.Hear} - -def hm(f, asked, raw, b): - match f: - case 0n: - {==} - case 1n+f2: - +g = f2 - hm.at(g, asked, raw, b, Http.divide(raw, "\r\n\r\n"), {==}, s => hm(g, asked, s, b)) - -def Laws.hear_more(method, a, b): - +asked = String.eq(method, "HEAD") - Equal.sym(Http.Hear, Http.hear.of(method, a ++ b), - Http.hear.more(Http.hear.of(method, a), b), - Equal.trans(Http.Hear, Http.hear.go(String.length(a ++ b), asked, a ++ b), - Http.hear.go(Nat.add(String.length(a), String.length(b)), asked, a ++ b), - Http.hear.more(Http.hear.go(String.length(a), asked, a), b), - Equal.cong(Nat, Http.Hear, k => Http.hear.go(k, asked, a ++ b), - String.length(a ++ b), Nat.add(String.length(a), String.length(b)), len_app(a, b)), - hm(String.length(a), asked, a, b))) - -law app_nil: - for x: String - {x ++ "" == x : String} - -def app_nil(x): - match x: - case SNil{}: - {==} - case SCon{+h, +t}: - Equal.cong(String, String, r => SCon{h, r}, t ++ "", t, app_nil(t)) - -law app_assoc: - for x: String - for +y: String - for +z: String - {(x ++ y) ++ z == x ++ (y ++ z) : String} - -def app_assoc(x, y, z): - match x: - case SNil{}: - {==} - case SCon{+h, +t}: - Equal.cong(String, String, r => SCon{h, r}, (t ++ y) ++ z, t ++ (y ++ z), - app_assoc(t, y, z)) - -law join_go: - for ps: List<&2, String> - for +acc: String - {Spec.wire.join.go(ps, acc) == Spec.wire.joined(ps) ++ acc : String} - -def join_go(ps, acc): - match ps: - case []: - {==} - case +p <> +rest: - Equal.trans(String, Spec.wire.join.go(rest, p ++ acc), - Spec.wire.joined(rest) ++ (p ++ acc), - (Spec.wire.joined(rest) ++ p) ++ acc, - join_go(rest, p ++ acc), - Equal.sym(String, (Spec.wire.joined(rest) ++ p) ++ acc, - Spec.wire.joined(rest) ++ (p ++ acc), - app_assoc(Spec.wire.joined(rest), p, acc))) - -def Laws.join_joined(ps): - Equal.trans(String, Spec.wire.join.go(ps, ""), Spec.wire.joined(ps) ++ "", - Spec.wire.joined(ps), join_go(ps, ""), app_nil(Spec.wire.joined(ps))) - - -# --- redirect targets (SPEC.md HTTP-REDIR-3 to 5) --- - -def Laws.target_bad(url, loc, e1, _why, e2): - %e1 : {Redirect.redirect.target.of(url, loc, _) == None{} - : Maybe<&2, String>} - %e2 : {Redirect.redirect.at(_, loc) == None{} : Maybe<&2, String>} - {==} - -def Laws.target_against(url, loc, e1, scheme, host, port, path, e2): - %e1 : {Redirect.redirect.target.of(url, loc, _) - == Redirect.redirect.against(scheme, host, port, path, loc, - Redirect.redirect.sort(loc)) : Maybe<&2, String>} - %e2 : {Redirect.redirect.at(_, loc) - == Redirect.redirect.against(scheme, host, port, path, loc, - Redirect.redirect.sort(loc)) : Maybe<&2, String>} - {==} - -def Laws.against_net(_scheme, _host, _port, _path, _loc): - {==} - -def Laws.against_abs(_scheme, _host, _port, _path, _loc): - {==} - -def Laws.against_stay(_scheme, _host, _port, _path, _loc): - {==} - -def Laws.against_query(_scheme, _host, _port, _path, _loc): - {==} - -def Laws.against_rel(_scheme, _host, _port, _path, _loc): - {==} - -def Laws.sort_net(loc, e): - %e : {Redirect.redirect.sort.of(_, String.starts_with(loc, "/"), - Redirect.redirect.stays(loc), String.starts_with(loc, "?")) == Redirect.Net{} : Redirect.Sort} - {==} - -def Laws.sort_abs(loc, e1, e2): - %e1 : {Redirect.redirect.sort.of(_, String.starts_with(loc, "/"), - Redirect.redirect.stays(loc), String.starts_with(loc, "?")) == Redirect.Abs{} : Redirect.Sort} - %e2 : {Redirect.redirect.sort.abs(_, Redirect.redirect.stays(loc), - String.starts_with(loc, "?")) == Redirect.Abs{} : Redirect.Sort} - {==} - -def Laws.sort_stay(loc, e1, e2, e3): - %e1 : {Redirect.redirect.sort.of(_, String.starts_with(loc, "/"), - Redirect.redirect.stays(loc), String.starts_with(loc, "?")) == Redirect.Stay{} : Redirect.Sort} - %e2 : {Redirect.redirect.sort.abs(_, Redirect.redirect.stays(loc), - String.starts_with(loc, "?")) == Redirect.Stay{} : Redirect.Sort} - %e3 : {Redirect.redirect.sort.stay(_, String.starts_with(loc, "?")) == Redirect.Stay{} : Redirect.Sort} - {==} - -def Laws.sort_query(loc, e1, e2, e3, e4): - %e1 : {Redirect.redirect.sort.of(_, String.starts_with(loc, "/"), - Redirect.redirect.stays(loc), String.starts_with(loc, "?")) == Redirect.Query{} : Redirect.Sort} - %e2 : {Redirect.redirect.sort.abs(_, Redirect.redirect.stays(loc), - String.starts_with(loc, "?")) == Redirect.Query{} : Redirect.Sort} - %e3 : {Redirect.redirect.sort.stay(_, String.starts_with(loc, "?")) == Redirect.Query{} : Redirect.Sort} - %e4 : {Redirect.redirect.sort.rel(_) == Redirect.Query{} : Redirect.Sort} - {==} - -def Laws.sort_rel(loc, e1, e2, e3, e4): - %e1 : {Redirect.redirect.sort.of(_, String.starts_with(loc, "/"), - Redirect.redirect.stays(loc), String.starts_with(loc, "?")) == Redirect.Rel{} : Redirect.Sort} - %e2 : {Redirect.redirect.sort.abs(_, Redirect.redirect.stays(loc), - String.starts_with(loc, "?")) == Redirect.Rel{} : Redirect.Sort} - %e3 : {Redirect.redirect.sort.stay(_, String.starts_with(loc, "?")) == Redirect.Rel{} : Redirect.Sort} - %e4 : {Redirect.redirect.sort.rel(_) == Redirect.Rel{} : Redirect.Sort} - {==} - -def Laws.stays_on(_loc): - {==} - -def Laws.schemed_nil(s, e): - match s: - case SNil{}: - {==} - case SCon{h, t}: - eq.bit.absurd(&2, {Redirect.redirect.schemed(SCon{h, t}) == False{} : Bool}, - Equal.sym(Bool, True{}, False{}, e)) - -def Laws.schemed_colon(_t): - {==} - -def Laws.schemed_stop(c, t, e): - %e : {Redirect.redirect.schemed.at(_, Char.is_eq(c, ':'), - _u => Redirect.redirect.schemed(t)) == False{} : Bool} - {==} - -def Laws.schemed_other(c, t, e1, e2): - %e2 : {Redirect.redirect.schemed.at(_, Char.is_eq(c, ':'), - _u => Redirect.redirect.schemed(t)) == Redirect.redirect.schemed(t) : Bool} - %e1 : {Redirect.redirect.schemed.colon(_, _u => Redirect.redirect.schemed(t)) - == Redirect.redirect.schemed(t) : Bool} - {==} - -def Laws.schemed_end(_c): - {==} - -def Laws.split_query(loc, p, q, e): - %e : {Redirect.redirect.split.of(_, Url.frag.drop(loc)) - == Redirect.Ref{p, "?" ++ q} : Redirect.Ref} - {==} - -def Laws.split_plain(loc, e): - %e : {Redirect.redirect.split.of(_, Url.frag.drop(loc)) - == Redirect.Ref{Url.frag.drop(loc), ""} : Redirect.Ref} - {==} - -def Laws.abs_of(_path, _query): - {==} - -def Laws.rel_of(_base, _path, _query): - {==} - -def Laws.base_path(_path): - {==} - -def Laws.hier_first(_h, _t): - {==} - -def Laws.init_nil(ss, e): - match ss: - case []: - {==} - case h <> t: - eq.bit.absurd(&2, {Redirect.redirect.init(h <> t) == [] : List<&2, String>}, - Equal.sym(Bool, True{}, False{}, e)) - -def Laws.init_cons(_s, _t): - {==} - -def Laws.init_last(_s): - {==} - -def Laws.init_keep(_s, _u, _r): - {==} - -def Laws.clean_path(_ss): - {==} - -def Laws.dots_nil(_out): - {==} - -def Laws.dots_dot(s, t, out, e): - %e : {Redirect.redirect.dots.one(_, String.eq(s, ".."), - List.is_empty(&2, String, t), s, out, o => Redirect.redirect.dots(t, o)) - == Redirect.redirect.dots(t, - Redirect.redirect.dots.end(List.is_empty(&2, String, t), out)) - : List<&2, String>} - {==} - -def Laws.dots_up(s, t, out, e1, e2): - %e1 : {Redirect.redirect.dots.one(_, String.eq(s, ".."), - List.is_empty(&2, String, t), s, out, o => Redirect.redirect.dots(t, o)) - == Redirect.redirect.dots(t, - Redirect.redirect.dots.end(List.is_empty(&2, String, t), - Redirect.redirect.pop(out))) : List<&2, String>} - %e2 : {Redirect.redirect.dots.up(_, List.is_empty(&2, String, t), s, out, - o => Redirect.redirect.dots(t, o)) - == Redirect.redirect.dots(t, - Redirect.redirect.dots.end(List.is_empty(&2, String, t), - Redirect.redirect.pop(out))) : List<&2, String>} - {==} - -def Laws.dots_seg(s, t, out, e1, e2): - %e1 : {Redirect.redirect.dots.one(_, String.eq(s, ".."), - List.is_empty(&2, String, t), s, out, o => Redirect.redirect.dots(t, o)) - == Redirect.redirect.dots(t, s <> out) : List<&2, String>} - %e2 : {Redirect.redirect.dots.up(_, List.is_empty(&2, String, t), s, out, - o => Redirect.redirect.dots(t, o)) - == Redirect.redirect.dots(t, s <> out) : List<&2, String>} - {==} - -def Laws.dots_last(_out): - {==} - -def Laws.dots_inner(_out): - {==} - -def Laws.pop_nil(out, e): - match out: - case []: - {==} - case h <> t: - eq.bit.absurd(&2, {Redirect.redirect.pop(h <> t) == [] : List<&2, String>}, - Equal.sym(Bool, True{}, False{}, e)) - -def Laws.pop_cons(_s, _t): - {==} - -# --- octet bodies, responses by method, multipart forms (HTTP-CLI-10, 10, HTTP-FORM) --- - -def Laws.whole_go(_method, _raw): - {==} - -def Laws.whole_fuel(_asked, _c): - {==} - -def Laws.whole_interim(f, asked, before, after, e): - %e : {Http.whole.step(asked, String.lines(before), after, _, - cut => Http.whole.go(f, asked, cut)) - == Http.whole.go(f, asked, Http.divide(after, "\r\n\r\n")) : Bool} - {==} - -def Laws.status_interim_some(_n): - {==} - -def Laws.status_interim_none(_m, e): - %e : {Http.status.interim(_) == False{} : Bool} - {==} - -def Laws.exchange_body_text(_method, _l, _hs, _text): - {==} - -def Laws.exchange_body_empty(_method, _l, _hs): - {==} - -def Laws.exchange_body_octets(_method, _l, _hs, _n, _buf): - {==} - -def Laws.exchange_octets_bad(_method, _why, _hs, _n): - {==} - -def Laws.exchange_octets_loc(_method, _scheme, _host, _port, _path, _hs, _n): - {==} - -def Laws.request_head_at(_method, _secure, _host, _port, _path, _hs, _n): - {==} - -def Laws.fields_n(_host, _hs, _body): - {==} - -def Laws.fields_clen_n(host, hs, n): - hd.of_fields(Http.FClen{}, - Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), - Http.fields.or(Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}]), - Http.fields.or(Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}]), - Http.fields.of(Http.FOther{}, hs), - Http.clen.field(n, Nat.is_eq(n, 0n)), - [], [], [], [], - Http.clen.field(n, - Nat.is_eq(n, 0n)), - hd.or_gone(Http.FClen{}, Http.fields.of(Http.FHost{}, hs), - [Http.H{"Host", host}], - hd.disj(Http.FClen{}, Http.FHost{}, {==}, hs), {==}), - hd.or_gone(Http.FClen{}, Http.fields.of(Http.FAgent{}, hs), - [Http.H{"User-Agent", "ezhttp"}], - hd.disj(Http.FClen{}, Http.FAgent{}, {==}, hs), {==}), - hd.or_gone(Http.FClen{}, Http.fields.of(Http.FAccept{}, hs), - [Http.H{"Accept", "*/*"}], - hd.disj(Http.FClen{}, Http.FAccept{}, {==}, hs), {==}), - Equal.sym(List<&2, Http.Header>, - Http.fields.of(Http.FClen{}, Http.fields.of(Http.FOther{}, hs)), [], - hd.disj(Http.FClen{}, Http.FOther{}, {==}, hs)), - hd.clen_self(n, Nat.is_eq(n, 0n))) - -def Laws.reply_to(_method, _raw): - {==} - -def Laws.reply_fuel(_head, _c): - {==} - -def Laws.reply_open(_f, _head): - {==} - -def Laws.reply_interim(f, head, before, after, e): - %e : {Http.parse.to.step(head, String.lines(before), Http.status.of(String.lines(before)), after, _, - cut => Http.parse.to.go(f, head, cut)) - == Http.parse.to.go(f, head, Http.divide(after, "\r\n\r\n")) - : Http.Reply} - {==} - -def Laws.reply_bare(f, head, before, after, e1, e2, n, e3): - %e1 : {Http.parse.to.step(head, String.lines(before), Http.status.of(String.lines(before)), after, _, - cut => Http.parse.to.go(f, head, cut)) - == Http.Reply{n, Http.head.list(String.lines(before)), ""} : Http.Reply} - %e2 : {Http.parse.to.final(Http.status.of(String.lines(before)), String.lines(before), after, _) - == Http.Reply{n, Http.head.list(String.lines(before)), ""} : Http.Reply} - %e3 : {Http.parse.to.bare(_, String.lines(before)) - == Http.Reply{n, Http.head.list(String.lines(before)), ""} : Http.Reply} - {==} - -def Laws.reply_bare_none(f, head, before, after, e1, e2, e3): - %e1 : {Http.parse.to.step(head, String.lines(before), Http.status.of(String.lines(before)), after, _, - cut => Http.parse.to.go(f, head, cut)) - == Http.Torn{"the response has no status line"} : Http.Reply} - %e2 : {Http.parse.to.final(Http.status.of(String.lines(before)), String.lines(before), after, _) - == Http.Torn{"the response has no status line"} : Http.Reply} - %e3 : {Http.parse.to.bare(_, String.lines(before)) - == Http.Torn{"the response has no status line"} : Http.Reply} - {==} - -def Laws.reply_framed(f, head, before, after, e1, e2): - %e1 : {Http.parse.to.step(head, String.lines(before), Http.status.of(String.lines(before)), after, _, - cut => Http.parse.to.go(f, head, cut)) - == Http.parse.head(String.lines(before), after) : Http.Reply} - %e2 : {Http.parse.to.final(Http.status.of(String.lines(before)), String.lines(before), after, _) - == Http.parse.head(String.lines(before), after) : Http.Reply} - {==} - -def Laws.interim_skip(_m, _after): - {==} - -def Laws.status_empty(_m): - {==} - -def Laws.heard_ok(method, text): - dz = Equal.sym(String, String.drop(text, 0n), text, eq.drop_zero(text)) - %dz : {Http.parse.to(method, _) == Http.parse.to(method, text) : Http.Reply} - {==} - -def Laws.heard_fail(method, raw, e): - %e : {Spec.wire.heard.of(method, raw, _) - == Http.Torn{Spec.wire.why(Spec.wire.code(raw), Spec.wire.text(raw))} - : Http.Reply} - {==} - -def Laws.all_step(ok, _rest): - match ok: - case False{}: - {==} - case True{}: - {==} - -def Laws.any_step(hit, _rest): - match hit: - case True{}: - {==} - case False{}: - {==} - -def Laws.form_bad_boundary(b, ps, e): - %e : {Form.form.check(b, ps, _) - == Form.NoPlan{"the boundary must be 1 to 70 of A-Z a-z 0-9 - _ ."} - : Form.Plan} - {==} - -def Laws.form_clash(b, ps, e1, e2): - %e1 : {Form.form.check(b, ps, _) - == Form.NoPlan{"the boundary occurs in a part's content"} : Form.Plan} - %e2 : {Form.form.check.clash(b, ps, _) - == Form.NoPlan{"the boundary occurs in a part's content"} : Form.Plan} - {==} - -def Laws.form_bad_type(b, ps, e1, e2, e3): - %e1 : {Form.form.check(b, ps, _) - == Form.NoPlan{"a file part's content type holds a CR or LF"} : Form.Plan} - %e2 : {Form.form.check.clash(b, ps, _) - == Form.NoPlan{"a file part's content type holds a CR or LF"} : Form.Plan} - %e3 : {Form.form.check.type(b, ps, _) - == Form.NoPlan{"a file part's content type holds a CR or LF"} : Form.Plan} - {==} - -def Laws.form_ok(b, ps, e1, e2, e3): - %e1 : {Form.form.check(b, ps, _) - == Form.Plan{Form.form.type(b), Form.form.parts(b, ps)} - : Form.Plan} - %e2 : {Form.form.check.clash(b, ps, _) - == Form.Plan{Form.form.type(b), Form.form.parts(b, ps)} - : Form.Plan} - %e3 : {Form.form.check.type(b, ps, _) - == Form.Plan{Form.form.type(b), Form.form.parts(b, ps)} - : Form.Plan} - {==} - -def Laws.form_type(_b): - {==} - -def Laws.boundary_ok(_b): - {==} - -def Laws.bchar(_cp): - {==} - -def Laws.bchars_nil(_s, e): - %e : {Form.form.bchars(_) == True{} : Bool} - {==} - -def Laws.bchars_cons(c, t): - Laws.all_step(Form.form.bchar(c), _u => Form.form.bchars(t)) - -def Laws.delim(_b): - {==} - -def Laws.clash_nil(_b): - {==} - -def Laws.clash_cons(b, p, t): - Laws.any_step(Form.form.hit(b, p), _u => Form.form.clash(b, t)) - -def Laws.has_nil(_pat): - {==} - -def Laws.has_cons(x, t, pat): - Laws.any_step(Form.octets.starts(x <> t, pat), _u => Form.octets.has(t, pat)) - -def Laws.starts_done(xs): - match xs: - case []: - {==} - case _x <> _t: - {==} - -def Laws.starts_short(_p, _q): - {==} - -def Laws.starts_cons(x, t, p, q): - Laws.all_step(U32.is_eq(x, p), _u => Form.octets.starts(t, q)) - -def Laws.content_field(_name, _value, _rest): - {==} - -def Laws.content_file(_name, _filename, _ctype, _clash, _rest): - {==} - -def Laws.badtype_nil(_ps, e): - %e : {Form.form.badtype(_) == False{} : Bool} - {==} - -def Laws.badtype_cons(p, t): - Laws.any_step(Form.form.badtype.one(p), _u => Form.form.badtype(t)) - -def Laws.badtype_field(_name, _value): - {==} - -def Laws.badtype_file(_name, _filename, _ctype, _clash): - {==} - -def Laws.parts_nil(_b): - {==} - -def Laws.parts_cons(_b, _p, _t): - {==} - -def Laws.part(_b, _p, _rest): - {==} - -def Laws.onto_nil(_rest): - {==} - -def Laws.onto_cons(_c, _t, _rest): - {==} - -def Laws.field_is(_d, _name, _value): - {==} - -def Laws.file_is(_name, _filename, _ctype, _n, _buf, _hit): - {==} - -def Laws.head_field(_name, _value): - {==} - -def Laws.head_file(_name, _filename, _ctype, _clash): - {==} - -def Laws.disposition(_name): - {==} - -def Laws.fq_nil(_s, e): - %e : {Form.form.quote(_) == "" : String} - {==} - -def Laws.fq_cons(_c, _t): - {==} - -def Laws.fq_dq(_rest): - {==} - -def Laws.fq_cr(_rest): - {==} - -def Laws.fq_lf(_rest): - {==} - -def Laws.fq_other(c, e1, e2, e3, rest): - %e1 : {Form.form.one.dq(c, rest, _) == SCon{Chr{c}, rest} : String} - %e2 : {Form.form.one.cr(c, rest, _) == SCon{Chr{c}, rest} : String} - %e3 : {Form.form.one.lf(c, rest, _) == SCon{Chr{c}, rest} : String} - {==} - -# --- a response's reason phrase (SPEC.md HTTP-CLI-11) --- - -def Laws.phrase_to(_raw): - {==} - -def Laws.phrase_fuel(_c): - {==} - -def Laws.phrase_open(_f): - {==} - -def Laws.phrase_interim(f, before, after, e): - %e : {Http.phrase.to.step(String.lines(before), after, _, - cut => Http.phrase.to.go(f, cut)) - == Http.phrase.to.go(f, Http.divide(after, "\r\n\r\n")) : String} - {==} - -def Laws.phrase_final(f, before, after, e): - %e : {Http.phrase.to.step(String.lines(before), after, _, - cut => Http.phrase.to.go(f, cut)) - == Http.phrase.line(Http.first(String.lines(before))) : String} - {==} - -# LAW: a string that avoids a character does not start with it -law avoids.head: - for +h: Char - for +t: String - for +c: Char - for e: {True{} == Laws.avoids(SCon{h, t}, c) : Bool} - {False{} == Char.is_eq(h, c) : Bool} - -def avoids.head(h, t, c, e): - Equal.sym(Bool, Char.is_eq(h, c), False{}, - eq.not.true(Char.is_eq(h, c), - eq.and.left(Bool.not(Char.is_eq(h, c)), Laws.avoids(t, c), - Equal.sym(Bool, True{}, Laws.avoids(SCon{h, t}, c), e)))) - -# LAW: a string that avoids a character has a tail that avoids it -law avoids.tail: - for +h: Char - for +t: String - for +c: Char - for e: {True{} == Laws.avoids(SCon{h, t}, c) : Bool} - {True{} == Laws.avoids(t, c) : Bool} - -def avoids.tail(h, t, c, e): - Equal.sym(Bool, Laws.avoids(t, c), True{}, - eq.and.right(Bool.not(Char.is_eq(h, c)), Laws.avoids(t, c), - Equal.sym(Bool, True{}, Laws.avoids(SCon{h, t}, c), e))) - -# LAW: one step of dividing at the first SP, past a character that is not one -law sp_cut.step: - for +h: Char - for +t: String - for +b: String - for f: {False{} == Char.is_eq(h, ' ') : Bool} - for ih: {Http.divide.go(String.append(t, SCon{' ', b}), " ", 1n) - == Http.Cut{t, b} : Http.Cut} - {Http.divide.go(String.append(SCon{h, t}, SCon{' ', b}), " ", 1n) - == Http.Cut{SCon{h, t}, b} : Http.Cut} - -def sp_cut.step(h, t, b, f, ih): - %f : {Http.divide.step( - String.starts_with.go.if("", String.append(t, SCon{' ', b}), _), h, - String.append(t, SCon{' ', b}), 1n, - _u => Http.divide.go(String.append(t, SCon{' ', b}), " ", 1n)) - == Http.Cut{SCon{h, t}, b} : Http.Cut} - Equal.cong(Http.Cut, Http.Cut, r => Http.divide.push(h, r), - Http.divide.go(String.append(t, SCon{' ', b}), " ", 1n), Http.Cut{t, b}, - ih) - -# LAW: a text that holds no SP before one divides there -law sp_cut: - for +a: String - for +b: String - for +e: {True{} == Laws.avoids(a, ' ') : Bool} - {Http.divide.go(String.append(a, SCon{' ', b}), " ", 1n) - == Http.Cut{a, b} : Http.Cut} - -def sp_cut(a, b, e): - match a: - case SNil{}: - Equal.cong(String, Http.Cut, x => Http.Cut{"", x}, String.drop(b, 0n), - b, eq.drop_zero(b)) - case SCon{+h, +t}: - sp_cut.step(h, t, b, avoids.head(h, t, ' ', e), - sp_cut(t, b, avoids.tail(h, t, ' ', e))) - -# LAW: one step of dividing a text with no SP -law sp_none.step: - for +h: Char - for +t: String - for f: {False{} == Char.is_eq(h, ' ') : Bool} - for ih: {Http.divide.go(t, " ", 1n) == Http.NoCut{} : Http.Cut} - {Http.divide.go(SCon{h, t}, " ", 1n) == Http.NoCut{} : Http.Cut} - -def sp_none.step(h, t, f, ih): - %f : {Http.divide.step(String.starts_with.go.if("", t, _), h, t, 1n, - _u => Http.divide.go(t, " ", 1n)) == Http.NoCut{} : Http.Cut} - Equal.cong(Http.Cut, Http.Cut, r => Http.divide.push(h, r), - Http.divide.go(t, " ", 1n), Http.NoCut{}, ih) - -# LAW: a text with no SP does not divide at one -law sp_none: - for +a: String - for +e: {True{} == Laws.avoids(a, ' ') : Bool} - {Http.divide.go(a, " ", 1n) == Http.NoCut{} : Http.Cut} - -def sp_none(a, e): - match a: - case SNil{}: - {==} - case SCon{+h, +t}: - sp_none.step(h, t, avoids.head(h, t, ' ', e), - sp_none(t, avoids.tail(h, t, ' ', e))) - -# LAW: one step of the chop, past a character that is not CR -law chop.step: - for +h: Char - for +t: String - for +x: String - for f: {False{} == Char.is_eq(h, '\r') : Bool} - for ih: {Http.phrase.chop(x) == t : String} - {Http.phrase.chop(SCon{h, x}) == SCon{h, t} : String} - -def chop.step(h, t, x, f, ih): - %f : {Http.phrase.chop.at(_, h, x, _u => Http.phrase.chop(x)) - == SCon{h, t} : String} - Equal.cong(String, String, y => SCon{h, y}, Http.phrase.chop(x), t, ih) - -# LAW: a text with no CR is its own chop -law chop.keep: - for +r: String - for +e: {True{} == Laws.avoids(r, '\r') : Bool} - {Http.phrase.chop(r) == r : String} - -def chop.keep(r, e): - match r: - case SNil{}: - {==} - case SCon{+h, +t}: - chop.step(h, t, t, avoids.head(h, t, '\r', e), - chop.keep(t, avoids.tail(h, t, '\r', e))) - -# LAW: the chop of a text with no CR and then a CR is the text -law chop.cr: - for +r: String - for +e: {True{} == Laws.avoids(r, '\r') : Bool} - {Http.phrase.chop(String.append(r, "\r")) == r : String} - -def chop.cr(r, e): - match r: - case SNil{}: - {==} - case SCon{+h, +t}: - chop.step(h, t, String.append(t, "\r"), avoids.head(h, t, '\r', e), - chop.cr(t, avoids.tail(h, t, '\r', e))) - -def Laws.phrase_line(v, c, r, e1, e2, e3): - Equal.trans(String, - Http.phrase.line(String.append(v, SCon{' ', String.append(c, SCon{' ', r})})), - Http.phrase.code(Http.Cut{c, r}), - r, - Equal.trans(String, - Http.phrase.line(String.append(v, SCon{' ', String.append(c, SCon{' ', r})})), - Http.phrase.after(Http.Cut{v, String.append(c, SCon{' ', r})}), - Http.phrase.code(Http.Cut{c, r}), - Equal.cong(Http.Cut, String, k => Http.phrase.after(k), - Http.divide.go(String.append(v, SCon{' ', String.append(c, SCon{' ', r})}), " ", 1n), - Http.Cut{v, String.append(c, SCon{' ', r})}, - sp_cut(v, String.append(c, SCon{' ', r}), e1)), - Equal.cong(Http.Cut, String, k => Http.phrase.code(k), - Http.divide.go(String.append(c, SCon{' ', r}), " ", 1n), - Http.Cut{c, r}, sp_cut(c, r, e2))), - chop.keep(r, e3)) - -def Laws.phrase_line_cr(v, c, r, e1, e2, e3): - Equal.trans(String, - Http.phrase.line(String.append(v, SCon{' ', String.append(c, - SCon{' ', String.append(r, "\r")})})), - Http.phrase.code(Http.Cut{c, String.append(r, "\r")}), - r, - Equal.trans(String, - Http.phrase.line(String.append(v, SCon{' ', String.append(c, - SCon{' ', String.append(r, "\r")})})), - Http.phrase.after(Http.Cut{v, String.append(c, - SCon{' ', String.append(r, "\r")})}), - Http.phrase.code(Http.Cut{c, String.append(r, "\r")}), - Equal.cong(Http.Cut, String, k => Http.phrase.after(k), - Http.divide.go(String.append(v, SCon{' ', String.append(c, - SCon{' ', String.append(r, "\r")})}), " ", 1n), - Http.Cut{v, String.append(c, SCon{' ', String.append(r, "\r")})}, - sp_cut(v, String.append(c, SCon{' ', String.append(r, "\r")}), e1)), - Equal.cong(Http.Cut, String, k => Http.phrase.code(k), - Http.divide.go(String.append(c, SCon{' ', String.append(r, "\r")}), - " ", 1n), - Http.Cut{c, String.append(r, "\r")}, - sp_cut(c, String.append(r, "\r"), e2))), - chop.cr(r, e3)) - -def Laws.phrase_line_one(v, c, e1, e2): - Equal.trans(String, - Http.phrase.line(String.append(v, SCon{' ', c})), - Http.phrase.after(Http.Cut{v, c}), - "", - Equal.cong(Http.Cut, String, k => Http.phrase.after(k), - Http.divide.go(String.append(v, SCon{' ', c}), " ", 1n), - Http.Cut{v, c}, sp_cut(v, c, e1)), - Equal.cong(Http.Cut, String, k => Http.phrase.code(k), - Http.divide.go(c, " ", 1n), Http.NoCut{}, sp_none(c, e2))) - -def Laws.phrase_line_none(v, e): - Equal.cong(Http.Cut, String, k => Http.phrase.after(k), - Http.divide.go(v, " ", 1n), Http.NoCut{}, sp_none(v, e)) - -def Laws.phrase_heard(text): - dz = Equal.sym(String, String.drop(text, 0n), text, eq.drop_zero(text)) - %dz : {Http.phrase.to(_) == Http.phrase.to(text) : String} - {==} - -# --- percent-decoding and query strings (SPEC.md HTTP-QS) --- - -def Laws.qs_unquote(_s): - {==} - -def Laws.qs_unquote_plus(_s): - {==} - -def Laws.qs_toks_nil(_plus, _st): - {==} - -def Laws.qs_toks_cons(_c, _t, _plus, _st): - {==} - -def Laws.qs_tok_wide(plus, c, k, e): - %e : {Qs.tok.wide(plus, c, k, _) == Qs.W{c} <> k(Qs.Plain{}) - : List<&2, Qs.Tok>} - {==} - -def Laws.qs_tok_open(plus, c, k, e1, e2): - %e1 : {Qs.tok.wide(plus, c, k, _) == k(Qs.Pct{}) : List<&2, Qs.Tok>} - %e2 : {Qs.tok.ascii(plus, c, k, _) == k(Qs.Pct{}) : List<&2, Qs.Tok>} - {==} - -def Laws.qs_tok_byte(plus, c, k, e1, e2): - %e1 : {Qs.tok.wide(plus, c, k, _) - == Qs.B{Qs.tok.byte(plus, c)} <> k(Qs.Plain{}) : List<&2, Qs.Tok>} - %e2 : {Qs.tok.ascii(plus, c, k, _) - == Qs.B{Qs.tok.byte(plus, c)} <> k(Qs.Plain{}) : List<&2, Qs.Tok>} - {==} - -def Laws.qs_byte_plus(c, e): - %e : {Bool.pick(U32, Bool.and(True{}, _), 32, Char.to_u32(c)) == 32 : U32} - {==} - -def Laws.qs_byte_other(_plus, c, e): - %e : {Bool.pick(U32, _, 32, Char.to_u32(c)) == Char.to_u32(c) : U32} - {==} - -def Laws.qs_tok_pct_hex(plus, c, k, e): - %e : {Qs.tok.pct(plus, c, k, _) == k(Qs.Pct1{c}) : List<&2, Qs.Tok>} - {==} - -def Laws.qs_tok_pct_other(plus, c, k, e): - %e : {Qs.tok.pct(plus, c, k, _) - == Qs.B{37} <> Qs.tok.step(plus, Qs.Plain{}, c, k) : List<&2, Qs.Tok>} - {==} - -def Laws.qs_tok_pct1_hex(plus, d, c, k, e): - %e : {Qs.tok.pct1(plus, d, c, k, _) - == Qs.B{U32.add(U32.mul(Http.hex.val(d), 16), Http.hex.val(c))} - <> k(Qs.Plain{}) : List<&2, Qs.Tok>} - {==} - -def Laws.qs_tok_pct1_other(plus, d, c, k, e): - %e : {Qs.tok.pct1(plus, d, c, k, _) - == Qs.B{37} <> (Qs.B{Char.to_u32(d)} - <> Qs.tok.step(plus, Qs.Plain{}, c, k)) : List<&2, Qs.Tok>} - {==} - -def Laws.qs_tok_end_plain(_st, e): - %e : {Qs.tok.end(_) == [] : List<&2, Qs.Tok>} - {==} - -def Laws.qs_tok_end_pct(_st, e): - %e : {Qs.tok.end(_) == [Qs.B{37}] : List<&2, Qs.Tok>} - {==} - -def Laws.qs_tok_end_pct1(_d): - {==} - -def Laws.qs_tok_hex(_c): - {==} - -def Laws.qs_hexval_dec(u, e): - %e : {Bool.pick(U32, _, U32.sub(u, 48), - Bool.pick(U32, Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)), - U32.sub(u, 87), - Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), - U32.sub(u, 55), 16))) == U32.sub(u, 48) : U32} - {==} - -def Laws.qs_hexval_upper(u, e1, e2, e3): - %e1 : {Bool.pick(U32, _, U32.sub(u, 48), - Bool.pick(U32, Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)), - U32.sub(u, 87), - Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), - U32.sub(u, 55), 16))) == U32.sub(u, 55) : U32} - %e2 : {Bool.pick(U32, _, U32.sub(u, 87), - Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), - U32.sub(u, 55), 16)) == U32.sub(u, 55) : U32} - %e3 : {Bool.pick(U32, _, U32.sub(u, 55), 16) == U32.sub(u, 55) : U32} - {==} - -def Laws.qs_hexval_lower(u, e1, e2): - %e1 : {Bool.pick(U32, _, U32.sub(u, 48), - Bool.pick(U32, Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)), - U32.sub(u, 87), - Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), - U32.sub(u, 55), 16))) == U32.sub(u, 87) : U32} - %e2 : {Bool.pick(U32, _, U32.sub(u, 87), - Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), - U32.sub(u, 55), 16)) == U32.sub(u, 87) : U32} - {==} - -def Laws.qs_hexval_none(u, e1, e2, e3): - %e1 : {Bool.pick(U32, _, U32.sub(u, 48), - Bool.pick(U32, Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)), - U32.sub(u, 87), - Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), - U32.sub(u, 55), 16))) == 16 : U32} - %e2 : {Bool.pick(U32, _, U32.sub(u, 87), - Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), - U32.sub(u, 55), 16)) == 16 : U32} - %e3 : {Bool.pick(U32, _, U32.sub(u, 55), 16) == 16 : U32} - {==} - -def Laws.qs_dec_nil(_st): - {==} - -def Laws.qs_dec_cons(_t, _rest, _st): - {==} - -def Laws.qs_end_idle(_st, e): - %e : {Qs.utf.end(_) == "" : String} - {==} - -def Laws.qs_end_need(_cp, _left, _lo, _hi): - {==} - -def Laws.qs_idle_wide(_c, _k): - {==} - -def Laws.qs_idle_byte(_b, _k): - {==} - -def Laws.qs_lead_on_ascii(_b, _k): - {==} - -def Laws.qs_lead_on_bad(_b, _k): - {==} - -def Laws.qs_lead_on_opens(_b, _k, _cp, _left, _lo, _hi): - {==} - -def Laws.qs_lead_ascii(b, e): - %e : {Qs.lead.ascii(b, _) == Qs.Ascii{} : Qs.Lead} - {==} - -def Laws.qs_lead_low(b, e1, e2): - %e1 : {Qs.lead.ascii(b, _) == Qs.Bad{} : Qs.Lead} - %e2 : {Qs.lead.low(b, _) == Qs.Bad{} : Qs.Lead} - {==} - -def Laws.qs_lead_two(b, e1, e2, e3): - %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{U32.and(b, 31), 0n, 128, 191} - : Qs.Lead} - %e2 : {Qs.lead.low(b, _) == Qs.Opens{U32.and(b, 31), 0n, 128, 191} - : Qs.Lead} - %e3 : {Qs.lead.two(b, _) == Qs.Opens{U32.and(b, 31), 0n, 128, 191} - : Qs.Lead} - {==} - -def Laws.qs_lead_e0(b, e1, e2, e3, e4): - %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{0, 1n, 160, 191} : Qs.Lead} - %e2 : {Qs.lead.low(b, _) == Qs.Opens{0, 1n, 160, 191} : Qs.Lead} - %e3 : {Qs.lead.two(b, _) == Qs.Opens{0, 1n, 160, 191} : Qs.Lead} - %e4 : {Qs.lead.e0(b, _) == Qs.Opens{0, 1n, 160, 191} : Qs.Lead} - {==} - -def Laws.qs_lead_e1(b, e1, e2, e3, e4, e5): - %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - %e2 : {Qs.lead.low(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - %e3 : {Qs.lead.two(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - %e4 : {Qs.lead.e0(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - %e5 : {Qs.lead.e1(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - {==} - -def Laws.qs_lead_ed(b, e1, e2, e3, e4, e5, e6): - %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} - %e2 : {Qs.lead.low(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} - %e3 : {Qs.lead.two(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} - %e4 : {Qs.lead.e0(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} - %e5 : {Qs.lead.e1(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} - %e6 : {Qs.lead.ed(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} - {==} - -def Laws.qs_lead_ee(b, e1, e2, e3, e4, e5, e6, e7): - %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - %e2 : {Qs.lead.low(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - %e3 : {Qs.lead.two(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - %e4 : {Qs.lead.e0(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - %e5 : {Qs.lead.e1(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - %e6 : {Qs.lead.ed(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - %e7 : {Qs.lead.ee(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} - : Qs.Lead} - {==} - -def Laws.qs_lead_f0(b, e1, e2, e3, e4, e5, e6, e7, e8): - %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} - %e2 : {Qs.lead.low(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} - %e3 : {Qs.lead.two(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} - %e4 : {Qs.lead.e0(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} - %e5 : {Qs.lead.e1(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} - %e6 : {Qs.lead.ed(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} - %e7 : {Qs.lead.ee(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} - %e8 : {Qs.lead.f0(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} - {==} - -def Laws.qs_lead_f1(b, e1, e2, e3, e4, e5, e6, e7, e8, e9): - %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} - : Qs.Lead} - %e2 : {Qs.lead.low(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} - : Qs.Lead} - %e3 : {Qs.lead.two(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} - : Qs.Lead} - %e4 : {Qs.lead.e0(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} - : Qs.Lead} - %e5 : {Qs.lead.e1(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} - : Qs.Lead} - %e6 : {Qs.lead.ed(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} - : Qs.Lead} - %e7 : {Qs.lead.ee(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} - : Qs.Lead} - %e8 : {Qs.lead.f0(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} - : Qs.Lead} - %e9 : {Qs.lead.f1(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} - : Qs.Lead} - {==} - -def Laws.qs_lead_f4(b, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10): - %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} - %e2 : {Qs.lead.low(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} - %e3 : {Qs.lead.two(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} - %e4 : {Qs.lead.e0(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} - %e5 : {Qs.lead.e1(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} - %e6 : {Qs.lead.ed(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} - %e7 : {Qs.lead.ee(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} - %e8 : {Qs.lead.f0(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} - %e9 : {Qs.lead.f1(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} - %e10 : {Qs.lead.f4(_) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} - {==} - -def Laws.qs_lead_high(b, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10): - %e1 : {Qs.lead.ascii(b, _) == Qs.Bad{} : Qs.Lead} - %e2 : {Qs.lead.low(b, _) == Qs.Bad{} : Qs.Lead} - %e3 : {Qs.lead.two(b, _) == Qs.Bad{} : Qs.Lead} - %e4 : {Qs.lead.e0(b, _) == Qs.Bad{} : Qs.Lead} - %e5 : {Qs.lead.e1(b, _) == Qs.Bad{} : Qs.Lead} - %e6 : {Qs.lead.ed(b, _) == Qs.Bad{} : Qs.Lead} - %e7 : {Qs.lead.ee(b, _) == Qs.Bad{} : Qs.Lead} - %e8 : {Qs.lead.f0(b, _) == Qs.Bad{} : Qs.Lead} - %e9 : {Qs.lead.f1(b, _) == Qs.Bad{} : Qs.Lead} - %e10 : {Qs.lead.f4(_) == Qs.Bad{} : Qs.Lead} - {==} - -def Laws.qs_need_wide(_cp, _left, _lo, _hi, _c, _k): - {==} - -def Laws.qs_need_last(cp, _lo, _hi, b, k, e): - %e : {Qs.utf.byte(cp, 0n, b, k, _) - == SCon{Chr{Qs.utf.add(cp, b)}, k(Qs.Idle{})} : String} - {==} - -def Laws.qs_need_more(cp, l, _lo, _hi, b, k, e): - %e : {Qs.utf.byte(cp, 1n+l, b, k, _) - == k(Qs.Need{Qs.utf.add(cp, b), l, 128, 191}) : String} - {==} - -def Laws.qs_need_miss(cp, left, _lo, _hi, b, k, e): - %e : {Qs.utf.byte(cp, left, b, k, _) - == SCon{Chr{65533}, Qs.utf.lead(b, k, Qs.lead(b))} : String} - {==} - -def Laws.qs_add(_cp, _b): - {==} - -def Laws.qs_qp_nil(_s, e): - %e : {Qs.quote.plus(_) == "" : String} - {==} - -def Laws.qs_qp_space(_t): - {==} - -def Laws.qs_qp_other(cp, t, e): - %e : {Qs.plus.one(cp, Qs.quote.plus(t), _) - == Pct.one(False{}, cp, Qs.quote.plus(t)) : String} - {==} - -def Laws.qs_ue_nil(_ps, e): - %e : {Qs.urlencode(_) == "" : String} - {==} - -def Laws.qs_ue_cons(_name, _value, _t): - {==} - -def Laws.qs_more_nil(_ps, e): - %e : {Qs.form.more(_) == "" : String} - {==} - -def Laws.qs_more_cons(_name, _value, _t): - {==} - -def Laws.qs_pair(_name, _value): - {==} - -def Laws.qs_parse(_text): - {==} - -def Laws.qs_fields_nil(_ps, e): - %e : {Qs.fields(_) == [] : List<&2, Qs.Kv()>} - {==} - -def Laws.qs_fields_skip(_t): - {==} - -def Laws.qs_fields_keep(p, t, e): - %e : {Qs.fields.put(p, Qs.fields(t), _) == Qs.field(p) <> Qs.fields(t) - : List<&2, Qs.Kv()>} - {==} - -def Laws.qs_field(_piece): - {==} - -def Laws.qs_field_none(_whole): - {==} - -def Laws.qs_field_some(_name, _value, _whole): - {==} - -def Laws.qs_cut_found(s, r, e): - match s: - case SNil{}: - {==} - case SCon{+h, +t}: - ne = Equal.sym(Bool, Char.is_eq(h, '='), False{}, - eq.not.true(Char.is_eq(h, '='), - eq.and.left(Bool.not(Char.is_eq(h, '=')), Laws.avoids(t, '='), e))) - ih = Equal.sym(Maybe<&2, Qs.Kv()>, - Qs.cut(String.append(t, SCon{'=', r})), Some{(t, r)}, - Laws.qs_cut_found(t, r, - eq.and.right(Bool.not(Char.is_eq(h, '=')), Laws.avoids(t, '='), e))) - %ne : {Qs.cut.at(h, String.append(t, SCon{'=', r}), _, - _u => Qs.cut(String.append(t, SCon{'=', r}))) - == Some{(SCon{h, t}, r)} : Maybe<&2, Qs.Kv()>} - %ih : {Qs.cut.push(h, _) == Some{(SCon{h, t}, r)} - : Maybe<&2, Qs.Kv()>} - {==} - -def Laws.qs_cut_none(s, e): - match s: - case SNil{}: - {==} - case SCon{+h, +t}: - ne = Equal.sym(Bool, Char.is_eq(h, '='), False{}, - eq.not.true(Char.is_eq(h, '='), - eq.and.left(Bool.not(Char.is_eq(h, '=')), Laws.avoids(t, '='), e))) - ih = Equal.sym(Maybe<&2, Qs.Kv()>, Qs.cut(t), None{}, - Laws.qs_cut_none(t, - eq.and.right(Bool.not(Char.is_eq(h, '=')), Laws.avoids(t, '='), e))) - %ne : {Qs.cut.at(h, t, _, _u => Qs.cut(t)) == None{} - : Maybe<&2, Qs.Kv()>} - %ih : {Qs.cut.push(h, _) == None{} : Maybe<&2, Qs.Kv()>} - {==} - -def Laws.qs_get_nil(_name): - {==} - -def Laws.qs_get_later(key, value, _t, name, x, e): - %e : {Qs.get.pick(_, value, String.eq(key, name)) == Some{x} - : Maybe<&2, String>} - {==} - -def Laws.qs_get_here(key, value, _t, name, e1, e2): - %e1 : {Qs.get.pick(_, value, String.eq(key, name)) == Some{value} - : Maybe<&2, String>} - %e2 : {Qs.get.pick(None{}, value, _) == Some{value} : Maybe<&2, String>} - {==} - -def Laws.qs_get_miss(key, value, _t, name, e1, e2): - %e1 : {Qs.get.pick(_, value, String.eq(key, name)) == None{} - : Maybe<&2, String>} - %e2 : {Qs.get.pick(None{}, value, _) == None{} : Maybe<&2, String>} - {==} - -# --- HTML character references (SPEC.md HTTP-REF) --- - -def Laws.ref_unescape(_s): - {==} - -def Laws.ref_scan_nil(_st): - {==} - -def Laws.ref_scan_cons(_c, _t, _st): - {==} - -def Laws.ref_text_amp(c, k, e): - %e : {Ref.text.of(c, k, _) == k(Ref.Amp{}) : String} - {==} - -def Laws.ref_text_other(c, k, e): - %e : {Ref.text.of(c, k, _) == SCon{c, k(Ref.Txt{})} : String} - {==} - -def Laws.ref_amp_hash(c, k, e): - %e : {Ref.amp(c, k, _) == k(Ref.Hash{}) : String} - {==} - -def Laws.ref_amp_name(c, k, e1, e2): - %e1 : {Ref.amp(c, k, _) == k(Ref.Name{SCon{c, ""}, 1n}) : String} - %e2 : {Ref.amp.name(c, k, _) == k(Ref.Name{SCon{c, ""}, 1n}) : String} - {==} - -def Laws.ref_amp_other(c, k, e1, e2): - %e1 : {Ref.amp(c, k, _) == SCon{'&', Ref.step(Ref.Txt{}, c, k)} : String} - %e2 : {Ref.amp.name(c, k, _) == SCon{'&', Ref.step(Ref.Txt{}, c, k)} - : String} - {==} - -def Laws.ref_hash_dec(c, k, e): - %e : {Ref.hash.dec(c, k, Ref.digit(False{}, c), _) - == k(Ref.Num{False{}, Ref.digit(False{}, c)}) : String} - {==} - -def Laws.ref_hash_x(c, k, e1, e2): - %e1 : {Ref.hash.dec(c, k, Ref.digit(False{}, c), _) == k(Ref.HashX{c}) - : String} - %e2 : {Ref.hash.x(c, k, _) == k(Ref.HashX{c}) : String} - {==} - -def Laws.ref_hash_other(c, k, e1, e2): - %e1 : {Ref.hash.dec(c, k, Ref.digit(False{}, c), _) - == "&#" ++ Ref.step(Ref.Txt{}, c, k) : String} - %e2 : {Ref.hash.x(c, k, _) == "&#" ++ Ref.step(Ref.Txt{}, c, k) : String} - {==} - -def Laws.ref_hashx_digit(x, c, k, e): - %e : {Ref.hash.hex.of(x, c, k, Ref.digit(True{}, c), _) - == k(Ref.Num{True{}, Ref.digit(True{}, c)}) : String} - {==} - -def Laws.ref_hashx_other(x, c, k, e): - %e : {Ref.hash.hex.of(x, c, k, Ref.digit(True{}, c), _) - == SCon{'&', SCon{'#', SCon{x, Ref.step(Ref.Txt{}, c, k)}}} : String} - {==} - -def Laws.ref_num_digit(hex, n, c, k, e): - %e : {Ref.num(hex, n, c, k, Ref.digit(hex, c), _) - == k(Ref.Num{hex, Ref.grow(hex, n, Ref.digit(hex, c))}) : String} - {==} - -def Laws.ref_num_semi(hex, n, c, k, e1, e2): - %e1 : {Ref.num(hex, n, c, k, Ref.digit(hex, c), _) - == Ref.code(n) ++ k(Ref.Txt{}) : String} - %e2 : {Ref.num.end(n, c, k, _) == Ref.code(n) ++ k(Ref.Txt{}) : String} - {==} - -def Laws.ref_num_other(hex, n, c, k, e1, e2): - %e1 : {Ref.num(hex, n, c, k, Ref.digit(hex, c), _) - == Ref.code(n) ++ Ref.step(Ref.Txt{}, c, k) : String} - %e2 : {Ref.num.end(n, c, k, _) == Ref.code(n) ++ Ref.step(Ref.Txt{}, c, k) - : String} - {==} - -def Laws.ref_name_semi(s, len, c, k, e): - %e : {Ref.name(s, len, c, k, _) == Ref.named(s ++ ";") ++ k(Ref.Txt{}) - : String} - {==} - -def Laws.ref_name_join(s, len, c, k, e1, e2): - %e1 : {Ref.name(s, len, c, k, _) - == k(Ref.Name{s ++ SCon{c, ""}, 1n+len}) : String} - %e2 : {Ref.name.more(s, len, c, k, _) - == k(Ref.Name{s ++ SCon{c, ""}, 1n+len}) : String} - {==} - -def Laws.ref_name_stop(s, len, c, k, e1, e2): - %e1 : {Ref.name(s, len, c, k, _) - == Ref.named(s) ++ Ref.step(Ref.Txt{}, c, k) : String} - %e2 : {Ref.name.more(s, len, c, k, _) - == Ref.named(s) ++ Ref.step(Ref.Txt{}, c, k) : String} - {==} - -def Laws.ref_end_text(_st, e): - %e : {Ref.end(_) == "" : String} - {==} - -def Laws.ref_end_amp(_st, e): - %e : {Ref.end(_) == "&" : String} - {==} - -def Laws.ref_end_hash(_st, e): - %e : {Ref.end(_) == "&#" : String} - {==} - -def Laws.ref_end_hashx(_x): - {==} - -def Laws.ref_end_num(_hex, _n): - {==} - -def Laws.ref_end_name(_s, _len): - {==} - -def Laws.ref_name_char(_c): - {==} - -def Laws.ref_digit_dec(u, e): - %e : {Bool.pick(U32, _, U32.sub(u, 48), 16) == U32.sub(u, 48) : U32} - {==} - -def Laws.ref_digit_dec_none(u, e): - %e : {Bool.pick(U32, _, U32.sub(u, 48), 16) == 16 : U32} - {==} - -def Laws.ref_digit_hex_dec(u, e): - %e : {Bool.pick(U32, _, U32.sub(u, 48), - Bool.pick(U32, Bool.and(U32.is_le(65, u), U32.is_le(u, 70)), - U32.sub(u, 55), - Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), - U32.sub(u, 87), 16))) == U32.sub(u, 48) : U32} - {==} - -def Laws.ref_digit_hex_upper(u, e1, e2): - %e1 : {Bool.pick(U32, _, U32.sub(u, 48), - Bool.pick(U32, Bool.and(U32.is_le(65, u), U32.is_le(u, 70)), - U32.sub(u, 55), - Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), - U32.sub(u, 87), 16))) == U32.sub(u, 55) : U32} - %e2 : {Bool.pick(U32, _, U32.sub(u, 55), - Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), - U32.sub(u, 87), 16)) == U32.sub(u, 55) : U32} - {==} - -def Laws.ref_digit_hex_lower(u, e1, e2, e3): - %e1 : {Bool.pick(U32, _, U32.sub(u, 48), - Bool.pick(U32, Bool.and(U32.is_le(65, u), U32.is_le(u, 70)), - U32.sub(u, 55), - Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), - U32.sub(u, 87), 16))) == U32.sub(u, 87) : U32} - %e2 : {Bool.pick(U32, _, U32.sub(u, 55), - Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), - U32.sub(u, 87), 16)) == U32.sub(u, 87) : U32} - %e3 : {Bool.pick(U32, _, U32.sub(u, 87), 16) == U32.sub(u, 87) : U32} - {==} - -def Laws.ref_digit_hex_none(u, e1, e2, e3): - %e1 : {Bool.pick(U32, _, U32.sub(u, 48), - Bool.pick(U32, Bool.and(U32.is_le(65, u), U32.is_le(u, 70)), - U32.sub(u, 55), - Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), - U32.sub(u, 87), 16))) == 16 : U32} - %e2 : {Bool.pick(U32, _, U32.sub(u, 55), - Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), - U32.sub(u, 87), 16)) == 16 : U32} - %e3 : {Bool.pick(U32, _, U32.sub(u, 87), 16) == 16 : U32} - {==} - -def Laws.ref_grow(_hex, _n, _d): - {==} - -def Laws.ref_code(_n): - {==} - -def Laws.ref_code_zero(n, e): - %e : {Ref.code.zero(n, _) == SCon{Chr{65533}, ""} : String} - {==} - -def Laws.ref_code_cr(n, e1, e2): - %e1 : {Ref.code.zero(n, _) == "\r" : String} - %e2 : {Ref.code.cr(n, _) == "\r" : String} - {==} - -def Laws.ref_code_c1(n, e1, e2, e3): - %e1 : {Ref.code.zero(n, _) - == SCon{Chr{Ref.nth(Ref.w1252(), U32.to_nat(U32.sub(n, 128)))}, ""} - : String} - %e2 : {Ref.code.cr(n, _) - == SCon{Chr{Ref.nth(Ref.w1252(), U32.to_nat(U32.sub(n, 128)))}, ""} - : String} - %e3 : {Ref.code.c1(n, _) - == SCon{Chr{Ref.nth(Ref.w1252(), U32.to_nat(U32.sub(n, 128)))}, ""} - : String} - {==} - -def Laws.ref_code_bad(n, e1, e2, e3, e4): - %e1 : {Ref.code.zero(n, _) == SCon{Chr{65533}, ""} : String} - %e2 : {Ref.code.cr(n, _) == SCon{Chr{65533}, ""} : String} - %e3 : {Ref.code.c1(n, _) == SCon{Chr{65533}, ""} : String} - %e4 : {Ref.code.range(n, _) == SCon{Chr{65533}, ""} : String} - {==} - -def Laws.ref_code_dropped(n, e1, e2, e3, e4, e5): - %e1 : {Ref.code.zero(n, _) == "" : String} - %e2 : {Ref.code.cr(n, _) == "" : String} - %e3 : {Ref.code.c1(n, _) == "" : String} - %e4 : {Ref.code.range(n, _) == "" : String} - %e5 : {Ref.code.kept(n, _) == "" : String} - {==} - -def Laws.ref_code_kept(n, e1, e2, e3, e4, e5): - %e1 : {Ref.code.zero(n, _) == SCon{Chr{n}, ""} : String} - %e2 : {Ref.code.cr(n, _) == SCon{Chr{n}, ""} : String} - %e3 : {Ref.code.c1(n, _) == SCon{Chr{n}, ""} : String} - %e4 : {Ref.code.range(n, _) == SCon{Chr{n}, ""} : String} - %e5 : {Ref.code.kept(n, _) == SCon{Chr{n}, ""} : String} - {==} - -def Laws.ref_dropped(_n): - {==} - -def Laws.ref_named(_s): - {==} - -def Laws.ref_named_none(_s): - {==} - -def Laws.ref_named_some(_name, _first, _second, _s): - {==} - -def Laws.ref_best_nil(_s, _sofar): - {==} - -def Laws.ref_best_cons(_r, _t, _s, _sofar): - {==} - -def Laws.ref_step(_sofar, _name, _first, _second, _s): - {==} - -def Laws.ref_hit_miss(_sofar, _r, _name): - {==} - -def Laws.ref_hit_take(_sofar, _r, _name): - {==} - -def Laws.ref_take_longer(_sofar, _r): - {==} - -def Laws.ref_take_keep(_sofar, _r): - {==} - -def Laws.ref_len_none(_m, e): - %e : {Ref.best.len(_) == 0n : Nat} - {==} - -def Laws.ref_len_some(_name, _first, _second): - {==} - -def Laws.ref_chars_one(first, second, e): - %e : {SCon{Chr{first}, Bool.pick(String, _, "", SCon{Chr{second}, ""})} - == SCon{Chr{first}, ""} : String} - {==} - -def Laws.ref_chars_two(first, second, e): - %e : {SCon{Chr{first}, Bool.pick(String, _, "", SCon{Chr{second}, ""})} - == SCon{Chr{first}, SCon{Chr{second}, ""}} : String} - {==} - -# --- cookies sent back (SPEC.md HTTP-CK) --- - -def Laws.ck_pair(_line): - {==} - -def Laws.ck_pair_none(_u, e): - %e : {Echo.pair.cut(_) == None{} : Maybe<&2, Qs.Kv()>} - {==} - -def Laws.ck_pair_some(_before, _after): - {==} - -def Laws.ck_named_empty(_name, _value): - {==} - -def Laws.ck_named_kept(_name, _value): - {==} - -def Laws.ck_trim(_s): - {==} - -def Laws.ck_lead_nil(_s, e): - %e : {Echo.wsp.lead(_) == "" : String} - {==} - -def Laws.ck_lead_wsp(h, t, e): - %e : {Echo.wsp.lead.if(t, h, _, _u => Echo.wsp.lead(t)) == Echo.wsp.lead(t) - : String} - {==} - -def Laws.ck_lead_other(h, t, e): - %e : {Echo.wsp.lead.if(t, h, _, _u => Echo.wsp.lead(t)) == SCon{h, t} - : String} - {==} - -def Laws.ck_wsp(_c): - {==} - -def Laws.ck_echo(_lines): - {==} - -def Laws.ck_store_nil(_jar): - {==} - -def Laws.ck_store_cons(_l, _t, _jar): - {==} - -def Laws.ck_add_none(_jar): - {==} - -def Laws.ck_add_some(_jar, _name, _value): - {==} - -def Laws.ck_put_nil(_name, _value): - {==} - -def Laws.ck_put_hit(key, old, t, name, value, e): - %e : {Echo.put.at(key, old, value, t, _, _u => Echo.put(t, name, value)) - == (key, value) <> t : List<&2, Qs.Kv()>} - {==} - -def Laws.ck_put_miss(key, old, t, name, value, e): - %e : {Echo.put.at(key, old, value, t, _, _u => Echo.put(t, name, value)) - == (key, old) <> Echo.put(t, name, value) : List<&2, Qs.Kv()>} - {==} - -def Laws.ck_line_nil(_jar, e): - %e : {Echo.line(_) == "" : String} - {==} - -def Laws.ck_line_cons(_name, _value, _t): - {==} - -def Laws.ck_more_nil(_jar, e): - %e : {Echo.more(_) == "" : String} - {==} - -def Laws.ck_more_cons(_name, _value, _t): - {==} - -def Laws.ck_one(_name, _value): - {==} - -def Laws.ck_values_nil(_hs, e): - %e : {Echo.values(_) == [] : List<&2, String>} - {==} - -def Laws.ck_values_hit(_name, value, t, e): - %e : {Echo.values.put(value, Echo.values(t), _) == value <> Echo.values(t) - : List<&2, String>} - {==} - -def Laws.ck_values_miss(_name, value, t, e): - %e : {Echo.values.put(value, Echo.values(t), _) == Echo.values(t) - : List<&2, String>} - {==} - -# --- unescape after escape (SPEC.md HTTP-REF-4) --- - -# what a comparison that is not EQ cannot equal -def eq.cmp.emp(c: Cmp) -> Type: - match c: - case LT{}: - Unit - case EQ{}: - Empty - case GT{}: - Unit - -def eq.cmp.lt(e: {LT{} == EQ{} : Cmp}) -> Empty: - %e : eq.cmp.emp(_) - Unit{} - -def eq.cmp.gt(e: {GT{} == EQ{} : Cmp}) -> Empty: - %e : eq.cmp.emp(_) - Unit{} - -def eq.bool_cmp_eq(a: Bool, b: Bool, e: {Bool.cmp(a, b) == EQ{} : Cmp}) - -> {a == b : Bool}: - match a b: - case False{} False{}: - {==} - case False{} True{}: - eq.bit.gone(&2, {False{} == True{} : Bool}, eq.cmp.lt(e)) - case True{} False{}: - eq.bit.gone(&2, {True{} == False{} : Bool}, eq.cmp.gt(e)) - case True{} True{}: - {==} - -def eq.fin.t(ab: Bool, bb: Bool, t: Cmp, - e: {Word.cmp.fin(ab, bb, t) == EQ{} : Cmp}) -> {t == EQ{} : Cmp}: - match t: - case LT{}: - eq.bit.gone(&2, {LT{} == EQ{} : Cmp}, eq.cmp.lt(e)) - case EQ{}: - {==} - case GT{}: - eq.bit.gone(&2, {GT{} == EQ{} : Cmp}, eq.cmp.gt(e)) - -def eq.fin.b(ab: Bool, bb: Bool, t: Cmp, - e: {Word.cmp.fin(ab, bb, t) == EQ{} : Cmp}) -> {Bool.cmp(ab, bb) == EQ{} : Cmp}: - match t: - case LT{}: - eq.bit.gone(&2, {Bool.cmp(ab, bb) == EQ{} : Cmp}, eq.cmp.lt(e)) - case EQ{}: - e - case GT{}: - eq.bit.gone(&2, {Bool.cmp(ab, bb) == EQ{} : Cmp}, eq.cmp.gt(e)) - -def eq.word_cmp_eq(n: Nat, a: Word(n), b: Word(n), - +e: {Word.cmp(n, a, b) == EQ{} : Cmp}) -> {a == b : Word(n)}: - match n: - case 0n: - match a b: - case WNil{} WNil{}: - {==} - case 1n+(+p): - match a b: - case WCon{+ab, +at} WCon{+bb, +bt}: - be = eq.bool_cmp_eq(ab, bb, eq.fin.b(ab, bb, Word.cmp(p, at, bt), e)) - we = eq.word_cmp_eq(p, at, bt, eq.fin.t(ab, bb, Word.cmp(p, at, bt), e)) - %be : {WCon{ab, at} == WCon{_, bt} : Word(1n+p)} - %we : {WCon{ab, at} == WCon{ab, _} : Word(1n+p)} - {==} - -def eq.cmp_is_eq(t: Cmp, e: {Cmp.is_eq(t) == True{} : Bool}) - -> {t == EQ{} : Cmp}: - match t: - case LT{}: - eq.bit.absurd(&2, {LT{} == EQ{} : Cmp}, e) - case EQ{}: - {==} - case GT{}: - eq.bit.absurd(&2, {GT{} == EQ{} : Cmp}, e) - -def eq.char_eq_true(c: Char, d: Char, e: {Char.is_eq(c, d) == True{} : Bool}) - -> {c == d : Char}: - match c d: - case Chr{U32{+w}} Chr{U32{+v}}: - we = eq.word_cmp_eq(32n, w, v, eq.cmp_is_eq(Word.cmp(32n, w, v), e)) - %we : {Chr{U32{w}} == Chr{U32{_}} : Char} - {==} - -# a character that is not `&`, `<`, `>`, `"` or `'` scans as itself -def rt.other(+c: Char, x: String, e: {False{} == Char.is_eq(c, '&') : Bool}) - -> {Ref.scan(SCon{c, x}, Ref.Txt{}) == SCon{c, Ref.scan(x, Ref.Txt{})} - : String}: - %e : {Ref.text.of(c, st2 => Ref.scan(x, st2), _) - == SCon{c, Ref.scan(x, Ref.Txt{})} : String} - {==} - -def rt.sq(+c: Char, x: String, ea: {False{} == Char.is_eq(c, '&') : Bool}, - b: Bool, e: {Char.is_eq(c, '\'') == b : Bool}) - -> {Ref.scan(Html.one.sq(c, x, b), Ref.Txt{}) - == SCon{c, Ref.scan(x, Ref.Txt{})} : String}: - match b: - case True{}: - ce = Equal.sym(Char, c, '\'', eq.char_eq_true(c, '\'', e)) - %ce : {Ref.scan("'" ++ x, Ref.Txt{}) - == SCon{_, Ref.scan(x, Ref.Txt{})} : String} - {==} - case False{}: - rt.other(c, x, ea) - -def rt.dq(+c: Char, x: String, ea: {False{} == Char.is_eq(c, '&') : Bool}, - b: Bool, e: {Char.is_eq(c, '"') == b : Bool}) - -> {Ref.scan(Html.one.dq(c, x, b), Ref.Txt{}) - == SCon{c, Ref.scan(x, Ref.Txt{})} : String}: - match b: - case True{}: - ce = Equal.sym(Char, c, '"', eq.char_eq_true(c, '"', e)) - %ce : {Ref.scan(""" ++ x, Ref.Txt{}) - == SCon{_, Ref.scan(x, Ref.Txt{})} : String} - {==} - case False{}: - rt.sq(c, x, ea, Char.is_eq(c, '\''), {==}) - -def rt.gt(+c: Char, x: String, ea: {False{} == Char.is_eq(c, '&') : Bool}, - b: Bool, e: {Char.is_eq(c, '>') == b : Bool}) - -> {Ref.scan(Html.one.gt(c, x, b), Ref.Txt{}) - == SCon{c, Ref.scan(x, Ref.Txt{})} : String}: - match b: - case True{}: - ce = Equal.sym(Char, c, '>', eq.char_eq_true(c, '>', e)) - %ce : {Ref.scan(">" ++ x, Ref.Txt{}) - == SCon{_, Ref.scan(x, Ref.Txt{})} : String} - {==} - case False{}: - rt.dq(c, x, ea, Char.is_eq(c, '"'), {==}) - -def rt.lt(+c: Char, x: String, ea: {False{} == Char.is_eq(c, '&') : Bool}, - b: Bool, e: {Char.is_eq(c, '<') == b : Bool}) - -> {Ref.scan(Html.one.lt(c, x, b), Ref.Txt{}) - == SCon{c, Ref.scan(x, Ref.Txt{})} : String}: - match b: - case True{}: - ce = Equal.sym(Char, c, '<', eq.char_eq_true(c, '<', e)) - %ce : {Ref.scan("<" ++ x, Ref.Txt{}) - == SCon{_, Ref.scan(x, Ref.Txt{})} : String} - {==} - case False{}: - rt.gt(c, x, ea, Char.is_eq(c, '>'), {==}) - -def rt.amp(+c: Char, x: String, b: Bool, e: {Char.is_eq(c, '&') == b : Bool}) - -> {Ref.scan(Html.one.amp(c, x, b), Ref.Txt{}) - == SCon{c, Ref.scan(x, Ref.Txt{})} : String}: - match b: - case True{}: - ce = Equal.sym(Char, c, '&', eq.char_eq_true(c, '&', e)) - %ce : {Ref.scan("&" ++ x, Ref.Txt{}) - == SCon{_, Ref.scan(x, Ref.Txt{})} : String} - {==} - case False{}: - rt.lt(c, x, Equal.sym(Bool, Char.is_eq(c, '&'), False{}, e), - Char.is_eq(c, '<'), {==}) - -def rt.go(s: String) - -> {Ref.scan(Html.escape.onto(s, ""), Ref.Txt{}) == s : String}: - match s: - case SNil{}: - {==} - case SCon{+c, +t}: - Equal.trans(String, - Ref.scan(Html.one(c, Html.escape.onto(t, "")), Ref.Txt{}), - SCon{c, Ref.scan(Html.escape.onto(t, ""), Ref.Txt{})}, - SCon{c, t}, - rt.amp(c, Html.escape.onto(t, ""), Char.is_eq(c, '&'), {==}), - Equal.cong(String, String, y => SCon{c, y}, - Ref.scan(Html.escape.onto(t, ""), Ref.Txt{}), t, rt.go(t))) - -def Laws.ref_round(s): - rt.go(s) - -# fresh_no_store_any: fresh matches no-store first -def Laws.fresh_no_store_any( - _no_cache, - _public, - _private, - _max_age, - _expires, - _age -): - {==} - -# fresh_no_cache_any: no-store either way, then no-cache -def Laws.fresh_no_cache_any( - no_store, - _public, - _private, - _max_age, - _expires, - _age -): - match no_store: - case False{}: - {==} - case True{}: - {==} - -# cookie_age_sets holds by unfolding cookie.age -def Laws.cookie_age_sets( - _name, - _value, - _expires, - _max_age, - _domain, - _path, - _secure, - _http_only, - _same_site, - _host_only, - _age -): - {==} - -# cors_preflight_bad: preflight_req matches the request first -def Laws.cors_preflight_bad(_cfg, _why): - {==} - -# b64_go_end: go matches the input first -def Laws.b64_go_end(_phase): - {==} - -# --- the client's deadline and failures (SPEC.md HTTP-CLI-1, 2, 13, 14) --- - -def Laws.ms_none(_m, e): - %e : {Spec.wire.ms.of(_) == 60000 : U32} - {==} - -def Laws.ms_given(ms, e): - %e : {Spec.wire.ms.pick(ms, _) == ms : U32} - {==} - -def Laws.ms_zero(ms, e): - %e : {Spec.wire.ms.pick(ms, _) == 1 : U32} - {==} - -# a resolve's failure is fail.resolve, ran out of time or not -def fail.resolve.any(host: String, +port: U32, +ms: U32, +n: U32, why: String, - late: Bool) - -> {Spec.wire.fail.of(Spec.Resolve{}, host, port, ms, n, why, late) - == Spec.wire.fail.resolve(host, n, U32.is_eq(n, 2)) : String}: - match late: - case True{}: - {==} - case False{}: - {==} - -# a resolve that failed with another number names the cause -def fail.resolve.some(host: String, +n: U32, - e: {False{} == U32.is_eq(n, 2) : Bool}) - -> {Spec.wire.fail.resolve(host, n, U32.is_eq(n, 2)) - == "6\ncould not resolve " ++ host ++ ": " ++ Spec.wire.cause(n) - : String}: - %e : {Spec.wire.fail.resolve(host, n, _) - == "6\ncould not resolve " ++ host ++ ": " ++ Spec.wire.cause(n) - : String} - {==} - -# a name with no address names the host alone -def fail.resolve.none(host: String, +n: U32, - e: {True{} == U32.is_eq(n, 2) : Bool}) - -> {Spec.wire.fail.resolve(host, n, U32.is_eq(n, 2)) - == "6\ncould not resolve " ++ host : String}: - %e : {Spec.wire.fail.resolve(host, n, _) == "6\ncould not resolve " ++ host - : String} - {==} - -def Laws.fail_resolve(host, port, ms, n, e, why): - Equal.trans(String, - Spec.wire.fail(Spec.Resolve{}, host, port, ms, n, why), - Spec.wire.fail.resolve(host, n, U32.is_eq(n, 2)), - "6\ncould not resolve " ++ host ++ ": " ++ Spec.wire.cause(n), - fail.resolve.any(host, port, ms, n, why, U32.is_eq(n, 110)), - fail.resolve.some(host, n, e)) - -def Laws.fail_resolve_none(host, port, ms, n, e, why): - Equal.trans(String, - Spec.wire.fail(Spec.Resolve{}, host, port, ms, n, why), - Spec.wire.fail.resolve(host, n, U32.is_eq(n, 2)), - "6\ncould not resolve " ++ host, - fail.resolve.any(host, port, ms, n, why, U32.is_eq(n, 110)), - fail.resolve.none(host, n, e)) - -# a call that ran out of time is reported by fail.late -def fail.at.late(p: Spec.Phase, host: String, +port: U32, +ms: U32, +n: U32, - why: String, e: {True{} == U32.is_eq(n, 110) : Bool}) - -> {Spec.wire.fail(p, host, port, ms, n, why) - == Spec.wire.fail.late(p, host, port, ms, n) : String}: - %e : {Spec.wire.fail.of(p, host, port, ms, n, why, _) - == Spec.wire.fail.late(p, host, port, ms, n) : String} - {==} - -# a call that failed otherwise is reported by fail.other -def fail.at.other(p: Spec.Phase, host: String, +port: U32, +ms: U32, +n: U32, - why: String, e: {False{} == U32.is_eq(n, 110) : Bool}) - -> {Spec.wire.fail(p, host, port, ms, n, why) - == Spec.wire.fail.other(p, host, port, n, why) : String}: - %e : {Spec.wire.fail.of(p, host, port, ms, n, why, _) - == Spec.wire.fail.other(p, host, port, n, why) : String} - {==} - -def Laws.fail_connect_late(host, port, ms, n, e, why): - fail.at.late(Spec.Connect{}, host, port, ms, n, why, e) - -def Laws.fail_handshake_late(host, port, ms, n, e, why): - fail.at.late(Spec.Handshake{}, host, port, ms, n, why, e) - -def Laws.fail_send_late(host, port, ms, n, e, why): - fail.at.late(Spec.Send{}, host, port, ms, n, why, e) - -def Laws.fail_receive_late(host, port, ms, n, e, why): - fail.at.late(Spec.Receive{}, host, port, ms, n, why, e) - -def Laws.fail_connect(host, port, ms, n, e, why): - fail.at.other(Spec.Connect{}, host, port, ms, n, why, e) - -def Laws.fail_handshake(host, port, ms, n, e, why): - fail.at.other(Spec.Handshake{}, host, port, ms, n, why, e) - -def Laws.fail_send(host, port, ms, n, e, why): - fail.at.other(Spec.Send{}, host, port, ms, n, why, e) - -def Laws.fail_receive(host, port, ms, n, e, why): - fail.at.other(Spec.Receive{}, host, port, ms, n, why, e) - -def Laws.cause_is(_n): - {==} - -def Laws.errno_names(_n): - {==} - -def Laws.errno_or_none(s, e): - match s: - case SNil{}: - {==} - case SCon{h, t}: - eq.bit.absurd(&2, {Spec.wire.errno.or(SCon{h, t}) == "errno" : String}, - Equal.sym(Bool, True{}, False{}, e)) - -def Laws.errno_or_named(s, e): - match s: - case SNil{}: - eq.bit.absurd(&2, {Spec.wire.errno.or(SNil{}) == SNil{} : String}, e) - case SCon{h, t}: - {==} - -# --- what is sent (SPEC.md HTTP-CLI-10) --- - -def Laws.payload_nil(_rest): - {==} - -def Laws.payload_cons(_cp, _t, _rest): - {==} - -# --- octets after a prefix (SPEC.md HTTP-BODY-2, 3) --- - -# the first |b| characters of b ++ r are b -def take_prefix(b: String, r: String) - -> {String.take(String.append(b, r), String.length(b)) == b : String}: - match b: - case SNil{}: - match r: - case SNil{}: - {==} - case SCon{h, t}: - {==} - case SCon{h, t}: - Equal.cong(String, String, x => SCon{h, x}, - String.take(String.append(t, r), String.length(t)), t, - take_prefix(t, r)) - -# what follows |b| + k characters of b ++ r is what follows k of r -def drop_prefix(b: String, r: String, k: Nat) - -> {String.drop(String.append(b, r), Nat.add(String.length(b), k)) - == String.drop(r, k) : String}: - match b: - case SNil{}: - {==} - case SCon{h, t}: - drop_prefix(t, r, k) - -# b ++ r is never shorter than b -def lt_prefix(b: String, r: String) - -> {Nat.is_lt(String.length(String.append(b, r)), String.length(b)) - == False{} : Bool}: - match b: - case SNil{}: - match r: - case SNil{}: - {==} - case SCon{h, t}: - {==} - case SCon{h, t}: - lt_prefix(t, r) - -def Laws.octs_len(s): - match s: - case SNil{}: - {==} - case SCon{h, t}: - Equal.cong(Nat, Nat, x => 1n+x, Http.octs.len(t), String.length(t), - Laws.octs_len(t)) - - -def Laws.octs_take(s, n): - match s: - case SNil{}: - {==} - case SCon{h, t}: - match n: - case 0n: - {==} - case 1n+k: - match k: - case 0n: - Equal.cong(String, String, x => SCon{h, x}, Http.octs.take(t, 0n), - String.take(t, 0n), Laws.octs_take(t, 0n)) - case 1n+j: - Equal.cong(String, String, x => SCon{h, x}, - Http.octs.take(t, 1n+j), String.take(t, 1n+j), - Laws.octs_take(t, 1n+j)) - - -def Laws.octs_drop(s, n): - match s: - case SNil{}: - {==} - case SCon{h, t}: - match n: - case 0n: - {==} - case 1n+k: - match k: - case 0n: - Laws.octs_drop(t, 0n) - case 1n+j: - Laws.octs_drop(t, 1n+j) - - -def Laws.clen_prefix(st, hs, b, r): - +br = String.append(b, r) - +n = String.length(b) - %Equal.sym(Nat, Http.octs.len(br), String.length(br), Laws.octs_len(br)) - : {Http.parse.short(st, hs, n, br, Nat.is_lt(_, n)) - == Http.Reply{st, hs, b} : Http.Reply} - %Equal.sym(Bool, Nat.is_lt(String.length(br), n), False{}, lt_prefix(b, r)) - : {Http.parse.short(st, hs, n, br, _) == Http.Reply{st, hs, b} - : Http.Reply} - %Equal.sym(String, Http.octs.take(br, n), String.take(br, n), - Laws.octs_take(br, n)) - : {Http.Reply{st, hs, _} == Http.Reply{st, hs, b} : Http.Reply} - %Equal.sym(String, String.take(br, n), b, take_prefix(b, r)) - : {Http.Reply{st, hs, _} == Http.Reply{st, hs, b} : Http.Reply} - {==} - - -def Laws.chunk_data(f, h, d, r, e, el): - +a = String.append(d, "\r\n" ++ r) - +rhs = String.append(d, Http.chunks(f, Http.divide(r, "\r\n"))) - %e : {Http.chunks.more(Http.hexlen(h), a, - _u => Http.chunks(f, Http.divide(Http.octs.drop(a, - Nat.add(Http.hexlen(h), 2n)), "\r\n")), _) == rhs : String} - %el : {String.append(Http.octs.take(a, _), - Http.chunks(f, Http.divide(Http.octs.drop(a, Nat.add(_, 2n)), "\r\n"))) - == rhs : String} - +n = String.length(d) - %Equal.sym(String, Http.octs.take(a, n), String.take(a, n), - Laws.octs_take(a, n)) - : {String.append(_, Http.chunks(f, Http.divide(Http.octs.drop(a, - Nat.add(n, 2n)), "\r\n"))) == rhs : String} - %Equal.sym(String, String.take(a, n), d, take_prefix(d, "\r\n" ++ r)) - : {String.append(_, Http.chunks(f, Http.divide(Http.octs.drop(a, - Nat.add(n, 2n)), "\r\n"))) == rhs : String} - %Equal.sym(String, Http.octs.drop(a, Nat.add(n, 2n)), - String.drop(a, Nat.add(n, 2n)), Laws.octs_drop(a, Nat.add(n, 2n))) - : {String.append(d, Http.chunks(f, Http.divide(_, "\r\n"))) == rhs - : String} - %Equal.sym(String, String.drop(a, Nat.add(n, 2n)), r, - Equal.trans(String, String.drop(a, Nat.add(n, 2n)), String.drop(r, 0n), r, - drop_prefix(d, "\r\n" ++ r, 2n), eq.drop_zero(r))) - : {String.append(d, Http.chunks(f, Http.divide(_, "\r\n"))) == rhs - : String} - {==} - -def Laws.chunk_last(_f, h, after, e): - %e : {Http.chunks.more(Http.hexlen(h), after, - _u => Http.chunks(_f, Http.divide(Http.octs.drop(after, - Nat.add(Http.hexlen(h), 2n)), "\r\n")), _) == "" : String} - {==} - - -def Laws.octets_text(_o): - {==} - -def Laws.octets_toks_nil(_s, e): - %e : {Qs.octets.toks(_) == [] : List<&2, Qs.Tok>} - {==} - - -def Laws.octets_toks_cons(_c, _t): - {==} - -def Laws.octets_headers_cons(_name, _value, _t): - {==} - -def Laws.octets_headers_nil(_hs, e): - %e : {Qs.octets.headers(_) == [] : List<&2, Http.Header>} - {==} - - -def Laws.step_text(_method, _target, _hs, _body): - {==} - -def Laws.step_text_bad(_why): - {==} - -def Laws.hit_field(_b, _name, _value): - {==} - -def Laws.hit_file(_b, _name, _filename, _ctype, _clash): - {==} - -def Laws.payload_is(_req): - {==} - -def Laws.accept_idle(_a, _code, e, why): - %e : {Step.step.missed(_a, why, _) == Step.Idle{} : Step.Accept<_a>} - {==} - -def Laws.accept_missed(_a, _code, e, why): - %e : {Step.step.missed(_a, why, _) == Step.Missed{"ezhttp: accept: " ++ why} - : Step.Accept<_a>} - {==} - -def Laws.accept_took(_a, _value): - {==} - -def Laws.respond_head(_status, _hs, _n): - {==} - -def Laws.respond_text_is(_status, _hs, _body): - {==} - -def Laws.respond_head_onto(_status, _hs, _n, _rest): - {==} - -def Laws.stream_head(status, e, hs): - %e : {"HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" - ++ "Connection: close\r\n" ++ Http.headers.lines(hs) - ++ Http.te.line(Bool.not(_)) ++ "\r\n" - == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" - ++ "Connection: close\r\n" ++ Http.headers.lines(hs) - ++ "Transfer-Encoding: chunked\r\n" ++ "\r\n" : String} - {==} - -def Laws.stream_head_bare(status, e, hs): - %e : {"HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" - ++ "Connection: close\r\n" ++ Http.headers.lines(hs) - ++ Http.te.line(Bool.not(_)) ++ "\r\n" - == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" - ++ "Connection: close\r\n" ++ Http.headers.lines(hs) ++ "" - ++ "\r\n" : String} - {==} - -def Laws.chunk_line(_n): - {==} - -def Laws.hex_at(_n, _k): - {==} - -def Laws.hex_nib(_v): - {==} - -def Laws.chunk_empty(n, e): - %e : {Http.chunk.frame.of(n, _) == Http.NoChunk{} : Http.Chunk} - {==} - -def Laws.chunk_some(n, e): - %e : {Http.chunk.frame.of(n, _) == Http.Chunk{Http.chunk.line(n)} : Http.Chunk} - {==} - -def Laws.chunk_end(_s, e): - %e : {Http.chunk.end() ++ _ == "0\r\n\r\n" : String} - {==} - -def Laws.bare_head(_status): - {==} - -def Laws.bare_other(_method, e, status): - %e : {Bool.or(_, Http.status.nocontent(status)) == Http.status.nocontent(status) - : Bool} - {==} - -def Laws.nocontent_is(_code): - {==} - -def Laws.step_octets(_method, _target, _hs, _body): - {==} - -# --- reading packed octets, read by read (SPEC.md HTTP-CLI-6) --- - -def Laws.read_closed(_h): - {==} - -def Laws.skip_short(b, n, e): - match b: - case SNil{}: - Equal.cong(Nat, Http.Skip, x => Http.Short{x}, n, Nat.sub(n, 0n), - Equal.sym(Nat, Nat.sub(n, 0n), n, sub_zero(n))) - case SCon{+h, +t}: - match n: - case 0n: - eq.bit.absurd(&2, {Http.octs.skip(SCon{h, t}, 0n) - == Http.Short{Nat.sub(0n, String.length(SCon{h, t}))} : Http.Skip}, e) - case 1n+k: - match k: - case 0n: - Laws.skip_short(t, 0n, e) - case 1n+j: - Laws.skip_short(t, 1n+j, e) - -def Laws.hear_count(h, b, e): - match h: - case Http.Heads{fuel, asked, raw}: - eq.bit.absurd(&2, {Http.hear.more.n(Http.Heads{fuel, asked, raw}, String.length(b)) - == Http.hear.more(Http.Heads{fuel, asked, raw}, b) : Http.Hear}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.Whole{}: - {==} - case Http.Never{}: - {==} - case Http.Count{n, have}: - Equal.cong(Nat, Http.Hear, x => Http.Count{n, Nat.add(have, x)}, - String.length(b), Http.octs.len(b), - Equal.sym(Nat, Http.octs.len(b), String.length(b), Laws.octs_len(b))) - case Http.Walking{w}: - match w: - case Http.Ended{}: - eq.bit.absurd(&2, {Http.hear.more.n(Http.Walking{Http.Ended{}}, String.length(b)) - == Http.hear.more(Http.Walking{Http.Ended{}}, b) : Http.Hear}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.Hold{fuel, rest}: - eq.bit.absurd(&2, {Http.hear.more.n(Http.Walking{Http.Hold{fuel, rest}}, String.length(b)) - == Http.hear.more(Http.Walking{Http.Hold{fuel, rest}}, b) : Http.Hear}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.Tail{fuel, left, rest}: - eq.bit.absurd(&2, {Http.hear.more.n(Http.Walking{Http.Tail{fuel, left, rest}}, String.length(b)) - == Http.hear.more(Http.Walking{Http.Tail{fuel, left, rest}}, b) : Http.Hear}, - Equal.sym(Bool, True{}, False{}, e)) - case Http.Owed{+g, +owed}: - +f = Nat.add(g, String.length(b)) - +lt = Equal.sym(Bool, Nat.is_lt(owed, String.length(b)), False{}, - eq.not.true(Nat.is_lt(owed, String.length(b)), Equal.sym(Bool, True{}, - Bool.not(Nat.is_lt(owed, String.length(b))), e))) - Equal.cong(Http.Skip, Http.Hear, - x => Http.Walking{Http.walk.data(f, x, s => Http.walk(f, s))}, - Http.Short{Nat.sub(owed, String.length(b))}, Http.octs.skip(b, owed), - Equal.sym(Http.Skip, Http.octs.skip(b, owed), Http.Short{Nat.sub(owed, String.length(b))}, - Laws.skip_short(b, owed, lt))) - -def Laws.read_count(h, b, e1, e2): - match b: - case SNil{}: - eq.bit.absurd(&2, {Spec.wire.read.count(h, 0n) - == Some{Spec.wire.read.next(h, "")} : Maybe<&2, Spec.Read>}, e1) - case SCon{+c, +t}: - +L = String.length(SCon{c, t}) - +e = e2 - %e : {Spec.wire.read.counted(h, L, _) - == Some{Spec.wire.read.next(h, SCon{c, t})} : Maybe<&2, Spec.Read>} - Equal.cong(Http.Hear, Maybe<&2, Spec.Read>, x => Some{Spec.wire.read.with(x)}, - Http.hear.more.n(h, L), Http.hear.more(h, SCon{c, t}), - Laws.hear_count(h, SCon{c, t}, e)) - -def Laws.read_whole(method, a, text, e1, e2): - +H0 = Http.hear.of(method, a) - +J = a ++ text - %e1 : {Spec.wire.read.got(H0, text, _) == Spec.Heard{} : Spec.Read} - %Equal.sym(Http.Hear, Http.hear.more(H0, text), Http.hear.of(method, J), - Laws.hear_more(method, a, text)) - : {Spec.wire.read.done(_, Http.hear.done(_)) == Spec.Heard{} : Spec.Read} - %Equal.sym(Bool, Http.hear.done(Http.hear.of(method, J)), Http.whole(method, J), - Laws.hear_done(method, J)) - : {Spec.wire.read.done(Http.hear.of(method, J), _) == Spec.Heard{} : Spec.Read} - %e2 : {Spec.wire.read.done(Http.hear.of(method, J), _) == Spec.Heard{} : Spec.Read} - {==} - -def Laws.read_more(method, a, text, e1, e2): - +H0 = Http.hear.of(method, a) - +J = a ++ text - %e1 : {Spec.wire.read.got(H0, text, _) == Spec.Again{Http.hear.of(method, J)} - : Spec.Read} - %Equal.sym(Http.Hear, Http.hear.more(H0, text), Http.hear.of(method, J), - Laws.hear_more(method, a, text)) - : {Spec.wire.read.done(_, Http.hear.done(_)) - == Spec.Again{Http.hear.of(method, J)} : Spec.Read} - %Equal.sym(Bool, Http.hear.done(Http.hear.of(method, J)), Http.whole(method, J), - Laws.hear_done(method, J)) - : {Spec.wire.read.done(Http.hear.of(method, J), _) - == Spec.Again{Http.hear.of(method, J)} : Spec.Read} - %e2 : {Spec.wire.read.done(Http.hear.of(method, J), _) - == Spec.Again{Http.hear.of(method, J)} : Spec.Read} - {==} - -# --- a response planned from its head (SPEC.md HTTP-CLI-15) --- - -def Laws.plan_none(_ls): - {==} - -def Laws.plan_disagree(n, ls, e): - %e : {Http.plan.agree(n, Http.head.list(ls), Http.head.go(ls, "transfer-encoding"), - Http.head.values(ls, "content-length"), _) - == Http.Unplanned{"Content-Length values disagree"} : Http.Plan} - {==} - -def Laws.plan_conflict(n, ls, e1, e2, e3): - +V = Http.head.values(ls, "content-length") - %e1 : {Http.plan.agree(n, Http.head.list(ls), Http.head.go(ls, "transfer-encoding"), - V, _) == Http.Unplanned{"Content-Length and Transfer-Encoding conflict"} : Http.Plan} - %e2 : {Http.plan.te(n, Http.head.list(ls), V, _) == Http.Unplanned{"Content-Length and Transfer-Encoding conflict"} : Http.Plan} - %e3 : {Http.plan.clash(n, Http.head.list(ls), _) == Http.Unplanned{"Content-Length and Transfer-Encoding conflict"} : Http.Plan} - {==} - -def Laws.plan_chunked(n, ls, e1, e2, e3): - +V = Http.head.values(ls, "content-length") - %e1 : {Http.plan.agree(n, Http.head.list(ls), Http.head.go(ls, "transfer-encoding"), - V, _) == Http.Planned{n, Http.head.list(ls), Http.Chunked{}} : Http.Plan} - %e2 : {Http.plan.te(n, Http.head.list(ls), V, _) == Http.Planned{n, Http.head.list(ls), Http.Chunked{}} : Http.Plan} - %e3 : {Http.plan.clash(n, Http.head.list(ls), _) == Http.Planned{n, Http.head.list(ls), Http.Chunked{}} : Http.Plan} - {==} - -def Laws.plan_sized(n, ls, e1, e2, k, e3): - +V = Http.head.values(ls, "content-length") - %e1 : {Http.plan.agree(n, Http.head.list(ls), Http.head.go(ls, "transfer-encoding"), - V, _) == Http.Planned{n, Http.head.list(ls), Http.Sized{k}} : Http.Plan} - %e2 : {Http.plan.te(n, Http.head.list(ls), V, _) == Http.Planned{n, Http.head.list(ls), Http.Sized{k}} : Http.Plan} - %e3 : {Http.plan.len(n, Http.head.list(ls), _) == Http.Planned{n, Http.head.list(ls), Http.Sized{k}} : Http.Plan} - {==} - -def Laws.plan_closed(n, ls, e1, e2, e3): - +V = Http.head.values(ls, "content-length") - %e1 : {Http.plan.agree(n, Http.head.list(ls), Http.head.go(ls, "transfer-encoding"), - V, _) == Http.Planned{n, Http.head.list(ls), Http.Closed{}} : Http.Plan} - %e2 : {Http.plan.te(n, Http.head.list(ls), V, _) == Http.Planned{n, Http.head.list(ls), Http.Closed{}} : Http.Plan} - %e3 : {Http.plan.len(n, Http.head.list(ls), _) == Http.Planned{n, Http.head.list(ls), Http.Closed{}} : Http.Plan} - {==} - -def Laws.plan_status(_m, _ls, _body): - {==} - -law final_of: - for b: Bool - for +m: Maybe<&2, U32> - for +ls: List<&2, String> - for +after: String - {Http.parse.to.final(m, ls, after, b) - == Http.plan.text(Http.head.final.of(b, m, ls), after) : Http.Reply} - -def final_of(b, m, _ls, _after): - match b: - case True{}: - match m: - case None{}: - {==} - case Some{n}: - {==} - case False{}: - {==} - -def Laws.plan_final(f, head, before, after, e): - +L = String.lines(before) - +M = Http.status.of(L) - %e : {Http.parse.to.step(head, L, M, after, _, cut => Http.parse.to.go(f, head, cut)) - == Http.plan.text(Http.head.final(head, L), after) : Http.Reply} - final_of(Bool.or(head, Http.status.empty(M)), M, L, after) - -law size_ready: - for +after: String - for +n: Nat - for r: Bool - {Http.chunks.next.of(False{}, r, after, n) == Http.chunk.next.data(after, n, r) - : Http.Next} - -def size_ready(_after, _n, r): - match r: - case True{}: - {==} - case False{}: - {==} - -law size_zero: - for +n: Nat - for +after: String - for z: Bool - {Http.chunks.next.of(z, Http.chunks.ready(after, n), after, n) - == Http.chunk.next(Http.chunk.size.n(n, z), after) : Http.Next} - -def size_zero(n, after, z): - match z: - case True{}: - {==} - case False{}: - size_ready(after, n, Http.chunks.ready(after, n)) - -law size_hex: - for +before: String - for +after: String - for ok: Bool - {Http.chunks.next.hex(before, after, ok) - == Http.chunk.next(Http.chunk.size.of(before, ok), after) : Http.Next} - -def size_hex(before, after, ok): - match ok: - case False{}: - {==} - case True{}: - size_zero(Http.hexlen(before), after, Nat.is_eq(Http.hexlen(before), 0n)) - -def Laws.size_next(before, after): - size_hex(before, after, Http.hex.well(Http.chunks.token(before))) +import ./proofs/AbsoluteFormTargetRejected.bend as PAbsoluteFormTargetRejected +import ./proofs/AbsoluteLocationsResolve.bend as PAbsoluteLocationsResolve +import ./proofs/BadRequestAnswers400.bend as PBadRequestAnswers400 +import ./proofs/BadRequestShowIsMarked.bend as PBadRequestShowIsMarked +import ./proofs/BareRepliesWriteHeadOnly.bend as PBareRepliesWriteHeadOnly +import ./proofs/Base64EmptyIsEmpty.bend as PBase64EmptyIsEmpty +import ./proofs/Base64EncodesF.bend as PBase64EncodesF +import ./proofs/Base64EncodesFo.bend as PBase64EncodesFo +import ./proofs/Base64EncodesFoo.bend as PBase64EncodesFoo +import ./proofs/Base64EncodesUserPass.bend as PBase64EncodesUserPass +import ./proofs/Base64PadsFinalQuantum.bend as PBase64PadsFinalQuantum +import ./proofs/BasicIsBase64OfUserPassword.bend as PBasicIsBase64OfUserPassword +import ./proofs/BearerIsAuthorization.bend as PBearerIsAuthorization +import ./proofs/BearerTokenAcceptsPlain.bend as PBearerTokenAcceptsPlain +import ./proofs/BearerTokenRejectsNewline.bend as PBearerTokenRejectsNewline +import ./proofs/BearerTokenRejectsSpace.bend as PBearerTokenRejectsSpace +import ./proofs/BearerTokenRejectsTab.bend as PBearerTokenRejectsTab +import ./proofs/CacheDirectiveNamesIgnoreCase.bend as PCacheDirectiveNamesIgnoreCase +import ./proofs/CacheIgnoresUnknownDirective.bend as PCacheIgnoresUnknownDirective +import ./proofs/CacheMaxAgeIgnoresJunk.bend as PCacheMaxAgeIgnoresJunk +import ./proofs/CacheReadsKnownDirectives.bend as PCacheReadsKnownDirectives +import ./proofs/CacheRecordsExpires.bend as PCacheRecordsExpires +import ./proofs/CacheRecordsMaxAge.bend as PCacheRecordsMaxAge +import ./proofs/CacheRecordsNoCache.bend as PCacheRecordsNoCache +import ./proofs/CacheRecordsPrivate.bend as PCacheRecordsPrivate +import ./proofs/ChunkDataUnchanged.bend as PChunkDataUnchanged +import ./proofs/ChunkSizeHexIgnoresCase.bend as PChunkSizeHexIgnoresCase +import ./proofs/ChunkSizeIgnoresExtension.bend as PChunkSizeIgnoresExtension +import ./proofs/ChunkSizeReadsHexSkipsExtension.bend as PChunkSizeReadsHexSkipsExtension +import ./proofs/ChunkedBodyJoinsChunks.bend as PChunkedBodyJoinsChunks +import ./proofs/ChunkedNameIgnoresCase.bend as PChunkedNameIgnoresCase +import ./proofs/ChunksStopWithoutFuel.bend as PChunksStopWithoutFuel +import ./proofs/ContentLengthFramesBody.bend as PContentLengthFramesBody +import ./proofs/ContentLengthReadsDecimal.bend as PContentLengthReadsDecimal +import ./proofs/ContentLengthRejectsJunk.bend as PContentLengthRejectsJunk +import ./proofs/ContentLengthTakesPrefix.bend as PContentLengthTakesPrefix +import ./proofs/ContinuationLineRejected.bend as PContinuationLineRejected +import ./proofs/CookieAliveUnderMaxAge.bend as PCookieAliveUnderMaxAge +import ./proofs/CookieAttributesJoinWithSemicolons.bend as PCookieAttributesJoinWithSemicolons +import ./proofs/CookieDefaultPathFallsBackToRoot.bend as PCookieDefaultPathFallsBackToRoot +import ./proofs/CookieDefaultPathIsDirectory.bend as PCookieDefaultPathIsDirectory +import ./proofs/CookieDefaultPathNeedsTwoSlashes.bend as PCookieDefaultPathNeedsTwoSlashes +import ./proofs/CookieDefaultPathOmitsTrailingSlash.bend as PCookieDefaultPathOmitsTrailingSlash +import ./proofs/CookieDomainLowercased.bend as PCookieDomainLowercased +import ./proofs/CookieDomainMatchesDotSuffix.bend as PCookieDomainMatchesDotSuffix +import ./proofs/CookieDomainNeedsDotBeforeSuffix.bend as PCookieDomainNeedsDotBeforeSuffix +import ./proofs/CookieHeaderEchoesStore.bend as PCookieHeaderEchoesStore +import ./proofs/CookieHostOnlyNotSentElsewhere.bend as PCookieHostOnlyNotSentElsewhere +import ./proofs/CookieHostOnlySentToHost.bend as PCookieHostOnlySentToHost +import ./proofs/CookieHostOnlyStoresHost.bend as PCookieHostOnlyStoresHost +import ./proofs/CookieIdenticalPathsMatch.bend as PCookieIdenticalPathsMatch +import ./proofs/CookieInsecureSentOnCleartext.bend as PCookieInsecureSentOnCleartext +import ./proofs/CookieIpHostMatchesExactly.bend as PCookieIpHostMatchesExactly +import ./proofs/CookieIpHostNoSuffixMatch.bend as PCookieIpHostNoSuffixMatch +import ./proofs/CookieLongerPathFails.bend as PCookieLongerPathFails +import ./proofs/CookieMaxAgeIgnoresJunk.bend as PCookieMaxAgeIgnoresJunk +import ./proofs/CookieMaxAgeOneStaleAtOne.bend as PCookieMaxAgeOneStaleAtOne +import ./proofs/CookieMaxAgeSetsAge.bend as PCookieMaxAgeSetsAge +import ./proofs/CookieMaxAgeZeroNotAlive.bend as PCookieMaxAgeZeroNotAlive +import ./proofs/CookieNameIsNotIpv4.bend as PCookieNameIsNotIpv4 +import ./proofs/CookieNoDomainNotSentElsewhere.bend as PCookieNoDomainNotSentElsewhere +import ./proofs/CookiePathMatchesSlashPrefix.bend as PCookiePathMatchesSlashPrefix +import ./proofs/CookiePathNeedsSegmentBoundary.bend as PCookiePathNeedsSegmentBoundary +import ./proofs/CookieRecognizesIpv4Literal.bend as PCookieRecognizesIpv4Literal +import ./proofs/CookieRejectsEmptyName.bend as PCookieRejectsEmptyName +import ./proofs/CookieRequestJoinsPairs.bend as PCookieRequestJoinsPairs +import ./proofs/CookieRootPathMatchesLonger.bend as PCookieRootPathMatchesLonger +import ./proofs/CookieSameSiteFoldsCase.bend as PCookieSameSiteFoldsCase +import ./proofs/CookieSameSiteNoneCanonical.bend as PCookieSameSiteNoneCanonical +import ./proofs/CookieSecureNeedsTls.bend as PCookieSecureNeedsTls +import ./proofs/CookieSecureSentOnTls.bend as PCookieSecureSentOnTls +import ./proofs/CookieSessionStaysAlive.bend as PCookieSessionStaysAlive +import ./proofs/CookieSlashEndedPathIsPrefix.bend as PCookieSlashEndedPathIsPrefix +import ./proofs/CookieUnknownSameSiteDropped.bend as PCookieUnknownSameSiteDropped +import ./proofs/CorsAllowListReflectsOrigin.bend as PCorsAllowListReflectsOrigin +import ./proofs/CorsAllowedPreflightIs200.bend as PCorsAllowedPreflightIs200 +import ./proofs/CorsBadPreflightIs400.bend as PCorsBadPreflightIs400 +import ./proofs/CorsCredentialsKeepExplicitOrigin.bend as PCorsCredentialsKeepExplicitOrigin +import ./proofs/CorsCredentialsReflectOrigin.bend as PCorsCredentialsReflectOrigin +import ./proofs/CorsGetIsNotPreflight.bend as PCorsGetIsNotPreflight +import ./proofs/CorsNoVaryForStar.bend as PCorsNoVaryForStar +import ./proofs/CorsNoVaryWhenRefused.bend as PCorsNoVaryWhenRefused +import ./proofs/CorsOptionsWithMethodIsPreflight.bend as PCorsOptionsWithMethodIsPreflight +import ./proofs/CorsPreflightAnswersStar.bend as PCorsPreflightAnswersStar +import ./proofs/CorsPreflightHeaderDenied.bend as PCorsPreflightHeaderDenied +import ./proofs/CorsPreflightHeadersIgnoreCase.bend as PCorsPreflightHeadersIgnoreCase +import ./proofs/CorsPreflightMethodDenied.bend as PCorsPreflightMethodDenied +import ./proofs/CorsPreflightOriginDenied.bend as PCorsPreflightOriginDenied +import ./proofs/CorsReadsOriginHeader.bend as PCorsReadsOriginHeader +import ./proofs/CorsReadsRequestHeaders.bend as PCorsReadsRequestHeaders +import ./proofs/CorsReadsRequestMethod.bend as PCorsReadsRequestMethod +import ./proofs/CorsRefusedLeavesReply.bend as PCorsRefusedLeavesReply +import ./proofs/CorsRefusesMissingOrigin.bend as PCorsRefusesMissingOrigin +import ./proofs/CorsSimpleAnswersStar.bend as PCorsSimpleAnswersStar +import ./proofs/CorsSimpleKeepsBody.bend as PCorsSimpleKeepsBody +import ./proofs/CorsStarStaysStar.bend as PCorsStarStaysStar +import ./proofs/CorsVaryOnSpecificOrigin.bend as PCorsVaryOnSpecificOrigin +import ./proofs/CutAtFirstEquals.bend as PCutAtFirstEquals +import ./proofs/DeadlineIsTimeout.bend as PDeadlineIsTimeout +import ./proofs/DechunkEmptyIsEmpty.bend as PDechunkEmptyIsEmpty +import ./proofs/DechunkIgnoresExtension.bend as PDechunkIgnoresExtension +import ./proofs/DechunkJoinsTwoChunks.bend as PDechunkJoinsTwoChunks +import ./proofs/DechunkLastChunkAloneIsEmpty.bend as PDechunkLastChunkAloneIsEmpty +import ./proofs/DechunkRfcExample.bend as PDechunkRfcExample +import ./proofs/DechunkStopsAtZero.bend as PDechunkStopsAtZero +import ./proofs/DeleteIsIdempotent.bend as PDeleteIsIdempotent +import ./proofs/DeleteNotSafe.bend as PDeleteNotSafe +import ./proofs/DeleteRequestLine.bend as PDeleteRequestLine +import ./proofs/DotSegmentsRemoved.bend as PDotSegmentsRemoved +import ./proofs/EqualContentLengthsAreOne.bend as PEqualContentLengthsAreOne +import ./proofs/ErrnoNamesAreLinux.bend as PErrnoNamesAreLinux +import ./proofs/ExactContentLengthBodyKept.bend as PExactContentLengthBodyKept +import ./proofs/ExchangeCarriesMethod.bend as PExchangeCarriesMethod +import ./proofs/FailedEffectsLoseConnection.bend as PFailedEffectsLoseConnection +import ./proofs/FailuresReportedByPhase.bend as PFailuresReportedByPhase +import ./proofs/FieldLookupFindsFirst.bend as PFieldLookupFindsFirst +import ./proofs/FieldLookupIgnoresCase.bend as PFieldLookupIgnoresCase +import ./proofs/FieldNamesIgnoreCase.bend as PFieldNamesIgnoreCase +import ./proofs/FieldValueTrimsWhitespace.bend as PFieldValueTrimsWhitespace +import ./proofs/FirstIsFirstWord.bend as PFirstIsFirstWord +import ./proofs/FormBodyLayout.bend as PFormBodyLayout +import ./proofs/FormBoundaryRules.bend as PFormBoundaryRules +import ./proofs/FormNamesEscaped.bend as PFormNamesEscaped +import ./proofs/FormPartHeaders.bend as PFormPartHeaders +import ./proofs/FormRefusesBadRequests.bend as PFormRefusesBadRequests +import ./proofs/FreshUnderMaxAge.bend as PFreshUnderMaxAge +import ./proofs/GetAsteriskTargetRejected.bend as PGetAsteriskTargetRejected +import ./proofs/GetIsIdempotent.bend as PGetIsIdempotent +import ./proofs/GetIsSafe.bend as PGetIsSafe +import ./proofs/GetOmitsContentLength.bend as PGetOmitsContentLength +import ./proofs/GetRequestLine.bend as PGetRequestLine +import ./proofs/HandlerGetsRequestText.bend as PHandlerGetsRequestText +import ./proofs/HeadAndNoContentHaveNoBody.bend as PHeadAndNoContentHaveNoBody +import ./proofs/HeadIsIdempotent.bend as PHeadIsIdempotent +import ./proofs/HeadIsSafe.bend as PHeadIsSafe +import ./proofs/HeadPlannedBeforeBody.bend as PHeadPlannedBeforeBody +import ./proofs/HeadReplyDropsBody.bend as PHeadReplyDropsBody +import ./proofs/HeadRequestLine.bend as PHeadRequestLine +import ./proofs/HtmlEscapeCharByChar.bend as PHtmlEscapeCharByChar +import ./proofs/HtmlEscapeIsSafe.bend as PHtmlEscapeIsSafe +import ./proofs/HtmlEscapesFiveChars.bend as PHtmlEscapesFiveChars +import ./proofs/HtmlKeepsOtherChars.bend as PHtmlKeepsOtherChars +import ./proofs/HttpsDefaultsTo443.bend as PHttpsDefaultsTo443 +import ./proofs/InterimResponsesSkipped.bend as PInterimResponsesSkipped +import ./proofs/InterimStatusIs1xx.bend as PInterimStatusIs1xx +import ./proofs/JoinedPiecesAreText.bend as PJoinedPiecesAreText +import ./proofs/JsonRejectsNonJson.bend as PJsonRejectsNonJson +import ./proofs/JsonStringIsTextBody.bend as PJsonStringIsTextBody +import ./proofs/JsonStringParsesBack.bend as PJsonStringParsesBack +import ./proofs/LocationResolvedBySort.bend as PLocationResolvedBySort +import ./proofs/LocationSplitsAtQuery.bend as PLocationSplitsAtQuery +import ./proofs/LowercaseGetNotSafe.bend as PLowercaseGetNotSafe +import ./proofs/MaxAgeOneFreshThenStale.bend as PMaxAgeOneFreshThenStale +import ./proofs/MaxAgeOverridesExpires.bend as PMaxAgeOverridesExpires +import ./proofs/MaxAgeZeroIsStale.bend as PMaxAgeZeroIsStale +import ./proofs/MethodTokensAreCaseSensitive.bend as PMethodTokensAreCaseSensitive +import ./proofs/MissingFieldIsEmpty.bend as PMissingFieldIsEmpty +import ./proofs/NamedReferencesLongestMatch.bend as PNamedReferencesLongestMatch +import ./proofs/NoCacheNeverFresh.bend as PNoCacheNeverFresh +import ./proofs/NoCacheNotFresh.bend as PNoCacheNotFresh +import ./proofs/NoMaxAgeNotFresh.bend as PNoMaxAgeNotFresh +import ./proofs/NoStoreNeverFresh.bend as PNoStoreNeverFresh +import ./proofs/NoStoreNotFresh.bend as PNoStoreNotFresh +import ./proofs/NonHeadReplyKeepsBody.bend as PNonHeadReplyKeepsBody +import ./proofs/NonHexChunkSizeTorn.bend as PNonHexChunkSizeTorn +import ./proofs/NumericReferencesDecode.bend as PNumericReferencesDecode +import ./proofs/ObsFoldRejected.bend as PObsFoldRejected +import ./proofs/OctetBodiesSentUnchanged.bend as POctetBodiesSentUnchanged +import ./proofs/OctetsDecodeAsUtf8.bend as POctetsDecodeAsUtf8 +import ./proofs/OneOctetPerChar.bend as POneOctetPerChar +import ./proofs/OnlyGetFollows.bend as POnlyGetFollows +import ./proofs/OptionsAsteriskNamesServer.bend as POptionsAsteriskNamesServer +import ./proofs/OptionsIsIdempotent.bend as POptionsIsIdempotent +import ./proofs/OptionsIsSafe.bend as POptionsIsSafe +import ./proofs/OptionsRequestLine.bend as POptionsRequestLine +import ./proofs/OriginFormKeepsQuery.bend as POriginFormKeepsQuery +import ./proofs/OtherSchemesDefaultTo80.bend as POtherSchemesDefaultTo80 +import ./proofs/OtherStatusesStopChain.bend as POtherStatusesStopChain +import ./proofs/ParseQuerySplitsPairs.bend as PParseQuerySplitsPairs +import ./proofs/PatchNotIdempotent.bend as PPatchNotIdempotent +import ./proofs/PathFormKeepsSlash.bend as PPathFormKeepsSlash +import ./proofs/PhraseTableLookup.bend as PPhraseTableLookup +import ./proofs/PostNotIdempotent.bend as PPostNotIdempotent +import ./proofs/PostNotSafe.bend as PPostNotSafe +import ./proofs/PostRequestLine.bend as PPostRequestLine +import ./proofs/PostWritesContentLength.bend as PPostWritesContentLength +import ./proofs/PutIsIdempotent.bend as PPutIsIdempotent +import ./proofs/PutNotSafe.bend as PPutNotSafe +import ./proofs/PutRequestLine.bend as PPutRequestLine +import ./proofs/QueryGetLastWins.bend as PQueryGetLastWins +import ./proofs/QuoteEscapesUtf8Bytes.bend as PQuoteEscapesUtf8Bytes +import ./proofs/QuoteKeepsUnreserved.bend as PQuoteKeepsUnreserved +import ./proofs/QuoteOutputIsFramed.bend as PQuoteOutputIsFramed +import ./proofs/QuotePlusWritesSpaces.bend as PQuotePlusWritesSpaces +import ./proofs/ReadStateMatchesWholeText.bend as PReadStateMatchesWholeText +import ./proofs/ReadsJudgedByCount.bend as PReadsJudgedByCount +import ./proofs/Reason200IsOk.bend as PReason200IsOk +import ./proofs/Reason201IsCreated.bend as PReason201IsCreated +import ./proofs/Reason204IsNoContent.bend as PReason204IsNoContent +import ./proofs/Reason400IsBadRequest.bend as PReason400IsBadRequest +import ./proofs/Reason404IsNotFound.bend as PReason404IsNotFound +import ./proofs/Reason405IsMethodNotAllowed.bend as PReason405IsMethodNotAllowed +import ./proofs/Reason500IsInternalServerError.bend as PReason500IsInternalServerError +import ./proofs/ReasonPhraseFromFinalHead.bend as PReasonPhraseFromFinalHead +import ./proofs/ReasonPhrasesFollowRfc.bend as PReasonPhrasesFollowRfc +import ./proofs/RedirectsFollowLocation.bend as PRedirectsFollowLocation +import ./proofs/RelativeLocationsMerge.bend as PRelativeLocationsMerge +import ./proofs/RelativeTargetRejected.bend as PRelativeTargetRejected +import ./proofs/ReplyHeadFramesBody.bend as PReplyHeadFramesBody +import ./proofs/RequestBodyOfContentLengthKept.bend as PRequestBodyOfContentLengthKept +import ./proofs/RequestDisagreeingContentLengthsRejected.bend as PRequestDisagreeingContentLengthsRejected +import ./proofs/RequestEqualContentLengthsFrameOneBody.bend as PRequestEqualContentLengthsFrameOneBody +import ./proofs/RequestHeaderFieldsOrdered.bend as PRequestHeaderFieldsOrdered +import ./proofs/RequestKeepsFieldNameSpelling.bend as PRequestKeepsFieldNameSpelling +import ./proofs/RequestLineAndHost.bend as PRequestLineAndHost +import ./proofs/RequestLineOpensWithMethod.bend as PRequestLineOpensWithMethod +import ./proofs/RequestNeedsBlankLine.bend as PRequestNeedsBlankLine +import ./proofs/RequestNeedsRequestLine.bend as PRequestNeedsRequestLine +import ./proofs/RequestWithBothFramingsRejected.bend as PRequestWithBothFramingsRejected +import ./proofs/Respond200KeepsBody.bend as PRespond200KeepsBody +import ./proofs/Respond204DropsBody.bend as PRespond204DropsBody +import ./proofs/Respond304DropsBody.bend as PRespond304DropsBody +import ./proofs/RespondOpensWithVersion.bend as PRespondOpensWithVersion +import ./proofs/ResponseCompleteByFraming.bend as PResponseCompleteByFraming +import ./proofs/ResponseDisagreeingContentLengthsTorn.bend as PResponseDisagreeingContentLengthsTorn +import ./proofs/ResponseNeedsBlankLine.bend as PResponseNeedsBlankLine +import ./proofs/ResponseNeedsStatus.bend as PResponseNeedsStatus +import ./proofs/ResponseWithBothFramingsTorn.bend as PResponseWithBothFramingsTorn +import ./proofs/SchemeBeforeFirstColon.bend as PSchemeBeforeFirstColon +import ./proofs/SecondIsNextWord.bend as PSecondIsNextWord +import ./proofs/SetCookieFieldsSelected.bend as PSetCookieFieldsSelected +import ./proofs/SetCookieParsesBack.bend as PSetCookieParsesBack +import ./proofs/SetCookieReadsPair.bend as PSetCookieReadsPair +import ./proofs/ShortRequestBodyIsBad.bend as PShortRequestBodyIsBad +import ./proofs/ShowReplyOpensWithStatus.bend as PShowReplyOpensWithStatus +import ./proofs/ShowTornIsMarked.bend as PShowTornIsMarked +import ./proofs/SplitAtCharacter.bend as PSplitAtCharacter +import ./proofs/StaleAtMaxAge.bend as PStaleAtMaxAge +import ./proofs/StreamedRepliesAreChunked.bend as PStreamedRepliesAreChunked +import ./proofs/TakeDropSplitAtOneOffset.bend as PTakeDropSplitAtOneOffset +import ./proofs/TimeoutZeroIsOneMs.bend as PTimeoutZeroIsOneMs +import ./proofs/TimeoutsReadAsTimedOut.bend as PTimeoutsReadAsTimedOut +import ./proofs/UnescapeFindsReferences.bend as PUnescapeFindsReferences +import ./proofs/UnescapeUndoesEscape.bend as PUnescapeUndoesEscape +import ./proofs/UnframedBodyIsRest.bend as PUnframedBodyIsRest +import ./proofs/UnknownStatusHasEmptyReason.bend as PUnknownStatusHasEmptyReason +import ./proofs/UnquotePlusReadsSpaces.bend as PUnquotePlusReadsSpaces +import ./proofs/UnquoteReadsEscapes.bend as PUnquoteReadsEscapes +import ./proofs/UrlBadPortUsesDefault.bend as PUrlBadPortUsesDefault +import ./proofs/UrlEmptyPath.bend as PUrlEmptyPath +import ./proofs/UrlFragmentHidesQuery.bend as PUrlFragmentHidesQuery +import ./proofs/UrlHttpDefaultPort.bend as PUrlHttpDefaultPort +import ./proofs/UrlHttpsDefaultPort.bend as PUrlHttpsDefaultPort +import ./proofs/UrlHttpsExplicitPort.bend as PUrlHttpsExplicitPort +import ./proofs/UrlIpv4Host.bend as PUrlIpv4Host +import ./proofs/UrlIpv4Port.bend as PUrlIpv4Port +import ./proofs/UrlNeedsSlashes.bend as PUrlNeedsSlashes +import ./proofs/UrlPathIsAbsolute.bend as PUrlPathIsAbsolute +import ./proofs/UrlQueryDropsFragment.bend as PUrlQueryDropsFragment +import ./proofs/UrlQueryKeepsQuestion.bend as PUrlQueryKeepsQuestion +import ./proofs/UrlQueryOnEmptyPath.bend as PUrlQueryOnEmptyPath +import ./proofs/UrlSchemeLowercased.bend as PUrlSchemeLowercased +import ./proofs/UrlShowBadIsMarked.bend as PUrlShowBadIsMarked +import ./proofs/UrlShowOpensWithScheme.bend as PUrlShowOpensWithScheme +import ./proofs/UrlTrailingSlash.bend as PUrlTrailingSlash +import ./proofs/UrlUserinfoLastAt.bend as PUrlUserinfoLastAt +import ./proofs/UrlUserinfoStripped.bend as PUrlUserinfoStripped +import ./proofs/UrlencodeJoinsPairs.bend as PUrlencodeJoinsPairs +import ./proofs/Utf8DecodesWithReplacement.bend as PUtf8DecodesWithReplacement +import ./proofs/WithServesGetOctetsAndContext.bend as PWithServesGetOctetsAndContext diff --git a/README.md b/README.md index f6847cf..d2deac4 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,9 @@ hub package; import ezjson from the hub to read a JSON body (below). exchange failed. A `Response` has the status, the header fields and the status line's reason phrase as text (decoded from what arrived as UTF-8, U+FFFD for a byte that is not), and `bytes`: the body's octets exactly as they arrived, -packed (SPEC.md HTTP-BODY), so a binary download is byte-exact. The body is -never turned into text unless you ask: `body_text(bytes)` decodes it as UTF-8. +packed ([laws/OneOctetPerChar.bend](laws/OneOctetPerChar.bend)), so a binary +download is byte-exact. The body is never turned into text unless you ask: +`body_text(bytes)` decodes it as UTF-8. `Response` holds `Bytes`, so it is a linear value: match on it once. Content-Length and chunk sizes count octets. A response is read and framed on its packed octets, so a 20 MB download peaks near 100 MB rather than 1.35 GB. @@ -104,8 +105,10 @@ the scheme, an absolute path keeps the scheme, host and port, and a query or a relative path such as `web/` is merged with the current path, dot segments removed. An empty `Location`, a fragment, or another scheme ends the chain. The caller's headers go to every hop, credentials included, so only follow -redirects from hosts you trust with them. SPEC.md lists these guarantees -(HTTP-CLI, HTTP-REDIR). They are new after ezhttp 0.10.0. +redirects from hosts you trust with them. The client and redirect guarantees +under [laws/](laws/) state this (such as +[laws/RedirectsFollowLocation.bend](laws/RedirectsFollowLocation.bend)). They +are new after ezhttp 0.10.0. ``` def probe(url: String) -> IO(Client.Response): @@ -125,7 +128,7 @@ pass whose name (compared case-insensitively) is `Host`, `User-Agent` or `Connection` and `Content-Length` are not overridable: yours are dropped, since the client frames the exchange by `Connection: close` and computes the length from the body. Every other header is sent as you gave it, in order -(SPEC.md HTTP-CLI-8). +([laws/RequestHeaderFieldsOrdered.bend](laws/RequestHeaderFieldsOrdered.bend)). ``` import ezhttp@0.22.0.0/main.bend as Http @@ -142,12 +145,14 @@ def ingest(url: String, json: String) -> IO(Client.Response): A body is text (`text_body`, sent as its UTF-8) or octets (`octets_body` of a bend-kit-bytes `Bytes`, or `octets_list_body` of a list of values 0 to 255), sent byte for byte after the request head, with `Content-Length` counting -them (SPEC.md HTTP-CLI-10), so binary uploads reach the server unchanged. `form_body(boundary, parts)` builds a +them ([laws/OctetBodiesSentUnchanged.bend](laws/OctetBodiesSentUnchanged.bend)), +so binary uploads reach the server unchanged. `form_body(boundary, parts)` builds a `multipart/form-data` body (RFC 7578) from `form_field(name, value)` and `form_file(name, filename, content_type, bytes)` parts: it answers the `Content-Type` value to send and an octet body, or refuses a boundary that is not 1 to 70 of `A-Z a-z 0-9 - _ .`, one whose `--boundary` occurs in a part's -content, and a file content type holding CR or LF (SPEC.md HTTP-FORM). Names +content, and a file content type holding CR or LF +([laws/FormRefusesBadRequests.bend](laws/FormRefusesBadRequests.bend)). Names and filenames have `"`, CR and LF written as `%22`, `%0D` and `%0A`. A file's content is `Bytes` (bend-kit-files' `read_bytes`, for example) and is sent unchanged; the builder searches it for the delimiter with `Bytes.find`. @@ -174,8 +179,9 @@ def transcribe(url: String, key: String, wav: Bytes.Bytes) Http.form_file("file", "audio.wav", "audio/wav", wav)])) ``` -The client reads a response with its request's method in mind (SPEC.md -HTTP-CLI-9): interim 1xx responses such as `100 Continue` are skipped, and a +The client reads a response with its request's method in mind +([laws/InterimResponsesSkipped.bend](laws/InterimResponsesSkipped.bend)): interim +1xx responses such as `100 Continue` are skipped, and a response to HEAD, or with a 1xx, 204 or 304 status, has an empty body whatever its `Content-Length` or `Transfer-Encoding` say. `parse_response_to(method, raw)` is that reading; `parse_response(raw)` does @@ -197,8 +203,9 @@ the handler as text (decoded from the octets that arrived as UTF-8). A connection that fails (a failed accept, recv or send, such as a client resetting mid-request) is closed and logged to stderr as one line, `ezhttp: : `; it -counts toward `limit`. SPEC.md lists these guarantees (HTTP-SRV) and what is trusted -rather than proved. +counts toward `limit`. The server guarantees under [laws/](laws/) state this (such +as [laws/BadRequestAnswers400.bend](laws/BadRequestAnswers400.bend)), and +`src/server.bend` says what is trusted rather than proved. `http.serve_with(~C, handle, ctx, host, port, limit, max)`, `http.serve_io_with` and `http.serve_forever_with` take a context value @@ -301,7 +308,9 @@ def read(text: String) -> Maybe<&2, String>: §2.1) the way Python's `urllib.parse.quote(s)` and `quote(s, safe="")` do: unreserved characters stay, every other character becomes `%XX` per UTF-8 byte in upper-case hex, and `/` stays in a path but is `%2F` in a component. -SPEC.md lists these guarantees (HTTP-URL). They are new after ezhttp 0.9.0. +The percent-encoding guarantees under [laws/](laws/) state this (such as +[laws/QuoteKeepsUnreserved.bend](laws/QuoteKeepsUnreserved.bend)). They are +new after ezhttp 0.9.0. ``` import ezhttp@0.22.0.0/main.bend as Http @@ -312,8 +321,9 @@ def search(q: String) -> String: `html_escape` escapes text for HTML as markupsafe and Jinja's autoescape do: `&`, `<`, `>`, `"` and `'` become `&`, `<`, `>`, `"` and -`'`, and every other character stays (HTTP-HTML). It is new after ezhttp -0.10.0. +`'`, and every other character stays +([laws/HtmlEscapesFiveChars.bend](laws/HtmlEscapesFiveChars.bend)). It is new +after ezhttp 0.10.0. `unquote` and `unquote_plus` percent-decode text the way Python's `urllib.parse.unquote` and `unquote_plus` do: `%XX` (either case) becomes a @@ -325,7 +335,9 @@ ill-formed part (Python's `errors="replace"`). `quote_plus` is strings as `urlencode` and `parse_qsl(text, keep_blank_values=True)` do: a pair is `(name, value)`, `parse_query` keeps every pair in order (duplicates and blank values included), and `query_get(pairs, name)` answers the last -value for a name (HTTP-QS). They are new after ezhttp 0.14.0. +value for a name (the query-string guarantees under [laws/](laws/), such as +[laws/ParseQuerySplitsPairs.bend](laws/ParseQuerySplitsPairs.bend)). They are +new after ezhttp 0.14.0. ``` import ezhttp@0.22.0.0/main.bend as Http @@ -341,12 +353,14 @@ def link(q: String) -> String: `html.unescape` does: decimal and hex numeric references with the HTML Standard's replacements, and all 2231 of its named references (the legacy ones also without `;`, read by longest match). It undoes `html_escape` -(HTTP-REF). `cookie_from_set_cookies(values)` and +([laws/UnescapeUndoesEscape.bend](laws/UnescapeUndoesEscape.bend)). +`cookie_from_set_cookies(values)` and `cookie_from_headers(headers)` give the `Cookie` header value to send back after `Set-Cookie` fields: each one's `name=value` (RFC 6265 §5.2), no attributes, a later cookie with the same name replacing the earlier one's -value in place, joined by `; ` (§5.4) (HTTP-CK). Both are new after -ezhttp 0.14.0. +value in place, joined by `; ` (§5.4) +([laws/CookieHeaderEchoesStore.bend](laws/CookieHeaderEchoesStore.bend)). Both +are new after ezhttp 0.14.0. `parse_cookie` reads a `Set-Cookie` field value. `set_cookie` writes one. `cache_control` reads `no-store`, `no-cache`, `max-age`, `public`, and @@ -356,8 +370,11 @@ never pair with `Access-Control-Allow-Origin: *`. ## Compliance -Closed equalities in `LAWS.bend`, proved in `PROOF.bend` -(`bend PROOF.bend` prints `ALL PROOFS CHECK`). `main.bend` and +Each guarantee is one file under [laws/](laws/), its prose on top and its +laws under it, proved by the file of the same name under [proofs/](proofs/); +`LAWS.bend` and `PROOF.bend` only import them (`bend PROOF.bend` prints +`ALL PROOFS CHECK`). What ezhttp trusts rather than proves is said on the +code that assumes it. `main.bend` and `src/client.bend` reach the wire effect, so no law file imports them: the laws are stated on the pure definitions under `src/`, and each re-export in `main.bend` that bolt's `coverage` rule (an error) would flag carries a diff --git a/bench/main.bend b/bench/main.bend index 6ff6541..4b02467 100644 --- a/bench/main.bend +++ b/bench/main.bend @@ -30,7 +30,7 @@ def wrote.close(file: File, ok: Unit) -> IO(Unit): File.close(file) # force a write Result, then close the file -def wrote(pair: File & Result<&1, &1, U32 & String, Unit>) -> IO(Unit): # noqa: L001 benchmark program +def wrote(pair: File & Result<&1, &1, U32 & String, Unit>) -> IO(Unit): (file, result) = pair do IO: ok : Unit <- IO.pass(Unit, result) @@ -525,7 +525,7 @@ def handle.req(req: Msg.Request, text: String, raw: String, small: String, mediu handle.method(method, target, body, text, raw, small, medium) # pure handler: fixtures are closed literals, not captured runtime values -def handle(req: Msg.Request) -> Msg.Reply: # noqa: L001 benchmark program, no law's subject +def handle(req: Msg.Request) -> Msg.Reply: handle.req(req, Fix.fix.text(), Fix.fix.bytes(), Fix.fix.small(), Fix.fix.medium()) # listen; the handler is closed, so fixtures stay in the binary @@ -608,7 +608,8 @@ def run.json(+cmd: String, +a1: String, +a2: String, a3: String) -> IO(Unit): _u => cmd.json.ok(a1, a2), _u => run.get(cmd, a1, a2, a3)) -# parse-diff: the packed reader against the text parse (SPEC.md HTTP-TRUST-9) +# parse-diff: the packed reader against the text parse (trusted in +# src/packed.bend) def run.parse(+cmd: String, a1: String, a2: String, a3: String) -> IO(Unit): on.io(String.eq(cmd, "parse-diff"), _u => IO.print(Parse.diff.report()), @@ -635,7 +636,7 @@ def main.go(args: List) -> IO(Unit): main.go.un(argv.w0(rest)) # CLI entry -def main() -> IO(Unit): # noqa: L001 benchmark program, no law's subject +def main() -> IO(Unit): do IO: args : List <- IO.args() main.go(args) diff --git a/bench/parse.bend b/bench/parse.bend index 0d109ba..b5eaa93 100644 --- a/bench/parse.bend +++ b/bench/parse.bend @@ -1,7 +1,7 @@ -# Differential check for SPEC.md HTTP-TRUST-9: packed.reply reads each -# response in the corpus off its packed octets exactly as Http.parse.to reads -# it as text, for GET and for HEAD. `ezhttp-bench parse-diff` prints one line -# a case, `ok` or `DIFF`; the flake check fails on any DIFF. +# Differential check for what src/packed.bend trusts: packed.reply reads +# each response in the corpus off its packed octets exactly as Http.parse.to +# reads it as text, for GET and for HEAD. `ezhttp-bench parse-diff` prints +# one line a case, `ok` or `DIFF`; the flake check fails on any DIFF. import Base import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes import ../src/http.bend as Http diff --git a/bolt.bend b/bolt.bend index 614cc9e..75d3bab 100644 --- a/bolt.bend +++ b/bolt.bend @@ -37,7 +37,3 @@ def closed() -> String: # which bolt never grades def coverage() -> String: "error" - -# SPEC.md rows and law tags agree (HTTP-SRV) -def trace() -> String: - "error" diff --git a/SPEC.md b/docs/SPEC.md similarity index 100% rename from SPEC.md rename to docs/SPEC.md diff --git a/ez.lock.toml b/ez.lock.toml index 8454d7e..bef17cd 100644 --- a/ez.lock.toml +++ b/ez.lock.toml @@ -51,8 +51,8 @@ LICENSE = "309f5aae946e4db157750e002fb179a74ed0fe27fce09a7be68a6977b14f205f" [tools] [tools.bolt] git = "https://github.com/Emerging-Patterns/bolt" -rev = "7f5bb328187efa21598b4fed51cb1e154cf36330" -tag = "v1.19.0" +rev = "78fad9564b1a5b49f1e39bbd46a7fec45e615a4c" +tag = "v1.22.0" root = "." -narHash = "sha256-9d+ikveuzegJVH8nbEYSAxy8Pt0dYDyW4ISZml+MsNM=" +narHash = "sha256-OHkE75Urs5jTTU8Nygs2+enKYKOXeHOBDZ5t0ZwJVA4=" entry = "main.bend" diff --git a/ez.toml b/ez.toml index 159759c..74e6d89 100644 --- a/ez.toml +++ b/ez.toml @@ -25,8 +25,8 @@ entry = "dns.bend" [tools] [tools.bolt] git = "https://github.com/Emerging-Patterns/bolt" -rev = "7f5bb328187efa21598b4fed51cb1e154cf36330" -tag = "v1.19.0" +rev = "78fad9564b1a5b49f1e39bbd46a7fec45e615a4c" +tag = "v1.22.0" root = "." -narHash = "sha256-9d+ikveuzegJVH8nbEYSAxy8Pt0dYDyW4ISZml+MsNM=" +narHash = "sha256-OHkE75Urs5jTTU8Nygs2+enKYKOXeHOBDZ5t0ZwJVA4=" entry = "main.bend" diff --git a/laws/AbsoluteFormTargetRejected.bend b/laws/AbsoluteFormTargetRejected.bend new file mode 100644 index 0000000..6d546d5 --- /dev/null +++ b/laws/AbsoluteFormTargetRejected.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §3.2.2): absolute-form is not accepted as origin-form +law ask_rejects_absolute_form: + {Http.ask("GET http://example.com/a HTTP/1.1\r\nHost: h\r\n\r\n") + == Http.Bad{"the request-target is not in origin-form"} : Http.Request} diff --git a/laws/AbsoluteLocationsResolve.bend b/laws/AbsoluteLocationsResolve.bend new file mode 100644 index 0000000..61fd31a --- /dev/null +++ b/laws/AbsoluteLocationsResolve.bend @@ -0,0 +1,138 @@ +# A Location that starts `http://` or `https://` is its own target. Against +# a current URL that parses into scheme, host, port and path, a Location +# that starts `//` (a network-path reference) is the scheme, `:` and the +# Location; one that starts with a single `/` (an absolute path) is the +# scheme, `://`, host, `:`, port, then its path with dot segments removed +# (laws/DotSegmentsRemoved.bend) and its query after it unchanged, its +# fragment dropped. An empty Location, one that starts `#`, one whose text +# before its first `/`, `?` or `#` holds a `:` (another scheme), and every +# other Location against a URL that does not parse have no target. +# +# also rests on: laws/LocationResolvedBySort.bend, +# laws/LocationSplitsAtQuery.bend +import Base +import ../src/url.bend as Url +import ../src/redirect.bend as Redirect + +# LAW: an absolute http or https Location is its own target +law target_absolute: + for url: String + for +loc: String + for e: {True{} == Bool.or(String.starts_with(loc, "http://"), + String.starts_with(loc, "https://")) : Bool} + {Redirect.redirect.target(url, loc) == Some{loc} : Maybe<&2, String>} + +# LAW: against a URL that does not parse, a Location that is not an absolute +# URL has no target +law target_bad: + for url: String + for +loc: String + for e1: {False{} == Bool.or(String.starts_with(loc, "http://"), + String.starts_with(loc, "https://")) : Bool} + for why: String + for e2: {Url.Bad{why} == Url.parse(url) : Url.Loc} + {Redirect.redirect.target(url, loc) == None{} : Maybe<&2, String>} + +# LAW (RFC 3986 §5.2.2): a network-path reference keeps only the scheme +law against_net: + for scheme: String + for host: String + for port: U32 + for path: String + for +loc: String + {Redirect.redirect.against(scheme, host, port, path, loc, Redirect.Net{}) + == Some{scheme ++ ":" ++ loc} : Maybe<&2, String>} + +# LAW (RFC 3986 §5.2.2): an absolute path keeps the origin, its own path with +# dot segments removed, then its query +law against_abs: + for scheme: String + for host: String + for port: U32 + for path: String + for +loc: String + {Redirect.redirect.against(scheme, host, port, path, loc, Redirect.Abs{}) + == Some{scheme ++ "://" ++ host ++ ":" ++ U32.show(port) + ++ Redirect.redirect.abs.of(Redirect.redirect.split(loc))} + : Maybe<&2, String>} + +# LAW: a Location the client stays on has no target +law against_stay: + for scheme: String + for host: String + for port: U32 + for path: String + for +loc: String + {Redirect.redirect.against(scheme, host, port, path, loc, Redirect.Stay{}) + == None{} : Maybe<&2, String>} + +# LAW (RFC 3986 §4.2): a Location that starts `//` is a network path +law sort_net: + for +loc: String + for e: {True{} == String.starts_with(loc, "//") : Bool} + {Redirect.redirect.sort(loc) == Redirect.Net{} : Redirect.Sort} + +# LAW (RFC 3986 §4.2): one that starts with a single `/` is an absolute path +law sort_abs: + for +loc: String + for e1: {False{} == String.starts_with(loc, "//") : Bool} + for e2: {True{} == String.starts_with(loc, "/") : Bool} + {Redirect.redirect.sort(loc) == Redirect.Abs{} : Redirect.Sort} + +# LAW: any other Location the client does not follow stays +law sort_stay: + for +loc: String + for e1: {False{} == String.starts_with(loc, "//") : Bool} + for e2: {False{} == String.starts_with(loc, "/") : Bool} + for e3: {True{} == Redirect.redirect.stays(loc) : Bool} + {Redirect.redirect.sort(loc) == Redirect.Stay{} : Redirect.Sort} + +# LAW: the client stays on an empty Location, a fragment, and one that names +# another scheme +law stays_on: + for +loc: String + {Redirect.redirect.stays(loc) == Bool.or(String.is_empty(loc), + Bool.or(String.starts_with(loc, "#"), Redirect.redirect.schemed(loc))) + : Bool} + +# LAW (RFC 3986 §3.1): an empty reference names no scheme +law schemed_nil: + for s: String + for e: {True{} == String.is_empty(s) : Bool} + {Redirect.redirect.schemed(s) == False{} : Bool} + +# LAW (RFC 3986 §3.1): a `:` before the first `/`, `?` or `#` names a scheme +law schemed_colon: + for t: String + {Redirect.redirect.schemed(SCon{':', t}) == True{} : Bool} + +# LAW (RFC 3986 §4.2): a `/`, `?` or `#` first means no scheme +law schemed_stop: + for +c: Char + for t: String + for e: {True{} == Redirect.redirect.schemed.end(c) : Bool} + {Redirect.redirect.schemed(SCon{c, t}) == False{} : Bool} + +# LAW: any other first character leaves the question to the rest +law schemed_other: + for +c: Char + for t: String + for e1: {False{} == Char.is_eq(c, ':') : Bool} + for e2: {False{} == Redirect.redirect.schemed.end(c) : Bool} + {Redirect.redirect.schemed(SCon{c, t}) == Redirect.redirect.schemed(t) + : Bool} + +# LAW: the characters that end a reference's first segment are `/`, `?`, `#` +law schemed_end: + for +c: Char + {Redirect.redirect.schemed.end(c) == Bool.or(Char.is_eq(c, '/'), + Bool.or(Char.is_eq(c, '?'), Char.is_eq(c, '#'))) : Bool} + +# LAW (RFC 3986 §5.2.2): an absolute path is its segments after the leading +# `/`, dot segments removed, then its query +law abs_of: + for path: String + for query: String + {Redirect.redirect.abs.of(Redirect.Ref{path, query}) + == Redirect.redirect.clean(List.tail(&2, String, String.split(path, '/'))) + ++ query : String} diff --git a/laws/BadRequestAnswers400.bend b/laws/BadRequestAnswers400.bend new file mode 100644 index 0000000..62e379c --- /dev/null +++ b/laws/BadRequestAnswers400.bend @@ -0,0 +1,31 @@ +# The server's decisions live in `src/step.bend` and `src/http.bend`, which +# do no IO; `src/server.bend` runs the effects and acts on them (trusted in +# src/server.bend). Each connection is served on its own fork, so +# connections overlap. `http.serve_with`, `http.serve_io_with` and +# `http.serve_forever_with` hand the handler a context value with each +# request (`Server.Req`: the request as text and its body's octets) and take +# a reply as text, as packed octets, or streamed from a channel of octet +# chunks; `http.serve`, `http.serve_io`, `http.serve_once` and +# `http.serve_forever` are those with a text handler and no context. It +# receives a request as octets, one character each +# (laws/OneOctetPerChar.bend), so its Content-Length counts octets. +# +# For every request that does not parse, with reason `why`, the server +# answers `400` with body `why` and does not call the handler. +import Base +import ../src/http.bend as Http +import ../src/step.bend as Step + +# --- the server's decisions --- + +# LAW: a request that does not parse is answered 400 with its reason, and +# the handler is not called +law step_bad: + for why: String + {Step.step.of(Http.Bad{why}) == Step.Answer{Http.Reply{400, [], why}} + : Step.Step} + +# LAW: a bad request keeps its reason +law step_text_bad: + for why: String + {Step.step.text(Http.Bad{why}) == Http.Bad{why} : Http.Request} diff --git a/laws/BadRequestShowIsMarked.bend b/laws/BadRequestShowIsMarked.bend new file mode 100644 index 0000000..df0628e --- /dev/null +++ b/laws/BadRequestShowIsMarked.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW: a bad request show is marked +law ask_show_bad_is_marked: + for why: String + {String.starts_with(Http.ask.show(Http.Bad{why}), "bad ") == True{} : Bool} diff --git a/laws/BareRepliesWriteHeadOnly.bend b/laws/BareRepliesWriteHeadOnly.bend new file mode 100644 index 0000000..a290379 --- /dev/null +++ b/laws/BareRepliesWriteHeadOnly.bend @@ -0,0 +1,27 @@ +# For every method and status, a reply is written with its head only, no +# octets and no chunks, exactly when the method is `HEAD` (RFC 9110 §9.3.2) +# or the status is 204 or 304 (`step.bare`, RFC 9110 §15.3.5, §15.4.5); a +# packed octets reply written bare carries the head of an empty body, as a +# text reply does (laws/HeadAndNoContentHaveNoBody.bend). +import Base +import ../src/http.bend as Http +import ../src/step.bend as Step + +# LAW (RFC 9110 §9.3.2): a reply to HEAD is written bare +law bare_head: + for +status: U32 + {Step.step.bare("HEAD", status) == True{} : Bool} + +# LAW: a reply to any other method is bare exactly when its status is 204 +# or 304 +law bare_other: + for +method: String + for e: {False{} == String.eq(method, "HEAD") : Bool} + for +status: U32 + {Step.step.bare(method, status) == Http.status.nocontent(status) : Bool} + +# LAW: a 204 or 304 is exactly what nocontent names +law nocontent_is: + for +code: U32 + {Http.status.nocontent(code) + == Bool.or(U32.is_eq(code, 204), U32.is_eq(code, 304)) : Bool} diff --git a/laws/Base64EmptyIsEmpty.bend b/laws/Base64EmptyIsEmpty.bend new file mode 100644 index 0000000..d50b2ce --- /dev/null +++ b/laws/Base64EmptyIsEmpty.bend @@ -0,0 +1,6 @@ +import Base +import ../src/b64.bend as B64 + +# LAW (RFC 4648 §4): the empty string encodes to empty +law b64_empty: + {B64.b64.encode("") == "" : String} diff --git a/laws/Base64EncodesF.bend b/laws/Base64EncodesF.bend new file mode 100644 index 0000000..8db8b18 --- /dev/null +++ b/laws/Base64EncodesF.bend @@ -0,0 +1,6 @@ +import Base +import ../src/b64.bend as B64 + +# LAW (RFC 4648 §10): `f` encodes to `Zg==` +law b64_f: + {B64.b64.encode("f") == "Zg==" : String} diff --git a/laws/Base64EncodesFo.bend b/laws/Base64EncodesFo.bend new file mode 100644 index 0000000..bee0e8e --- /dev/null +++ b/laws/Base64EncodesFo.bend @@ -0,0 +1,6 @@ +import Base +import ../src/b64.bend as B64 + +# LAW (RFC 4648 §10): `fo` encodes to `Zm8=` +law b64_fo: + {B64.b64.encode("fo") == "Zm8=" : String} diff --git a/laws/Base64EncodesFoo.bend b/laws/Base64EncodesFoo.bend new file mode 100644 index 0000000..2ee4e09 --- /dev/null +++ b/laws/Base64EncodesFoo.bend @@ -0,0 +1,6 @@ +import Base +import ../src/b64.bend as B64 + +# LAW (RFC 4648 §10): `foo` encodes to `Zm9v` +law b64_foo: + {B64.b64.encode("foo") == "Zm9v" : String} diff --git a/laws/Base64EncodesUserPass.bend b/laws/Base64EncodesUserPass.bend new file mode 100644 index 0000000..24256a8 --- /dev/null +++ b/laws/Base64EncodesUserPass.bend @@ -0,0 +1,6 @@ +import Base +import ../src/b64.bend as B64 + +# LAW (RFC 4648 §10 / RFC 7617 §2): user:pass is the Basic credential alphabet +law b64_user_pass: + {B64.b64.encode("user:pass") == "dXNlcjpwYXNz" : String} diff --git a/laws/Base64PadsFinalQuantum.bend b/laws/Base64PadsFinalQuantum.bend new file mode 100644 index 0000000..fb3127c --- /dev/null +++ b/laws/Base64PadsFinalQuantum.bend @@ -0,0 +1,8 @@ +import Base +import ../src/b64.bend as B64 + +# LAW (RFC 4648 §4): at the end of the input, the quantum in progress is +# written out with its padding +law b64_go_end: + for phase: B64.Phase + {B64.b64.go([], phase) == B64.b64.end(phase) : String} diff --git a/laws/BasicIsBase64OfUserPassword.bend b/laws/BasicIsBase64OfUserPassword.bend new file mode 100644 index 0000000..e523327 --- /dev/null +++ b/laws/BasicIsBase64OfUserPassword.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http +import ../src/auth.bend as Auth + +# LAW (RFC 7617 §2): Basic credentials are Base64 of user:password +law basic_user_pass: + {Auth.auth.basic("user", "pass") + == Http.H{"Authorization", "Basic dXNlcjpwYXNz"} : Http.Header} diff --git a/laws/BearerIsAuthorization.bend b/laws/BearerIsAuthorization.bend new file mode 100644 index 0000000..f5ae239 --- /dev/null +++ b/laws/BearerIsAuthorization.bend @@ -0,0 +1,11 @@ +import Base +import ../src/http.bend as Http +import ../src/auth.bend as Auth + +# --- main API surface (named so bolt laws stay clean) --- + +# LAW: bearer builds Authorization: Bearer … +law bearer_is_authorization: + for token: String + {Auth.auth.bearer(token) == Http.H{"Authorization", "Bearer " ++ token} + : Http.Header} diff --git a/laws/BearerTokenAcceptsPlain.bend b/laws/BearerTokenAcceptsPlain.bend new file mode 100644 index 0000000..eaa2eda --- /dev/null +++ b/laws/BearerTokenAcceptsPlain.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http + +# --- RFC 6750 / 7617 / 4648 credentials --- + +# LAW (RFC 6750 §2.1): a token with no whitespace is acceptable +law token_ok_plain: + {Http.token.ok("abc") == True{} : Bool} diff --git a/laws/BearerTokenRejectsNewline.bend b/laws/BearerTokenRejectsNewline.bend new file mode 100644 index 0000000..f865b14 --- /dev/null +++ b/laws/BearerTokenRejectsNewline.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 6750 §2.1): a newline is not a bearer token character +law token_ok_newline: + {Http.token.ok("a\nb") == False{} : Bool} diff --git a/laws/BearerTokenRejectsSpace.bend b/laws/BearerTokenRejectsSpace.bend new file mode 100644 index 0000000..f6698ff --- /dev/null +++ b/laws/BearerTokenRejectsSpace.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 6750 §2.1): a space is not a bearer token character +law token_ok_space: + {Http.token.ok("a b") == False{} : Bool} diff --git a/laws/BearerTokenRejectsTab.bend b/laws/BearerTokenRejectsTab.bend new file mode 100644 index 0000000..e52ba40 --- /dev/null +++ b/laws/BearerTokenRejectsTab.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 6750 §2.1): a tab is not a bearer token character +law token_ok_tab: + {Http.token.ok("a\tb") == False{} : Bool} diff --git a/laws/CacheDirectiveNamesIgnoreCase.bend b/laws/CacheDirectiveNamesIgnoreCase.bend new file mode 100644 index 0000000..a82e194 --- /dev/null +++ b/laws/CacheDirectiveNamesIgnoreCase.bend @@ -0,0 +1,9 @@ +import Base +import ../src/cache.bend as Cache + +# --- RFC 9111 cache directives --- + +# LAW (RFC 9111 §5.2): directive names are case-insensitive +law cache_directive_case: + {Cache.cache.parse("No-Store, MAX-AGE=0, Public") + == Cache.Cc{True{}, False{}, True{}, False{}, "0", ""} : Cache.Cc} diff --git a/laws/CacheIgnoresUnknownDirective.bend b/laws/CacheIgnoresUnknownDirective.bend new file mode 100644 index 0000000..7c0e05e --- /dev/null +++ b/laws/CacheIgnoresUnknownDirective.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §5.2): an unrecognized directive is ignored +law cache_ignores_s_maxage: + {Cache.cache.parse("s-maxage=9, max-age=3") + == Cache.Cc{False{}, False{}, False{}, False{}, "3", ""} : Cache.Cc} diff --git a/laws/CacheMaxAgeIgnoresJunk.bend b/laws/CacheMaxAgeIgnoresJunk.bend new file mode 100644 index 0000000..46e1b28 --- /dev/null +++ b/laws/CacheMaxAgeIgnoresJunk.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §5.2.1.1): a non-digit max-age is not recorded +law cache_max_age_ignores_junk: + {Cache.cache.parse("max-age=nope") + == Cache.Cc{False{}, False{}, False{}, False{}, "", ""} : Cache.Cc} diff --git a/laws/CacheReadsKnownDirectives.bend b/laws/CacheReadsKnownDirectives.bend new file mode 100644 index 0000000..c598b83 --- /dev/null +++ b/laws/CacheReadsKnownDirectives.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §5.2): no-store, no-cache, public, private, and max-age +law cache_directives: + {Cache.cache.parse("no-store, no-cache, public, private, max-age=5") + == Cache.Cc{True{}, True{}, True{}, True{}, "5", ""} : Cache.Cc} diff --git a/laws/CacheRecordsExpires.bend b/laws/CacheRecordsExpires.bend new file mode 100644 index 0000000..e31a7ca --- /dev/null +++ b/laws/CacheRecordsExpires.bend @@ -0,0 +1,8 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §5.3): Expires is recorded beside Cache-Control +law cache_note_expires: + {Cache.cache.note("max-age=0", "Wed, 21 Oct 2099 07:28:00 GMT") + == Cache.Cc{False{}, False{}, False{}, False{}, "0", + "Wed, 21 Oct 2099 07:28:00 GMT"} : Cache.Cc} diff --git a/laws/CacheRecordsMaxAge.bend b/laws/CacheRecordsMaxAge.bend new file mode 100644 index 0000000..2d4c8ac --- /dev/null +++ b/laws/CacheRecordsMaxAge.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §4.2.1): a present max-age is what freshness uses +law cache_max_age_present: + {Cache.cache.max_age(Cache.cache.parse("max-age=1")) == True{} : Bool} diff --git a/laws/CacheRecordsNoCache.bend b/laws/CacheRecordsNoCache.bend new file mode 100644 index 0000000..656c50c --- /dev/null +++ b/laws/CacheRecordsNoCache.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §5.2.2.2): no-cache alone is recorded and not fresh +law cache_no_cache_alone: + {Cache.cache.parse("no-cache") + == Cache.Cc{False{}, True{}, False{}, False{}, "", ""} : Cache.Cc} diff --git a/laws/CacheRecordsPrivate.bend b/laws/CacheRecordsPrivate.bend new file mode 100644 index 0000000..555c413 --- /dev/null +++ b/laws/CacheRecordsPrivate.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §5.2.2.6): private alone is recorded +law cache_private_alone: + {Cache.cache.parse("private") + == Cache.Cc{False{}, False{}, False{}, True{}, "", ""} : Cache.Cc} diff --git a/laws/ChunkDataUnchanged.bend b/laws/ChunkDataUnchanged.bend new file mode 100644 index 0000000..3477070 --- /dev/null +++ b/laws/ChunkDataUnchanged.bend @@ -0,0 +1,27 @@ +# For every chunk-size line `h` that is a hexadecimal size `n` other than 0, +# its `n` octets of data `d` and the text `r` after the CRLF that ends them, +# the chunked decoder writes exactly `d`, then what it decodes from `r` (RFC +# 9112 §7.1: a chunk's data is its octets, unchanged); a chunk-size line +# whose size is 0 ends the body. +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): a chunk of n octets other than 0 is its data, then +# the chunks after its CRLF +law chunk_data: + for f: Nat + for +h: String + for +d: String + for +r: String + for e: {False{} == Nat.is_eq(Http.hexlen(h), 0n) : Bool} + for el: {String.length(d) == Http.hexlen(h) : Nat} + {Http.chunks(1n+f, Http.Cut{h, String.append(d, "\r\n" ++ r)}) + == String.append(d, Http.chunks(f, Http.divide(r, "\r\n"))) : String} + +# LAW (RFC 9112 §7.1): a chunk of size 0 ends the body +law chunk_last: + for f: Nat + for +h: String + for after: String + for e: {True{} == Nat.is_eq(Http.hexlen(h), 0n) : Bool} + {Http.chunks(1n+f, Http.Cut{h, after}) == "" : String} diff --git a/laws/ChunkSizeHexIgnoresCase.bend b/laws/ChunkSizeHexIgnoresCase.bend new file mode 100644 index 0000000..63ffc29 --- /dev/null +++ b/laws/ChunkSizeHexIgnoresCase.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): `a` and `A` are the same chunk-size digit +law hexlen_alpha: + {Http.hexlen("a") == Http.hexlen("A") : Nat} diff --git a/laws/ChunkSizeIgnoresExtension.bend b/laws/ChunkSizeIgnoresExtension.bend new file mode 100644 index 0000000..af6a57b --- /dev/null +++ b/laws/ChunkSizeIgnoresExtension.bend @@ -0,0 +1,11 @@ +import Base +import ../src/http.bend as Http +import ./defs/CharacterAbsent.bend as D1 + +# LAW (RFC 9112 §7.1): a chunk-size extension after `;` does not change the +# length hexlen reads. +law hexlen_ignores_extension: + for +s: String + for ext: String + for +e: {D1.avoids(s, ';') == True{} : Bool} + {Http.hexlen(String.append(s, SCon{';', ext})) == Http.hexlen(s) : Nat} diff --git a/laws/ChunkSizeReadsHexSkipsExtension.bend b/laws/ChunkSizeReadsHexSkipsExtension.bend new file mode 100644 index 0000000..406ed23 --- /dev/null +++ b/laws/ChunkSizeReadsHexSkipsExtension.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): chunk-size is hexadecimal, and an extension is skipped +law hexlen_extension_and_case: + {Http.hexlen("5;ext=1") == 5n : Nat} diff --git a/laws/ChunkedBodyJoinsChunks.bend b/laws/ChunkedBodyJoinsChunks.bend new file mode 100644 index 0000000..6dbfd21 --- /dev/null +++ b/laws/ChunkedBodyJoinsChunks.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): a chunked response body is the concatenated chunks +law parse_chunked_body: + {Http.parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nwiki\r\n5\r\npedia\r\n0\r\n\r\n") + == Http.Reply{200, [Http.H{"Transfer-Encoding", "chunked"}], "wikipedia"} + : Http.Reply} diff --git a/laws/ChunkedNameIgnoresCase.bend b/laws/ChunkedNameIgnoresCase.bend new file mode 100644 index 0000000..c8f5dc6 --- /dev/null +++ b/laws/ChunkedNameIgnoresCase.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): chunked is recognized in any case +law parse_chunked_ignores_case: + {Http.parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: CHUNKED\r\n\r\n1\r\nZ\r\n0\r\n\r\n") + == Http.Reply{200, [Http.H{"Transfer-Encoding", "CHUNKED"}], "Z"} + : Http.Reply} diff --git a/laws/ChunksStopWithoutFuel.bend b/laws/ChunksStopWithoutFuel.bend new file mode 100644 index 0000000..5a97cae --- /dev/null +++ b/laws/ChunksStopWithoutFuel.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): no fuel left means no more chunks +law chunks_empty_fuel: + {Http.chunks(0n, Http.NoCut{}) == "" : String} diff --git a/laws/ContentLengthFramesBody.bend b/laws/ContentLengthFramesBody.bend new file mode 100644 index 0000000..93aa5c9 --- /dev/null +++ b/laws/ContentLengthFramesBody.bend @@ -0,0 +1,31 @@ +# For every status, header fields, body octets `b` and octets `r` after +# them, a response framed by a Content-Length of exactly `b`'s octets is +# `b`, whatever `r` holds (RFC 9112 §6.3 item 5: octets past the length are +# not the message's), and one whose octets after the head are fewer than its +# Content-Length is torn with `the response body stopped before its +# Content-Length`. +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §6.3): a body shorter than its Content-Length is torn — a cut +# connection is not a short file. +law clen_short: + for st: U32 + for hs: List<&2, Http.Header> + for +n: Nat + for +body: String + for e: {True{} == Nat.is_lt(Http.octs.len(body), n) : Bool} + {Http.parse.clen(st, hs, n, body) + == Http.Torn{"the response body stopped before its Content-Length"} + : Http.Reply} + +# LAW (RFC 9112 §6.3 item 5): a body framed by a Content-Length of exactly +# its octets is those octets, whatever follows them +law clen_prefix: + for st: U32 + for hs: List<&2, Http.Header> + for +b: String + for +r: String + {Http.parse.clen(st, hs, String.length(b), String.append(b, r)) + == Http.Reply{st, hs, b} + : Http.Reply} diff --git a/laws/ContentLengthReadsDecimal.bend b/laws/ContentLengthReadsDecimal.bend new file mode 100644 index 0000000..06a69bb --- /dev/null +++ b/laws/ContentLengthReadsDecimal.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §6.3): Content-Length is a decimal, not a prefix of junk +law digits_read_decimal: + {Http.digits.read("12") == Some{12n} : Maybe<&2, Nat>} diff --git a/laws/ContentLengthRejectsJunk.bend b/laws/ContentLengthRejectsJunk.bend new file mode 100644 index 0000000..c002203 --- /dev/null +++ b/laws/ContentLengthRejectsJunk.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §6.3): a Content-Length that is not all digits is absent +law digits_read_rejects_junk: + {Http.digits.read("12a") == None{} : Maybe<&2, Nat>} diff --git a/laws/ContentLengthTakesPrefix.bend b/laws/ContentLengthTakesPrefix.bend new file mode 100644 index 0000000..60a8c16 --- /dev/null +++ b/laws/ContentLengthTakesPrefix.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §8.6): Content-Length takes that many octets and no more +law parse_clen_takes_prefix: + {Http.parse("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello!!") + == Http.Reply{200, [Http.H{"Content-Length", "5"}], "hello"} : Http.Reply} diff --git a/laws/ContinuationLineRejected.bend b/laws/ContinuationLineRejected.bend new file mode 100644 index 0000000..137f940 --- /dev/null +++ b/laws/ContinuationLineRejected.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW: a continuation line with no colon is not a field +law head_line_rejects_continuation: + {Http.head.line(" folded") == None{} : Maybe<&2, Http.Header>} diff --git a/laws/CookieAliveUnderMaxAge.bend b/laws/CookieAliveUnderMaxAge.bend new file mode 100644 index 0000000..236fb7b --- /dev/null +++ b/laws/CookieAliveUnderMaxAge.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): an age under Max-Age is still alive +law cookie_max_age_open: + {Cookie.cookie.alive("10", 0n) == True{} : Bool} diff --git a/laws/CookieAttributesJoinWithSemicolons.bend b/laws/CookieAttributesJoinWithSemicolons.bend new file mode 100644 index 0000000..c92b5b7 --- /dev/null +++ b/laws/CookieAttributesJoinWithSemicolons.bend @@ -0,0 +1,12 @@ +import Base +import ../src/cookie.bend as Cookie + +# --- RFC 6265 cookies --- + +# LAW (RFC 6265 §4.1.1): attributes serialize with `; ` separators +law cookie_line_attributes: + {Cookie.cookie.line(Cookie.Cookie{"a", "b", + "Wed, 21 Oct 2015 07:28:00 GMT", "12", "example.com", "/app", + True{}, True{}, "Lax", False{}}) + == "a=b; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Max-Age=12; Domain=example.com; Path=/app; Secure; HttpOnly; SameSite=Lax" + : String} diff --git a/laws/CookieDefaultPathFallsBackToRoot.bend b/laws/CookieDefaultPathFallsBackToRoot.bend new file mode 100644 index 0000000..8a952b7 --- /dev/null +++ b/laws/CookieDefaultPathFallsBackToRoot.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.4): fewer than two slashes means default-path `/` +law cookie_default_path_root: + {Cookie.cookie.default_path("/a") == "/" : String} diff --git a/laws/CookieDefaultPathIsDirectory.bend b/laws/CookieDefaultPathIsDirectory.bend new file mode 100644 index 0000000..d6c621c --- /dev/null +++ b/laws/CookieDefaultPathIsDirectory.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.4): default-path is the directory of a path with two slashes +law cookie_default_path_dir: + {Cookie.cookie.default_path("/a/b") == "/a" : String} diff --git a/laws/CookieDefaultPathNeedsTwoSlashes.bend b/laws/CookieDefaultPathNeedsTwoSlashes.bend new file mode 100644 index 0000000..641915a --- /dev/null +++ b/laws/CookieDefaultPathNeedsTwoSlashes.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.4): one slash is not enough for a directory default-path +law cookie_default_path_one_slash: + {Cookie.cookie.default_path("/") == "/" : String} diff --git a/laws/CookieDefaultPathOmitsTrailingSlash.bend b/laws/CookieDefaultPathOmitsTrailingSlash.bend new file mode 100644 index 0000000..c5bacec --- /dev/null +++ b/laws/CookieDefaultPathOmitsTrailingSlash.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.4): a trailing slash keeps the directory, not the slash +law cookie_default_path_trailing: + {Cookie.cookie.default_path("/foo/bar/") == "/foo/bar" : String} diff --git a/laws/CookieDomainLowercased.bend b/laws/CookieDomainLowercased.bend new file mode 100644 index 0000000..a61c740 --- /dev/null +++ b/laws/CookieDomainLowercased.bend @@ -0,0 +1,8 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.2.3): a Domain attribute is stored lowercase +law cookie_domain_lowercased: + {Cookie.cookie.parse("a=b; Domain=Example.COM") + == Some{Cookie.Cookie{"a", "b", "", "", "example.com", "", False{}, + False{}, "", False{}}} : Maybe<&2, Cookie.Cookie>} diff --git a/laws/CookieDomainMatchesDotSuffix.bend b/laws/CookieDomainMatchesDotSuffix.bend new file mode 100644 index 0000000..ff0732f --- /dev/null +++ b/laws/CookieDomainMatchesDotSuffix.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.3): a domain-match allows a leading-dot suffix +law cookie_domain_suffix: + {Cookie.cookie.domain_ok("www.example.com", "example.com", False{}) + == True{} : Bool} diff --git a/laws/CookieDomainNeedsDotBeforeSuffix.bend b/laws/CookieDomainNeedsDotBeforeSuffix.bend new file mode 100644 index 0000000..478ee00 --- /dev/null +++ b/laws/CookieDomainNeedsDotBeforeSuffix.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.3): a suffix without the separating dot does not match +law cookie_domain_not_suffix: + {Cookie.cookie.domain_ok("notexample.com", "example.com", False{}) + == False{} : Bool} diff --git a/laws/CookieHeaderEchoesStore.bend b/laws/CookieHeaderEchoesStore.bend new file mode 100644 index 0000000..266b41f --- /dev/null +++ b/laws/CookieHeaderEchoesStore.bend @@ -0,0 +1,109 @@ +# `cookie_from_set_cookies(values)` and `cookie_from_headers(headers)` +# (`src/echo.bend`) give the `Cookie` request header value a client sends +# after a response's `Set-Cookie` fields, keeping only names and values: no +# attribute is read, so nothing expires and every cookie goes back. +# +# For every list of Set-Cookie field values, the cookies they set are stored +# in order: a cookie whose name (compared exactly) is already stored +# replaces that one's value in its place (§5.3 step 11.3 keeps the old +# cookie's creation-time, which §5.4 step 2 orders by), any other goes after +# those stored. The Cookie header value is each stored cookie as its name, +# `=` and its value (§5.4 step 4.1), with `; ` between two (step 4.2), and +# empty when none is stored. +import Base +import ../src/qs.bend as Qs +import ../src/echo.bend as Echo + +# LAW: the header sent back is the store after the values, written out +law ck_echo: + for lines: List<&2, String> + {Echo.echo(lines) == Echo.line(Echo.store(lines, [])) : String} + +# LAW: with no more values, the store is as it is +law ck_store_nil: + for jar: List<&2, Qs.Kv()> + {Echo.store([], jar) == jar : List<&2, Qs.Kv()>} + +# LAW: values are stored in the order they came +law ck_store_cons: + for l: String + for t: List<&2, String> + for jar: List<&2, Qs.Kv()> + {Echo.store(l <> t, jar) == Echo.store(t, Echo.add(jar, Echo.pair(l))) + : List<&2, Qs.Kv()>} + +# LAW: a value that sets nothing leaves the store +law ck_add_none: + for jar: List<&2, Qs.Kv()> + {Echo.add(jar, None{}) == jar : List<&2, Qs.Kv()>} + +# LAW: a cookie is put in the store +law ck_add_some: + for jar: List<&2, Qs.Kv()> + for name: String + for value: String + {Echo.add(jar, Some{(name, value)}) == Echo.put(jar, name, value) + : List<&2, Qs.Kv()>} + +# LAW (§5.3): a cookie no stored one is named like goes last +law ck_put_nil: + for +name: String + for +value: String + {Echo.put([], name, value) == [(name, value)] : List<&2, Qs.Kv()>} + +# LAW (§5.3 step 11): a stored cookie of the same name takes the new value +# in its place +law ck_put_hit: + for +key: String + for old: String + for +t: List<&2, Qs.Kv()> + for +name: String + for +value: String + for e: {True{} == String.eq(key, name) : Bool} + {Echo.put((key, old) <> t, name, value) == (key, value) <> t + : List<&2, Qs.Kv()>} + +# LAW: any other stored cookie stays, and the rest of the store is looked at +law ck_put_miss: + for +key: String + for old: String + for +t: List<&2, Qs.Kv()> + for +name: String + for +value: String + for e: {False{} == String.eq(key, name) : Bool} + {Echo.put((key, old) <> t, name, value) == (key, old) <> Echo.put(t, name, + value) : List<&2, Qs.Kv()>} + +# LAW (§5.4): an empty store sends the empty value +law ck_line_nil: + for jar: List<&2, Qs.Kv()> + for e: {[] == jar : List<&2, Qs.Kv()>} + {Echo.line(jar) == "" : String} + +# LAW (§5.4 step 4): the first cookie, then the others +law ck_line_cons: + for name: String + for value: String + for t: List<&2, Qs.Kv()> + {Echo.line((name, value) <> t) == Echo.one(name, value) ++ Echo.more(t) + : String} + +# LAW: after the last cookie, nothing +law ck_more_nil: + for jar: List<&2, Qs.Kv()> + for e: {[] == jar : List<&2, Qs.Kv()>} + {Echo.more(jar) == "" : String} + +# LAW (§5.4 step 4.2): each cookie after the first follows `; ` +law ck_more_cons: + for name: String + for value: String + for t: List<&2, Qs.Kv()> + {Echo.more((name, value) <> t) == "; " ++ Echo.one(name, value) + ++ Echo.more(t) : String} + +# LAW (§5.4 step 4.1): a cookie is its name, `=` and its value +law ck_one: + for name: String + for value: String + {Echo.one(name, value) == name ++ "=" ++ value : String} diff --git a/laws/CookieHostOnlyNotSentElsewhere.bend b/laws/CookieHostOnlyNotSentElsewhere.bend new file mode 100644 index 0000000..faf5551 --- /dev/null +++ b/laws/CookieHostOnlyNotSentElsewhere.bend @@ -0,0 +1,8 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): a host-only cookie is not sent to another host +law cookie_send_other_host: + {Cookie.cookie.send("other.com", "/", False{}, + Cookie.cookie.receive("example.com", "/", Cookie.cookie.new("a", "b"))) + == False{} : Bool} diff --git a/laws/CookieHostOnlySentToHost.bend b/laws/CookieHostOnlySentToHost.bend new file mode 100644 index 0000000..e1a16d7 --- /dev/null +++ b/laws/CookieHostOnlySentToHost.bend @@ -0,0 +1,8 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): a host-only cookie is sent on its host and default-path +law cookie_send_host_only: + {Cookie.cookie.send("example.com", "/a/x", False{}, + Cookie.cookie.receive("Example.COM", "/a/b", Cookie.cookie.new("a", "b"))) + == True{} : Bool} diff --git a/laws/CookieHostOnlyStoresHost.bend b/laws/CookieHostOnlyStoresHost.bend new file mode 100644 index 0000000..d277e4c --- /dev/null +++ b/laws/CookieHostOnlyStoresHost.bend @@ -0,0 +1,8 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): a host-only cookie stores the response host and default-path +law cookie_receive_host_only: + {Cookie.cookie.line(Cookie.cookie.receive("Example.COM", "/a/b", + Cookie.cookie.new("a", "b"))) + == "a=b; Path=/a" : String} diff --git a/laws/CookieIdenticalPathsMatch.bend b/laws/CookieIdenticalPathsMatch.bend new file mode 100644 index 0000000..a5a1f6f --- /dev/null +++ b/laws/CookieIdenticalPathsMatch.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.4): identical paths match +law cookie_path_identical: + {Cookie.cookie.path_ok("/app", "/app") == True{} : Bool} diff --git a/laws/CookieInsecureSentOnCleartext.bend b/laws/CookieInsecureSentOnCleartext.bend new file mode 100644 index 0000000..a042202 --- /dev/null +++ b/laws/CookieInsecureSentOnCleartext.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): a cookie that is not Secure is sent on cleartext +law cookie_insecure_on_tcp: + {Cookie.cookie.send.secure(False{}, False{}) == True{} : Bool} diff --git a/laws/CookieIpHostMatchesExactly.bend b/laws/CookieIpHostMatchesExactly.bend new file mode 100644 index 0000000..6041894 --- /dev/null +++ b/laws/CookieIpHostMatchesExactly.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.3): an IPv4 host matches its own address exactly +law cookie_domain_ip_exact: + {Cookie.cookie.domain_ok("192.0.2.1", "192.0.2.1", False{}) == True{} : Bool} diff --git a/laws/CookieIpHostNoSuffixMatch.bend b/laws/CookieIpHostNoSuffixMatch.bend new file mode 100644 index 0000000..3ae8799 --- /dev/null +++ b/laws/CookieIpHostNoSuffixMatch.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.3): an IPv4 host does not domain-match a suffix +law cookie_domain_ip_suffix: + {Cookie.cookie.domain_ok("192.0.2.1", "2.1", False{}) == False{} : Bool} diff --git a/laws/CookieLongerPathFails.bend b/laws/CookieLongerPathFails.bend new file mode 100644 index 0000000..02f41db --- /dev/null +++ b/laws/CookieLongerPathFails.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.4): a cookie-path the request does not extend fails +law cookie_path_longer_cookie: + {Cookie.cookie.path_ok("/app", "/app/") == False{} : Bool} diff --git a/laws/CookieMaxAgeIgnoresJunk.bend b/laws/CookieMaxAgeIgnoresJunk.bend new file mode 100644 index 0000000..bd8f29c --- /dev/null +++ b/laws/CookieMaxAgeIgnoresJunk.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.2.2): a Max-Age that is not digits is ignored +law cookie_max_age_ignores_junk: + {Cookie.cookie.parse("a=b; Max-Age=nope") + == Some{Cookie.cookie.new("a", "b")} : Maybe<&2, Cookie.Cookie>} diff --git a/laws/CookieMaxAgeOneStaleAtOne.bend b/laws/CookieMaxAgeOneStaleAtOne.bend new file mode 100644 index 0000000..2bc6898 --- /dev/null +++ b/laws/CookieMaxAgeOneStaleAtOne.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): Max-Age 1 is alive at age 0 and stale at age 1 +law cookie_max_age_one: + {Bool.and(Cookie.cookie.alive("1", 0n), + Bool.not(Cookie.cookie.alive("1", 1n))) == True{} : Bool} diff --git a/laws/CookieMaxAgeSetsAge.bend b/laws/CookieMaxAgeSetsAge.bend new file mode 100644 index 0000000..e7f0237 --- /dev/null +++ b/laws/CookieMaxAgeSetsAge.bend @@ -0,0 +1,21 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.2.2): Max-Age replaces the cookie's max-age and nothing +# else +law cookie_age_sets: + for name: String + for value: String + for expires: String + for max_age: String + for domain: String + for path: String + for secure: Bool + for http_only: Bool + for same_site: String + for host_only: Bool + for age: String + {Cookie.cookie.age(Cookie.Cookie{name, value, expires, max_age, domain, path, + secure, http_only, same_site, host_only}, age) + == Cookie.Cookie{name, value, expires, age, domain, path, secure, + http_only, same_site, host_only} : Cookie.Cookie} diff --git a/laws/CookieMaxAgeZeroNotAlive.bend b/laws/CookieMaxAgeZeroNotAlive.bend new file mode 100644 index 0000000..7680eed --- /dev/null +++ b/laws/CookieMaxAgeZeroNotAlive.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): Max-Age 0 is not alive at age 0 +law cookie_max_age_zero: + {Cookie.cookie.alive("0", 0n) == False{} : Bool} diff --git a/laws/CookieNameIsNotIpv4.bend b/laws/CookieNameIsNotIpv4.bend new file mode 100644 index 0000000..06d80cd --- /dev/null +++ b/laws/CookieNameIsNotIpv4.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.3): a registered name is not an IPv4 address +law cookie_ipv4_name: + {Cookie.cookie.ipv4("example.com") == False{} : Bool} diff --git a/laws/CookieNoDomainNotSentElsewhere.bend b/laws/CookieNoDomainNotSentElsewhere.bend new file mode 100644 index 0000000..ef45e2f --- /dev/null +++ b/laws/CookieNoDomainNotSentElsewhere.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): a cookie with no Domain does not match a different host +law cookie_send_unset_domain: + {Cookie.cookie.send("h", "/", False{}, Cookie.cookie.new("a", "b")) + == False{} : Bool} diff --git a/laws/CookiePathMatchesSlashPrefix.bend b/laws/CookiePathMatchesSlashPrefix.bend new file mode 100644 index 0000000..2c1bcb4 --- /dev/null +++ b/laws/CookiePathMatchesSlashPrefix.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.4): a path-match allows a prefix whose next character is `/` +law cookie_path_prefix: + {Cookie.cookie.path_ok("/app/x", "/app") == True{} : Bool} diff --git a/laws/CookiePathNeedsSegmentBoundary.bend b/laws/CookiePathNeedsSegmentBoundary.bend new file mode 100644 index 0000000..ae88fd6 --- /dev/null +++ b/laws/CookiePathNeedsSegmentBoundary.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.4): a longer segment is not a path-match +law cookie_path_not_prefix: + {Cookie.cookie.path_ok("/application", "/app") == False{} : Bool} diff --git a/laws/CookieRecognizesIpv4Literal.bend b/laws/CookieRecognizesIpv4Literal.bend new file mode 100644 index 0000000..b495155 --- /dev/null +++ b/laws/CookieRecognizesIpv4Literal.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.3): an IPv4 literal is recognized, a name is not +law cookie_ipv4_literal: + {Cookie.cookie.ipv4("192.0.2.1") == True{} : Bool} diff --git a/laws/CookieRejectsEmptyName.bend b/laws/CookieRejectsEmptyName.bend new file mode 100644 index 0000000..7e093da --- /dev/null +++ b/laws/CookieRejectsEmptyName.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §4.1.1): an empty cookie-name is rejected +law cookie_rejects_empty_name: + {Cookie.cookie.parse("=nope") == None{} : Maybe<&2, Cookie.Cookie>} diff --git a/laws/CookieRequestJoinsPairs.bend b/laws/CookieRequestJoinsPairs.bend new file mode 100644 index 0000000..6d724b3 --- /dev/null +++ b/laws/CookieRequestJoinsPairs.bend @@ -0,0 +1,9 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): the Cookie header joins pairs with `; ` and drops an +# empty name +law cookie_request_joins: + {Cookie.cookie.join([Cookie.cookie.new("a", "b"), Cookie.cookie.new("c", "d"), + Cookie.cookie.new("", "x")]) + == "a=b; c=d" : String} diff --git a/laws/CookieRootPathMatchesLonger.bend b/laws/CookieRootPathMatchesLonger.bend new file mode 100644 index 0000000..5e76af7 --- /dev/null +++ b/laws/CookieRootPathMatchesLonger.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.4): default-path `/` matches a longer request path +law cookie_path_root: + {Cookie.cookie.path_ok("/a", "/") == True{} : Bool} diff --git a/laws/CookieSameSiteFoldsCase.bend b/laws/CookieSameSiteFoldsCase.bend new file mode 100644 index 0000000..32ae846 --- /dev/null +++ b/laws/CookieSameSiteFoldsCase.bend @@ -0,0 +1,8 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §4.1.1): SameSite=STRICT is canonical Strict +law cookie_samesite_strict: + {Cookie.cookie.parse("a=b; SameSite=STRICT") + == Some{Cookie.Cookie{"a", "b", "", "", "", "", False{}, False{}, + "Strict", True{}}} : Maybe<&2, Cookie.Cookie>} diff --git a/laws/CookieSameSiteNoneCanonical.bend b/laws/CookieSameSiteNoneCanonical.bend new file mode 100644 index 0000000..93099c9 --- /dev/null +++ b/laws/CookieSameSiteNoneCanonical.bend @@ -0,0 +1,8 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §4.1.1): SameSite=none is canonical None +law cookie_samesite_none: + {Cookie.cookie.parse("a=b; SameSite=none") + == Some{Cookie.Cookie{"a", "b", "", "", "", "", False{}, False{}, + "None", True{}}} : Maybe<&2, Cookie.Cookie>} diff --git a/laws/CookieSecureNeedsTls.bend b/laws/CookieSecureNeedsTls.bend new file mode 100644 index 0000000..62496ae --- /dev/null +++ b/laws/CookieSecureNeedsTls.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): a Secure cookie is not sent on a cleartext request +law cookie_secure_needs_tls: + {Cookie.cookie.send.secure(False{}, True{}) == False{} : Bool} diff --git a/laws/CookieSecureSentOnTls.bend b/laws/CookieSecureSentOnTls.bend new file mode 100644 index 0000000..f623329 --- /dev/null +++ b/laws/CookieSecureSentOnTls.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): a Secure cookie is sent when the request is TLS +law cookie_secure_on_tls: + {Cookie.cookie.send.secure(True{}, True{}) == True{} : Bool} diff --git a/laws/CookieSessionStaysAlive.bend b/laws/CookieSessionStaysAlive.bend new file mode 100644 index 0000000..92e7d84 --- /dev/null +++ b/laws/CookieSessionStaysAlive.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.4): a session cookie (no Max-Age) stays sendable +law cookie_session_alive: + {Cookie.cookie.alive("", 5n) == True{} : Bool} diff --git a/laws/CookieSlashEndedPathIsPrefix.bend b/laws/CookieSlashEndedPathIsPrefix.bend new file mode 100644 index 0000000..db32a16 --- /dev/null +++ b/laws/CookieSlashEndedPathIsPrefix.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §5.1.4): a cookie-path that already ends in `/` is a prefix +law cookie_path_slash_prefix: + {Cookie.cookie.path_ok("/app/x", "/app/") == True{} : Bool} diff --git a/laws/CookieUnknownSameSiteDropped.bend b/laws/CookieUnknownSameSiteDropped.bend new file mode 100644 index 0000000..fd5e314 --- /dev/null +++ b/laws/CookieUnknownSameSiteDropped.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §4.1.1): an unknown SameSite attribute is dropped +law cookie_samesite_unknown: + {Cookie.cookie.parse("a=b; SameSite=weird") + == Some{Cookie.cookie.new("a", "b")} : Maybe<&2, Cookie.Cookie>} diff --git a/laws/CorsAllowListReflectsOrigin.bend b/laws/CorsAllowListReflectsOrigin.bend new file mode 100644 index 0000000..5e0ef6b --- /dev/null +++ b/laws/CorsAllowListReflectsOrigin.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): an explicit allow-list reflects the matching origin +law cors_explicit_origin: + {Cors.cors.acao(["https://app.example"], False{}, "https://app.example") + == "https://app.example" : String} diff --git a/laws/CorsAllowedPreflightIs200.bend b/laws/CorsAllowedPreflightIs200.bend new file mode 100644 index 0000000..50477a5 --- /dev/null +++ b/laws/CorsAllowedPreflightIs200.bend @@ -0,0 +1,17 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): an allowed preflight is 200 with the CORS headers +law cors_preflight_allowed: + {Cors.cors.preflight(Cors.Cfg{["https://app.example"], ["GET", "PUT"], + ["Content-Type"], True{}, "600", ["X-Trace"]}, + "https://app.example", "PUT", "Content-Type") + == Http.Reply{200, [ + Http.H{"Access-Control-Allow-Credentials", "true"}, + Http.H{"Vary", "Origin"}, + Http.H{"Access-Control-Allow-Origin", "https://app.example"}, + Http.H{"Access-Control-Allow-Methods", "GET, PUT"}, + Http.H{"Access-Control-Allow-Headers", "Content-Type"}, + Http.H{"Access-Control-Max-Age", "600"} + ], ""} : Http.Reply} diff --git a/laws/CorsBadPreflightIs400.bend b/laws/CorsBadPreflightIs400.bend new file mode 100644 index 0000000..3079050 --- /dev/null +++ b/laws/CorsBadPreflightIs400.bend @@ -0,0 +1,11 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW: a preflight for a request that did not parse is 400 with its reason, +# whatever the allow-list +law cors_preflight_bad: + for cfg: Cors.Cfg + for why: String + {Cors.cors.preflight_req(cfg, Http.Bad{why}) == Http.Reply{400, [], why} + : Http.Reply} diff --git a/laws/CorsCredentialsKeepExplicitOrigin.bend b/laws/CorsCredentialsKeepExplicitOrigin.bend new file mode 100644 index 0000000..76079f4 --- /dev/null +++ b/laws/CorsCredentialsKeepExplicitOrigin.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): credentials with an explicit origin still reflect it +law cors_credentials_explicit_origin: + {Cors.cors.acao(["https://app.example"], True{}, "https://app.example") + == "https://app.example" : String} diff --git a/laws/CorsCredentialsReflectOrigin.bend b/laws/CorsCredentialsReflectOrigin.bend new file mode 100644 index 0000000..03b3437 --- /dev/null +++ b/laws/CorsCredentialsReflectOrigin.bend @@ -0,0 +1,9 @@ +import Base +import ../src/cors.bend as Cors + +# --- Fetch CORS (https://fetch.spec.whatwg.org/#cors-protocol) --- + +# LAW (Fetch CORS): credentials reflect the request origin, never `*` +law cors_credentials_reflect_origin: + {Cors.cors.acao(["*"], True{}, "https://app.example") + == "https://app.example" : String} diff --git a/laws/CorsGetIsNotPreflight.bend b/laws/CorsGetIsNotPreflight.bend new file mode 100644 index 0000000..e2571c3 --- /dev/null +++ b/laws/CorsGetIsNotPreflight.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): a GET with no Access-Control-Request-Method is not a preflight +law cors_get_is_not_preflight: + {Cors.cors.is_preflight(Http.Request{"GET", "/", + [Http.H{"Origin", "https://a"}], ""}) == False{} : Bool} diff --git a/laws/CorsNoVaryForStar.bend b/laws/CorsNoVaryForStar.bend new file mode 100644 index 0000000..dd45431 --- /dev/null +++ b/laws/CorsNoVaryForStar.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): Vary is not set for `*` +law cors_vary_star: + {Cors.cors.vary("*") == False{} : Bool} diff --git a/laws/CorsNoVaryWhenRefused.bend b/laws/CorsNoVaryWhenRefused.bend new file mode 100644 index 0000000..f64612a --- /dev/null +++ b/laws/CorsNoVaryWhenRefused.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): Vary is not set when the origin was refused +law cors_vary_empty: + {Cors.cors.vary("") == False{} : Bool} diff --git a/laws/CorsOptionsWithMethodIsPreflight.bend b/laws/CorsOptionsWithMethodIsPreflight.bend new file mode 100644 index 0000000..983f1fe --- /dev/null +++ b/laws/CorsOptionsWithMethodIsPreflight.bend @@ -0,0 +1,21 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): OPTIONS plus Access-Control-Request-Method is a preflight +law cors_on_preflight: + {Cors.cors.on(Cors.Cfg{["https://app.example"], ["GET", "PUT"], + ["Content-Type"], True{}, "600", ["X-Trace"]}, + Http.Request{"OPTIONS", "/", [ + Http.H{"Origin", "https://app.example"}, + Http.H{"Access-Control-Request-Method", "PUT"}, + Http.H{"Access-Control-Request-Headers", "Content-Type"} + ], ""}, Http.Reply{200, [], "nope"}) + == Http.Reply{200, [ + Http.H{"Access-Control-Allow-Credentials", "true"}, + Http.H{"Vary", "Origin"}, + Http.H{"Access-Control-Allow-Origin", "https://app.example"}, + Http.H{"Access-Control-Allow-Methods", "GET, PUT"}, + Http.H{"Access-Control-Allow-Headers", "Content-Type"}, + Http.H{"Access-Control-Max-Age", "600"} + ], ""} : Http.Reply} diff --git a/laws/CorsPreflightAnswersStar.bend b/laws/CorsPreflightAnswersStar.bend new file mode 100644 index 0000000..f62d65f --- /dev/null +++ b/laws/CorsPreflightAnswersStar.bend @@ -0,0 +1,14 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): `*` without credentials is ACAO on a successful preflight +law cors_preflight_star: + {Cors.cors.preflight(Cors.Cfg{["*"], ["GET"], [], False{}, "0", []}, + "https://a", "GET", "") + == Http.Reply{200, [ + Http.H{"Access-Control-Allow-Origin", "*"}, + Http.H{"Access-Control-Allow-Methods", "GET"}, + Http.H{"Access-Control-Allow-Headers", ""}, + Http.H{"Access-Control-Max-Age", "0"} + ], ""} : Http.Reply} diff --git a/laws/CorsPreflightHeaderDenied.bend b/laws/CorsPreflightHeaderDenied.bend new file mode 100644 index 0000000..b870ae0 --- /dev/null +++ b/laws/CorsPreflightHeaderDenied.bend @@ -0,0 +1,10 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): a requested header outside the allow-list fails preflight +law cors_preflight_header_denied: + {Cors.cors.preflight(Cors.Cfg{["https://app.example"], ["PUT"], + ["Content-Type"], False{}, "10", []}, + "https://app.example", "PUT", "X-Nope") + == Http.Reply{400, [], ""} : Http.Reply} diff --git a/laws/CorsPreflightHeadersIgnoreCase.bend b/laws/CorsPreflightHeadersIgnoreCase.bend new file mode 100644 index 0000000..d138e30 --- /dev/null +++ b/laws/CorsPreflightHeadersIgnoreCase.bend @@ -0,0 +1,16 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): requested header names are matched case-insensitively +law cors_preflight_header_case: + {Cors.cors.preflight(Cors.Cfg{["https://app.example"], ["PUT"], + ["Content-Type"], False{}, "10", []}, + "https://app.example", "PUT", "content-type") + == Http.Reply{200, [ + Http.H{"Vary", "Origin"}, + Http.H{"Access-Control-Allow-Origin", "https://app.example"}, + Http.H{"Access-Control-Allow-Methods", "PUT"}, + Http.H{"Access-Control-Allow-Headers", "Content-Type"}, + Http.H{"Access-Control-Max-Age", "10"} + ], ""} : Http.Reply} diff --git a/laws/CorsPreflightMethodDenied.bend b/laws/CorsPreflightMethodDenied.bend new file mode 100644 index 0000000..f233131 --- /dev/null +++ b/laws/CorsPreflightMethodDenied.bend @@ -0,0 +1,10 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): a method outside the allow-list is a failed preflight +law cors_preflight_method_denied: + {Cors.cors.preflight(Cors.Cfg{["https://app.example"], ["GET", "PUT"], + ["Content-Type"], True{}, "600", ["X-Trace"]}, + "https://app.example", "DELETE", "") + == Http.Reply{400, [], ""} : Http.Reply} diff --git a/laws/CorsPreflightOriginDenied.bend b/laws/CorsPreflightOriginDenied.bend new file mode 100644 index 0000000..31de261 --- /dev/null +++ b/laws/CorsPreflightOriginDenied.bend @@ -0,0 +1,10 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): a preflight whose origin is not allowed is 400 +law cors_preflight_origin_denied: + {Cors.cors.preflight(Cors.Cfg{["https://app.example"], ["PUT"], + ["Content-Type"], False{}, "10", []}, + "https://no.example", "PUT", "") + == Http.Reply{400, [], ""} : Http.Reply} diff --git a/laws/CorsReadsOriginHeader.bend b/laws/CorsReadsOriginHeader.bend new file mode 100644 index 0000000..c0451de --- /dev/null +++ b/laws/CorsReadsOriginHeader.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): Origin is the request header of that name +law cors_origin_header: + {Cors.cors.origin("https://app.example") + == Http.H{"Origin", "https://app.example"} : Http.Header} diff --git a/laws/CorsReadsRequestHeaders.bend b/laws/CorsReadsRequestHeaders.bend new file mode 100644 index 0000000..4be7223 --- /dev/null +++ b/laws/CorsReadsRequestHeaders.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): Access-Control-Request-Headers names the headers +law cors_request_headers_header: + {Cors.cors.request_headers("Content-Type") + == Http.H{"Access-Control-Request-Headers", "Content-Type"} : Http.Header} diff --git a/laws/CorsReadsRequestMethod.bend b/laws/CorsReadsRequestMethod.bend new file mode 100644 index 0000000..01de247 --- /dev/null +++ b/laws/CorsReadsRequestMethod.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): Access-Control-Request-Method names the method +law cors_request_method_header: + {Cors.cors.request_method("PUT") + == Http.H{"Access-Control-Request-Method", "PUT"} : Http.Header} diff --git a/laws/CorsRefusedLeavesReply.bend b/laws/CorsRefusedLeavesReply.bend new file mode 100644 index 0000000..ff319c4 --- /dev/null +++ b/laws/CorsRefusedLeavesReply.bend @@ -0,0 +1,9 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): a refused origin leaves the handler reply unchanged +law cors_simple_refused: + {Cors.cors.simple(Cors.Cfg{["https://ok.example"], [], [], False{}, "", []}, + "https://no.example", Http.Reply{200, [], "ok"}) + == Http.Reply{200, [], "ok"} : Http.Reply} diff --git a/laws/CorsRefusesMissingOrigin.bend b/laws/CorsRefusesMissingOrigin.bend new file mode 100644 index 0000000..20927e6 --- /dev/null +++ b/laws/CorsRefusesMissingOrigin.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): a missing origin is refused +law cors_refused_origin: + {Cors.cors.acao(["https://ok.example"], False{}, "https://no.example") + == "" : String} diff --git a/laws/CorsSimpleAnswersStar.bend b/laws/CorsSimpleAnswersStar.bend new file mode 100644 index 0000000..7775521 --- /dev/null +++ b/laws/CorsSimpleAnswersStar.bend @@ -0,0 +1,10 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): `*` without credentials is the only ACAO header +law cors_simple_star: + {Cors.cors.simple(Cors.Cfg{["*"], ["GET"], [], False{}, "", []}, + "https://app.example", Http.Reply{200, [], "ok"}) + == Http.Reply{200, [Http.H{"Access-Control-Allow-Origin", "*"}], "ok"} + : Http.Reply} diff --git a/laws/CorsSimpleKeepsBody.bend b/laws/CorsSimpleKeepsBody.bend new file mode 100644 index 0000000..ca9ba38 --- /dev/null +++ b/laws/CorsSimpleKeepsBody.bend @@ -0,0 +1,15 @@ +import Base +import ../src/http.bend as Http +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): a simple response keeps its body and gains CORS headers +law cors_simple_reflects: + {Cors.cors.simple(Cors.Cfg{["https://app.example"], ["GET", "PUT"], + ["Content-Type"], True{}, "600", ["X-Trace"]}, + "https://app.example", Http.Reply{200, [], "ok"}) + == Http.Reply{200, [ + Http.H{"Access-Control-Allow-Credentials", "true"}, + Http.H{"Vary", "Origin"}, + Http.H{"Access-Control-Expose-Headers", "X-Trace"}, + Http.H{"Access-Control-Allow-Origin", "https://app.example"} + ], "ok"} : Http.Reply} diff --git a/laws/CorsStarStaysStar.bend b/laws/CorsStarStaysStar.bend new file mode 100644 index 0000000..fd68b29 --- /dev/null +++ b/laws/CorsStarStaysStar.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): without credentials, `*` stays `*` +law cors_star_without_credentials: + {Cors.cors.acao(["*"], False{}, "https://app.example") == "*" : String} diff --git a/laws/CorsVaryOnSpecificOrigin.bend b/laws/CorsVaryOnSpecificOrigin.bend new file mode 100644 index 0000000..f6c1666 --- /dev/null +++ b/laws/CorsVaryOnSpecificOrigin.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cors.bend as Cors + +# LAW (Fetch CORS): Vary is set when ACAO is a specific origin +law cors_vary_specific: + {Cors.cors.vary("https://app.example") == True{} : Bool} diff --git a/laws/CutAtFirstEquals.bend b/laws/CutAtFirstEquals.bend new file mode 100644 index 0000000..7626247 --- /dev/null +++ b/laws/CutAtFirstEquals.bend @@ -0,0 +1,19 @@ +# `Qs.cut` cuts a text at its first `=` into the text before it and the +# text after it, and does not cut a text with no `=`. +import Base +import ../src/qs.bend as Qs +import ./defs/CharacterAbsent.bend as D1 + +# LAW: a text with no `=` before one is cut at that one +law qs_cut_found: + for +s: String + for r: String + for +e: {D1.avoids(s, '=') == True{} : Bool} + {Qs.cut(String.append(s, SCon{'=', r})) == Some{(s, r)} + : Maybe<&2, Qs.Kv()>} + +# LAW: a text with no `=` is not cut +law qs_cut_none: + for +s: String + for +e: {D1.avoids(s, '=') == True{} : Bool} + {Qs.cut(s) == None{} : Maybe<&2, Qs.Kv()>} diff --git a/laws/DeadlineIsTimeout.bend b/laws/DeadlineIsTimeout.bend new file mode 100644 index 0000000..1bd89de --- /dev/null +++ b/laws/DeadlineIsTimeout.bend @@ -0,0 +1,18 @@ +# The deadline an exchange gives bend-kit-wire for its connect, TLS +# handshake, sends and receives (`wire.ms.of`) is, for an exchange made with +# a timeout (`http.get_opts`, `http.request_opts`, `http.post_opts`, +# `http.put_opts`, `http.delete_opts`, `http.send.timed`), the timeout as +# laws/TimeoutZeroIsOneMs.bend gives it, and for every other exchange +# exactly 60000 ms. +# +# also rests on: laws/TimeoutZeroIsOneMs.bend +import Base +import ../src/wirespec.bend as Spec + +# --- the client's deadline --- + +# LAW: an exchange made with no timeout gives every call 60000 ms +law ms_none: + for m: Maybe<&2, U32> + for e: {None{} == m : Maybe<&2, U32>} + {Spec.wire.ms.of(m) == 60000 : U32} diff --git a/laws/DechunkEmptyIsEmpty.bend b/laws/DechunkEmptyIsEmpty.bend new file mode 100644 index 0000000..56258b1 --- /dev/null +++ b/laws/DechunkEmptyIsEmpty.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): an empty chunked body decodes to empty +law dechunk_empty: + {Http.dechunk("") == "" : String} diff --git a/laws/DechunkIgnoresExtension.bend b/laws/DechunkIgnoresExtension.bend new file mode 100644 index 0000000..b63f863 --- /dev/null +++ b/laws/DechunkIgnoresExtension.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): a chunk-ext after `;` does not change the decoded body +law dechunk_ignores_extension: + {Http.dechunk("5;ext=1\r\nhello\r\n0\r\n\r\n") == "hello" : String} diff --git a/laws/DechunkJoinsTwoChunks.bend b/laws/DechunkJoinsTwoChunks.bend new file mode 100644 index 0000000..85f2caa --- /dev/null +++ b/laws/DechunkJoinsTwoChunks.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): two non-empty chunks round-trip +law dechunk_two_chunks: + {Http.dechunk("1\r\nZ\r\n2\r\nOK\r\n0\r\n\r\n") == "ZOK" : String} diff --git a/laws/DechunkLastChunkAloneIsEmpty.bend b/laws/DechunkLastChunkAloneIsEmpty.bend new file mode 100644 index 0000000..a901f05 --- /dev/null +++ b/laws/DechunkLastChunkAloneIsEmpty.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): a body that is only the last chunk is empty +law dechunk_only_last: + {Http.dechunk("0\r\n\r\n") == "" : String} diff --git a/laws/DechunkRfcExample.bend b/laws/DechunkRfcExample.bend new file mode 100644 index 0000000..30b639e --- /dev/null +++ b/laws/DechunkRfcExample.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): the chunked example wiki + pedia is wikipedia +law dechunk_wiki: + {Http.dechunk("4\r\nwiki\r\n5\r\npedia\r\n0\r\n\r\n") == "wikipedia" : String} diff --git a/laws/DechunkStopsAtZero.bend b/laws/DechunkStopsAtZero.bend new file mode 100644 index 0000000..b26a0a4 --- /dev/null +++ b/laws/DechunkStopsAtZero.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): the chunk of size 0 ends the body; a trailer is not data +law dechunk_stops_at_zero: + {Http.dechunk("5\r\nhello\r\n0\r\nIgnored: 1\r\n\r\n") == "hello" : String} diff --git a/laws/DeleteIsIdempotent.bend b/laws/DeleteIsIdempotent.bend new file mode 100644 index 0000000..bbe10c8 --- /dev/null +++ b/laws/DeleteIsIdempotent.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW: DELETE is idempotent +law method_idempotent_delete: + {Http.method.idempotent("DELETE") == True{} : Bool} diff --git a/laws/DeleteNotSafe.bend b/laws/DeleteNotSafe.bend new file mode 100644 index 0000000..b6454a1 --- /dev/null +++ b/laws/DeleteNotSafe.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.2.1): DELETE is not safe +law method_safe_delete: + {Http.method.safe("DELETE") == False{} : Bool} diff --git a/laws/DeleteRequestLine.bend b/laws/DeleteRequestLine.bend new file mode 100644 index 0000000..321fce0 --- /dev/null +++ b/laws/DeleteRequestLine.bend @@ -0,0 +1,11 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.3.5): DELETE request-line opens with DELETE and the path +law delete_req_names_path: + for +host: String + for +path: String + for hs: List<&2, Http.Header> + for +body: String + {String.starts_with(Http.delete_req(host, path, hs, body), "DELETE " ++ path) + == True{} : Bool} diff --git a/laws/DotSegmentsRemoved.bend b/laws/DotSegmentsRemoved.bend new file mode 100644 index 0000000..80b6167 --- /dev/null +++ b/laws/DotSegmentsRemoved.bend @@ -0,0 +1,75 @@ +# Dot segments are removed as RFC 3986 §5.2.4 removes them, segment by +# segment from the left: a `.` segment is dropped, a `..` segment drops the +# last segment kept so far (none when none is kept), every other segment is +# kept, and a path that ends in `.` or `..` keeps its trailing `/`. +import Base +import ../src/redirect.bend as Redirect + +# LAW (RFC 3986 §5.2.4): a path is `/`, then its segments with dot segments +# removed, joined by `/` +law clean_path: + for ss: List<&2, String> + {Redirect.redirect.clean(ss) + == "/" ++ String.join(Redirect.redirect.dots(ss, []), "/") : String} + +# LAW: once every segment is read, the kept ones are the path, in order +law dots_nil: + for out: List<&2, String> + {Redirect.redirect.dots([], out) == List.reverse(&2, String, out) + : List<&2, String>} + +# LAW (RFC 3986 §5.2.4 B, D): a `.` segment is dropped +law dots_dot: + for +s: String + for +t: List<&2, String> + for out: List<&2, String> + for e: {True{} == String.eq(s, ".") : Bool} + {Redirect.redirect.dots(s <> t, out) + == Redirect.redirect.dots(t, + Redirect.redirect.dots.end(List.is_empty(&2, String, t), out)) + : List<&2, String>} + +# LAW (RFC 3986 §5.2.4 C, D): a `..` segment drops the last segment kept +law dots_up: + for +s: String + for +t: List<&2, String> + for out: List<&2, String> + for e1: {False{} == String.eq(s, ".") : Bool} + for e2: {True{} == String.eq(s, "..") : Bool} + {Redirect.redirect.dots(s <> t, out) + == Redirect.redirect.dots(t, + Redirect.redirect.dots.end(List.is_empty(&2, String, t), + Redirect.redirect.pop(out))) : List<&2, String>} + +# LAW (RFC 3986 §5.2.4 E): every other segment is kept +law dots_seg: + for +s: String + for +t: List<&2, String> + for out: List<&2, String> + for e1: {False{} == String.eq(s, ".") : Bool} + for e2: {False{} == String.eq(s, "..") : Bool} + {Redirect.redirect.dots(s <> t, out) == Redirect.redirect.dots(t, s <> out) + : List<&2, String>} + +# LAW (RFC 3986 §5.2.4 B, C): a path that ends in `.` or `..` keeps its +# trailing `/` +law dots_last: + for out: List<&2, String> + {Redirect.redirect.dots.end(True{}, out) == "" <> out : List<&2, String>} + +# LAW: a `.` or `..` before other segments adds nothing +law dots_inner: + for out: List<&2, String> + {Redirect.redirect.dots.end(False{}, out) == out : List<&2, String>} + +# LAW (RFC 3986 §5.2.4 C): `..` with nothing kept drops nothing +law pop_nil: + for out: List<&2, String> + for e: {True{} == List.is_empty(&2, String, out) : Bool} + {Redirect.redirect.pop(out) == [] : List<&2, String>} + +# LAW (RFC 3986 §5.2.4 C): `..` drops the last segment kept +law pop_cons: + for s: String + for t: List<&2, String> + {Redirect.redirect.pop(s <> t) == t : List<&2, String>} diff --git a/laws/EqualContentLengthsAreOne.bend b/laws/EqualContentLengthsAreOne.bend new file mode 100644 index 0000000..114080c --- /dev/null +++ b/laws/EqualContentLengthsAreOne.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §6.3): identical Content-Length values are that one length +law parse_equal_content_lengths: + {Http.parse("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\nhello!!") + == Http.Reply{200, [Http.H{"Content-Length", "5"}, + Http.H{"Content-Length", "5"}], "hello"} : Http.Reply} diff --git a/laws/ErrnoNamesAreLinux.bend b/laws/ErrnoNamesAreLinux.bend new file mode 100644 index 0000000..13c2c4f --- /dev/null +++ b/laws/ErrnoNamesAreLinux.bend @@ -0,0 +1,52 @@ +# The name of an error number (`wire.errno.name`) is its Linux errno symbol +# for 1 `EPERM`, 2 `ENOENT`, 4 `EINTR`, 5 `EIO`, 9 `EBADF`, 11 `EAGAIN`, 12 +# `ENOMEM`, 13 `EACCES`, 22 `EINVAL`, 23 `ENFILE`, 24 `EMFILE`, 32 `EPIPE`, +# 71 `EPROTO`, 88 `ENOTSOCK`, 93 `EPROTONOSUPPORT`, 97 `EAFNOSUPPORT`, 98 +# `EADDRINUSE`, 99 `EADDRNOTAVAIL`, 100 `ENETDOWN`, 101 `ENETUNREACH`, 103 +# `ECONNABORTED`, 104 `ECONNRESET`, 105 `ENOBUFS`, 106 `EISCONN`, 107 +# `ENOTCONN`, 110 `ETIMEDOUT`, 111 `ECONNREFUSED`, 112 `EHOSTDOWN`, 113 +# `EHOSTUNREACH`, 114 `EALREADY` and 115 `EINPROGRESS`, and `errno` for +# every other number. +# +# also rests on: laws/PhraseTableLookup.bend +import Base +import ../src/http.bend as Http +import ../src/wirespec.bend as Spec + +# the Linux errno symbols laws/ErrnoNamesAreLinux.bend lists, as the law +# states them +def errnos() -> List<&2, Http.Phrase>: + [Http.P{1, "EPERM"}, Http.P{2, "ENOENT"}, Http.P{4, "EINTR"}, + Http.P{5, "EIO"}, Http.P{9, "EBADF"}, Http.P{11, "EAGAIN"}, + Http.P{12, "ENOMEM"}, Http.P{13, "EACCES"}, Http.P{22, "EINVAL"}, + Http.P{23, "ENFILE"}, Http.P{24, "EMFILE"}, Http.P{32, "EPIPE"}, + Http.P{71, "EPROTO"}, Http.P{88, "ENOTSOCK"}, + Http.P{93, "EPROTONOSUPPORT"}, Http.P{97, "EAFNOSUPPORT"}, + Http.P{98, "EADDRINUSE"}, Http.P{99, "EADDRNOTAVAIL"}, + Http.P{100, "ENETDOWN"}, Http.P{101, "ENETUNREACH"}, + Http.P{103, "ECONNABORTED"}, Http.P{104, "ECONNRESET"}, + Http.P{105, "ENOBUFS"}, Http.P{106, "EISCONN"}, + Http.P{107, "ENOTCONN"}, Http.P{110, "ETIMEDOUT"}, + Http.P{111, "ECONNREFUSED"}, Http.P{112, "EHOSTDOWN"}, + Http.P{113, "EHOSTUNREACH"}, Http.P{114, "EALREADY"}, + Http.P{115, "EINPROGRESS"}] + +# LAW: an error number's name is the one the table lists for it, or `errno` +# when the table lists none (reason_in_nil, reason_in_hit, reason_in_miss +# read the table) +law errno_names: + for +n: U32 + {Spec.wire.errno.name(n) == Spec.wire.errno.or(Http.reason.in(errnos(), n)) + : String} + +# LAW: a number the table has no name for is named `errno` +law errno_or_none: + for s: String + for e: {True{} == String.is_empty(s) : Bool} + {Spec.wire.errno.or(s) == "errno" : String} + +# LAW: a name the table gives is kept as it is +law errno_or_named: + for s: String + for e: {False{} == String.is_empty(s) : Bool} + {Spec.wire.errno.or(s) == s : String} diff --git a/laws/ExactContentLengthBodyKept.bend b/laws/ExactContentLengthBodyKept.bend new file mode 100644 index 0000000..a47a3d9 --- /dev/null +++ b/laws/ExactContentLengthBodyKept.bend @@ -0,0 +1,11 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §8.6 / RFC 9112 §6.3): a body whose Content-Length is its own +# octet count comes back exactly. +law clen_exact: + for st: U32 + for hs: List<&2, Http.Header> + for +body: String + {Http.parse.clen(st, hs, Http.octs.len(body), body) + == Http.Reply{st, hs, body} : Http.Reply} diff --git a/laws/ExchangeCarriesMethod.bend b/laws/ExchangeCarriesMethod.bend new file mode 100644 index 0000000..d77b1e0 --- /dev/null +++ b/laws/ExchangeCarriesMethod.bend @@ -0,0 +1,38 @@ +# For every method, URL that parses, headers and body, the exchange the +# client builds carries exactly that method, and the request it carries +# opens with exactly ` HTTP/1.1` and CRLF; the client reads +# the answer by the exchange's method (laws/ResponseCompleteByFraming.bend), +# timed or not (trusted in src/client.bend). +# +# also rests on: laws/RequestLineAndHost.bend +import Base +import ../src/http.bend as Http +import ../src/url.bend as Url +import ../src/wirespec.bend as Spec + +# LAW: the method the wire layer reads the answer by is the method the +# exchange was built for, for every method and every URL that parses +law exchange_method: + for method: String + for scheme: String + for host: String + for port: U32 + for path: String + for hs: List<&2, Http.Header> + for body: String + {Spec.wire.exchange.method( + Spec.wire.exchange(method, Url.Loc{scheme, host, port, path}, hs, body)) + == Some{method} : Maybe<&2, String>} + +# LAW (RFC 9112 §3): for every method, the request a client writes opens with +# that method, the path and the protocol version on its request line +law request_line: + for +method: String + for secure: Bool + for host: String + for port: U32 + for +path: String + for hs: List<&2, Http.Header> + for +body: String + {String.starts_with(Http.request.at(method, secure, host, port, path, hs, + body), method ++ " " ++ path ++ " HTTP/1.1\r\n") == True{} : Bool} diff --git a/laws/FailedEffectsLoseConnection.bend b/laws/FailedEffectsLoseConnection.bend new file mode 100644 index 0000000..51220c1 --- /dev/null +++ b/laws/FailedEffectsLoseConnection.bend @@ -0,0 +1,62 @@ +# For every failed accept, recv or send with reason `why`, the connection is +# lost with the log line exactly `ezhttp: : `, phase `accept`, +# `read` or `write`; an effect that succeeds hands on its value unchanged. +# An accept whose deadline passes with no connection (ETIMEDOUT, 110) is +# neither: the server accepts again (`step.accepted`). +import Base +import ../src/step.bend as Step + +# LAW: a failed accept or recv loses the connection with the line +# `ezhttp: : ` +law lost_failed: + for -A: Type + for phase: String + for code: U32 + for why: String + {Step.step.got(A, phase, Fail{(code, why)}) + == Step.Lost{"ezhttp: " ++ phase ++ ": " ++ why} : Step.Got} + +# LAW: an accept or recv that succeeds hands on its value unchanged +law lost_kept: + for -A: Type + for phase: String + for value: A + {Step.step.got(A, phase, Done{value}) == Step.Kept{value} : Step.Got} + +# LAW: a failed send loses the connection with the line `ezhttp: write: ` +law lost_send: + for code: U32 + for why: String + for left: List<&2, U32> + {Step.step.sent(Fail{((code, why), left)}) + == Step.Lost{"ezhttp: write: " ++ why} : Step.Got} + +# LAW: a send that succeeds keeps the connection +law lost_sent: + for done: Unit + {Step.step.sent(Done{done}) == Step.Kept{done} : Step.Got} + +# --- the concurrent server's replies and accepts --- + +# LAW: an accept whose deadline passed (ETIMEDOUT) is not a lost connection +law accept_idle: + for -A: Type + for +code: U32 + for e: {True{} == U32.is_eq(code, 110) : Bool} + for why: String + {Step.step.accepted(A, Fail{(code, why)}) == Step.Idle{} : Step.Accept} + +# LAW: any other failed accept is lost with the line `ezhttp: accept: ` +law accept_missed: + for -A: Type + for +code: U32 + for e: {False{} == U32.is_eq(code, 110) : Bool} + for why: String + {Step.step.accepted(A, Fail{(code, why)}) + == Step.Missed{"ezhttp: accept: " ++ why} : Step.Accept} + +# LAW: an accept that brought a connection hands it on unchanged +law accept_took: + for -A: Type + for value: A + {Step.step.accepted(A, Done{value}) == Step.Took{value} : Step.Accept} diff --git a/laws/FailuresReportedByPhase.bend b/laws/FailuresReportedByPhase.bend new file mode 100644 index 0000000..2d65c71 --- /dev/null +++ b/laws/FailuresReportedByPhase.bend @@ -0,0 +1,147 @@ +# For every phase, host, port, deadline `ms`, error number `n` and reason +# `why` a failed resolve, connect, TLS handshake, send or receive gives, the +# exchange's answer (`wire.fail`) is: when `n` is ETIMEDOUT (110) and the +# phase is not the resolve, status 28 (laws/TimeoutsReadAsTimedOut.bend) and +# exactly `connecting to port took over ms`, `the TLS +# handshake with took over ms`, `sending the request to +# took over ms` or ` sent nothing for ms` for the connect, +# handshake, send and receive; otherwise status 6 and exactly `could not +# resolve ` when the resolve failed with ENOENT (2), which is how +# bend-kit-dns reports a name that has no address (an errno that would read +# as a missing file is left out), and `could not resolve : ` +# for every other number, status 7 and `could not reach port : +# `, status 35 and `the TLS handshake with failed: : +# `, status 55 and `the request to could not be sent: `, +# or status 56 and `the answer from was cut short: `, where +# the cause is ` ()`, the name as laws/ErrnoNamesAreLinux.bend +# gives it. +import Base +import ../src/wirespec.bend as Spec + +# LAW (laws/FailuresReportedByPhase.bend): a resolve that fails with any +# number but ENOENT is status 6 and names the host and the cause, ran out of +# time or not +law fail_resolve: + for +host: String + for +port: U32 + for +ms: U32 + for +n: U32 + for e: {False{} == U32.is_eq(n, 2) : Bool} + for +why: String + {Spec.wire.fail(Spec.Resolve{}, host, port, ms, n, why) + == "6\ncould not resolve " ++ host ++ ": " ++ Spec.wire.cause(n) : String} + +# LAW: a name with no address (ENOENT) is status 6 and the host alone +law fail_resolve_none: + for +host: String + for +port: U32 + for +ms: U32 + for +n: U32 + for e: {True{} == U32.is_eq(n, 2) : Bool} + for +why: String + {Spec.wire.fail(Spec.Resolve{}, host, port, ms, n, why) + == "6\ncould not resolve " ++ host : String} + +# LAW: a connect that ran out of time is status 28 and says how long it took +law fail_connect_late: + for host: String + for +port: U32 + for +ms: U32 + for +n: U32 + for e: {True{} == U32.is_eq(n, 110) : Bool} + for why: String + {Spec.wire.fail(Spec.Connect{}, host, port, ms, n, why) + == "28\nconnecting to " ++ host ++ " port " ++ U32.show(port) + ++ " took over " ++ U32.show(ms) ++ " ms" : String} + +# LAW: a TLS handshake that ran out of time is status 28 +law fail_handshake_late: + for host: String + for +port: U32 + for +ms: U32 + for +n: U32 + for e: {True{} == U32.is_eq(n, 110) : Bool} + for why: String + {Spec.wire.fail(Spec.Handshake{}, host, port, ms, n, why) + == "28\nthe TLS handshake with " ++ host ++ " took over " + ++ U32.show(ms) ++ " ms" : String} + +# LAW: a send that ran out of time is status 28 +law fail_send_late: + for host: String + for +port: U32 + for +ms: U32 + for +n: U32 + for e: {True{} == U32.is_eq(n, 110) : Bool} + for why: String + {Spec.wire.fail(Spec.Send{}, host, port, ms, n, why) + == "28\nsending the request to " ++ host ++ " took over " + ++ U32.show(ms) ++ " ms" : String} + +# LAW: a receive that ran out of time is status 28 +law fail_receive_late: + for host: String + for +port: U32 + for +ms: U32 + for +n: U32 + for e: {True{} == U32.is_eq(n, 110) : Bool} + for why: String + {Spec.wire.fail(Spec.Receive{}, host, port, ms, n, why) + == "28\n" ++ host ++ " sent nothing for " ++ U32.show(ms) ++ " ms" + : String} + +# LAW: a connect that failed otherwise is status 7 and names host, port and +# cause +law fail_connect: + for host: String + for +port: U32 + for +ms: U32 + for +n: U32 + for e: {False{} == U32.is_eq(n, 110) : Bool} + for why: String + {Spec.wire.fail(Spec.Connect{}, host, port, ms, n, why) + == "7\ncould not reach " ++ host ++ " port " ++ U32.show(port) ++ ": " + ++ Spec.wire.cause(n) : String} + +# LAW: a TLS handshake that failed otherwise is status 35, with the cause and +# the reason bend-kit-wire gave +law fail_handshake: + for host: String + for +port: U32 + for +ms: U32 + for +n: U32 + for e: {False{} == U32.is_eq(n, 110) : Bool} + for why: String + {Spec.wire.fail(Spec.Handshake{}, host, port, ms, n, why) + == "35\nthe TLS handshake with " ++ host ++ " failed: " + ++ Spec.wire.cause(n) ++ ": " ++ why : String} + +# LAW: a send that failed otherwise is status 55 +law fail_send: + for host: String + for +port: U32 + for +ms: U32 + for +n: U32 + for e: {False{} == U32.is_eq(n, 110) : Bool} + for why: String + {Spec.wire.fail(Spec.Send{}, host, port, ms, n, why) + == "55\nthe request to " ++ host ++ " could not be sent: " + ++ Spec.wire.cause(n) : String} + +# LAW: a receive that failed otherwise is status 56 +law fail_receive: + for host: String + for +port: U32 + for +ms: U32 + for +n: U32 + for e: {False{} == U32.is_eq(n, 110) : Bool} + for why: String + {Spec.wire.fail(Spec.Receive{}, host, port, ms, n, why) + == "56\nthe answer from " ++ host ++ " was cut short: " + ++ Spec.wire.cause(n) : String} + +# LAW: a cause is the error number's name, then the number in parentheses +law cause_is: + for +n: U32 + {Spec.wire.cause(n) == Spec.wire.errno.name(n) ++ " (" ++ U32.show(n) ++ ")" + : String} diff --git a/laws/FieldLookupFindsFirst.bend b/laws/FieldLookupFindsFirst.bend new file mode 100644 index 0000000..bb1d87c --- /dev/null +++ b/laws/FieldLookupFindsFirst.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §5.2): the first field of a repeated name is the one found +law headers_find_first: + {Http.headers.find([Http.H{"Host", "a"}, Http.H{"Host", "b"}], "host") + == "a" : String} diff --git a/laws/FieldLookupIgnoresCase.bend b/laws/FieldLookupIgnoresCase.bend new file mode 100644 index 0000000..20943b4 --- /dev/null +++ b/laws/FieldLookupIgnoresCase.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §5.1): field lookup folds the name and keeps the value +law headers_find_ignores_case: + {Http.headers.find([Http.H{"hOsT", "Ex.COM"}], "host") == "Ex.COM" : String} diff --git a/laws/FieldNamesIgnoreCase.bend b/laws/FieldNamesIgnoreCase.bend new file mode 100644 index 0000000..8687155 --- /dev/null +++ b/laws/FieldNamesIgnoreCase.bend @@ -0,0 +1,9 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §5.1): field names are compared case-insensitively +law head_hit_ignores_case: + for +h: String + for +name: String + {Http.head.hit(h, name) + == String.starts_with(String.to_lower(h), name ++ ":") : Bool} diff --git a/laws/FieldValueTrimsWhitespace.bend b/laws/FieldValueTrimsWhitespace.bend new file mode 100644 index 0000000..13fd334 --- /dev/null +++ b/laws/FieldValueTrimsWhitespace.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §5.5): OWS around a field value is trimmed +law head_line_trims: + {Http.head.line("A: b ") == Some{Http.H{"A", "b"}} : Maybe<&2, Http.Header>} diff --git a/laws/FirstIsFirstWord.bend b/laws/FirstIsFirstWord.bend new file mode 100644 index 0000000..93aa489 --- /dev/null +++ b/laws/FirstIsFirstWord.bend @@ -0,0 +1,10 @@ +import Base +import ../src/http.bend as Http +import ./defs/CharacterAbsent.bend as D1 + +# LAW: the first piece of a line split on a space is the word before it +law first_is_first_word: + for +w: String + for r: String + for +e: {D1.avoids(w, ' ') == True{} : Bool} + {Http.first(String.split(String.append(w, SCon{' ', r}), ' ')) == w : String} diff --git a/laws/FormBodyLayout.bend b/laws/FormBodyLayout.bend new file mode 100644 index 0000000..5afbb2e --- /dev/null +++ b/laws/FormBodyLayout.bend @@ -0,0 +1,67 @@ +# For every valid boundary `b` and list of parts, the body is laid out, for +# each part in order, as `--b` CRLF, the part's header lines +# (laws/FormPartHeaders.bend), CRLF, the part's content (a field's value as +# octets; for a file, its bytes as they are, the next file's in the order +# the parts give), CRLF; then `--b--` CRLF (RFC 7578 §4.1, RFC 2046 §5.1.1); +# every text in it is written as its UTF-8 octets, character by character. +import Base +import ../src/pct.bend as Pct +import ../src/form.bend as Form + +# LAW: a field's content is a run of its value's UTF-8 octets +law content_field: + for name: String + for value: String + for rest: List<&2, Form.Seg> + {Form.form.content(Form.Field{name, value}, rest) + == Form.Run{Form.octets.onto(value, [])} <> rest : List<&2, Form.Seg>} + +# LAW: a file's content is the next file's bytes, unchanged +law content_file: + for name: String + for filename: String + for ctype: String + for clash: Bool + for rest: List<&2, Form.Seg> + {Form.form.content(Form.File{name, filename, ctype, clash}, rest) + == Form.Blob{} <> rest : List<&2, Form.Seg>} + +# LAW (RFC 2046 §5.1.1): after the last part comes the close delimiter +law parts_nil: + for +b: String + {Form.form.parts(b, []) == [Form.Run{Form.octets.onto("--" ++ b ++ "--\r\n", [])}] + : List<&2, Form.Seg>} + +# LAW: the parts are written in order, each in front of the rest +law parts_cons: + for +b: String + for p: Form.Part + for t: List<&2, Form.Part> + {Form.form.parts(b, p <> t) == Form.form.part(b, p, Form.form.parts(b, t)) + : List<&2, Form.Seg>} + +# LAW (RFC 7578 §4.1): a part is the delimiter line, its header lines, a blank +# line, its content and a CRLF +law part: + for +b: String + for +p: Form.Part + for rest: List<&2, Form.Seg> + {Form.form.part(b, p, rest) + == Form.Run{Form.octets.onto("--" ++ b ++ "\r\n" ++ Form.form.head(p) + ++ "\r\n", [])} <> Form.form.content(p, + Form.Run{Form.octets.onto("\r\n", [])} <> rest) : List<&2, Form.Seg>} + +# LAW: the empty string writes no octets +law onto_nil: + for rest: List<&2, U32> + {Form.octets.onto("", rest) == rest : List<&2, U32>} + +# LAW (RFC 3629 §3): a string is written as its first character's UTF-8 +# octets, then the rest's +law onto_cons: + for +c: U32 + for t: String + for rest: List<&2, U32> + {Form.octets.onto(SCon{Chr{c}, t}, rest) + == List.append(&2, U32, Pct.utf8(c), Form.octets.onto(t, rest)) + : List<&2, U32>} diff --git a/laws/FormBoundaryRules.bend b/laws/FormBoundaryRules.bend new file mode 100644 index 0000000..f3536df --- /dev/null +++ b/laws/FormBoundaryRules.bend @@ -0,0 +1,160 @@ +# A boundary is valid exactly when it has 1 to 70 characters (RFC 2046 +# §5.1.1), each `A`-`Z`, `a`-`z`, `0`-`9`, `-`, `_` or `.` (characters RFC +# 2046 allows in a boundary and RFC 9110 §5.6.2 in a token, so the parameter +# needs no quotes). The delimiter occurs in a field's content (its value as +# UTF-8 octets) exactly when the octets of `--b` appear in it as a +# contiguous run, anywhere; it occurs in a file's content exactly when +# `Bytes.find` finds the octets of `--b` in its bytes (trusted in +# src/wire.bend). A part's content type holds a CR or LF exactly when it is +# a file part whose content type contains one; a field part has none. +# +# also rests on: laws/FormPartHeaders.bend +import Base +import ../src/form.bend as Form + +# LAW (RFC 2046 §5.1.1): a boundary is 1 to 70 boundary characters +law boundary_ok: + for +b: String + {Form.form.boundary.ok(b) + == Bool.and(Bool.and(Bool.not(String.is_empty(b)), + Nat.is_le(String.length(b), 70n)), Form.form.bchars(b)) : Bool} + +# LAW: the boundary characters are A-Z, a-z, 0-9, `-`, `_` and `.` +law bchar: + for +cp: U32 + {Form.form.bchar(cp) + == Bool.or(Bool.and(U32.is_le(65, cp), U32.is_le(cp, 90)), + Bool.or(Bool.and(U32.is_le(97, cp), U32.is_le(cp, 122)), + Bool.or(Bool.and(U32.is_le(48, cp), U32.is_le(cp, 57)), + Bool.or(U32.is_eq(cp, 45), + Bool.or(U32.is_eq(cp, 95), U32.is_eq(cp, 46)))))) : Bool} + +# LAW: the empty string has no character that is not a boundary character +law bchars_nil: + for s: String + for e: {SNil{} == s : String} + {Form.form.bchars(s) == True{} : Bool} + +# LAW: a string's characters are boundary characters exactly when its first +# is and the rest's are +law bchars_cons: + for +c: U32 + for t: String + {Form.form.bchars(SCon{Chr{c}, t}) + == Bool.and(Form.form.bchar(c), Form.form.bchars(t)) : Bool} + +# LAW (RFC 2046 §5.1.1): the delimiter is `--` and the boundary, as octets +law delim: + for b: String + {Form.form.delim(b) == Form.octets.onto("--" ++ b, []) : List<&2, U32>} + +# LAW: no parts, no clash +law clash_nil: + for b: String + {Form.form.clash(b, []) == False{} : Bool} + +# LAW: a list of parts clashes exactly when its first part's content holds +# the delimiter or the rest clashes +law clash_cons: + for +b: String + for p: Form.Part + for t: List<&2, Form.Part> + {Form.form.clash(b, p <> t) + == Bool.or(Form.form.hit(b, p), Form.form.clash(b, t)) : Bool} + +# LAW: a field's content holds the delimiter exactly when its value's UTF-8 +# octets hold the delimiter's octets as a contiguous run +law hit_field: + for +b: String + for name: String + for value: String + {Form.form.hit(b, Form.Field{name, value}) + == Form.octets.has(Form.octets.onto(value, []), Form.form.delim(b)) : Bool} + +# LAW: a file's content holds the delimiter exactly when the search of its +# bytes found it +law hit_file: + for +b: String + for name: String + for filename: String + for ctype: String + for clash: Bool + {Form.form.hit(b, Form.File{name, filename, ctype, clash}) == clash : Bool} + +# LAW: a check of this and the rest holds exactly when both do +law all_step: + for ok: Bool + for rest: Unit -> Bool + {Form.all.step(ok, rest) == Bool.and(ok, rest(Unit{})) : Bool} + +# LAW: a search of this and the rest hits exactly when either does +law any_step: + for hit: Bool + for rest: Unit -> Bool + {Form.any.step(hit, rest) == Bool.or(hit, rest(Unit{})) : Bool} + +# LAW: no octets hold a pattern exactly when the pattern is empty +law has_nil: + for +pat: List<&2, U32> + {Form.octets.has([], pat) == Form.octets.starts([], pat) : Bool} + +# LAW: octets hold a pattern exactly when they open with it or their rest +# holds it +law has_cons: + for +x: U32 + for +t: List<&2, U32> + for +pat: List<&2, U32> + {Form.octets.has(x <> t, pat) + == Bool.or(Form.octets.starts(x <> t, pat), Form.octets.has(t, pat)) + : Bool} + +# LAW: every octets open with the empty pattern +law starts_done: + for xs: List<&2, U32> + {Form.octets.starts(xs, []) == True{} : Bool} + +# LAW: no octets open with a pattern that is not empty +law starts_short: + for p: U32 + for q: List<&2, U32> + {Form.octets.starts([], p <> q) == False{} : Bool} + +# LAW: octets open with a pattern exactly when their first octets are equal +# and the rest opens with the pattern's rest +law starts_cons: + for x: U32 + for t: List<&2, U32> + for p: U32 + for q: List<&2, U32> + {Form.octets.starts(x <> t, p <> q) + == Bool.and(U32.is_eq(x, p), Form.octets.starts(t, q)) : Bool} + +# LAW: no parts hold a bad content type +law badtype_nil: + for ps: List<&2, Form.Part> + for e: {[] == ps : List<&2, Form.Part>} + {Form.form.badtype(ps) == False{} : Bool} + +# LAW: a list of parts holds a bad content type exactly when its first part +# or its rest does +law badtype_cons: + for p: Form.Part + for t: List<&2, Form.Part> + {Form.form.badtype(p <> t) + == Bool.or(Form.form.badtype.one(p), Form.form.badtype(t)) : Bool} + +# LAW: a field part has no content type to be bad +law badtype_field: + for name: String + for value: String + {Form.form.badtype.one(Form.Field{name, value}) == False{} : Bool} + +# LAW: a file part's content type is bad exactly when it holds a CR or an LF +law badtype_file: + for name: String + for filename: String + for +ctype: String + for clash: Bool + {Form.form.badtype.one(Form.File{name, filename, ctype, clash}) + == Bool.or(String.contains(ctype, "\r"), String.contains(ctype, "\n")) + : Bool} diff --git a/laws/FormNamesEscaped.bend b/laws/FormNamesEscaped.bend new file mode 100644 index 0000000..d108cc6 --- /dev/null +++ b/laws/FormNamesEscaped.bend @@ -0,0 +1,44 @@ +# For every name or filename, `q` writes its characters in order, each +# independently of the others: `"` as `%22`, CR as `%0D`, LF as `%0A` (RFC +# 7578 §4.2, as HTML's form submission encodes them), and every other +# character as itself. +import Base +import ../src/form.bend as Form + +# LAW: the empty name is written as the empty string +law fq_nil: + for s: String + for e: {SNil{} == s : String} + {Form.form.quote(s) == "" : String} + +# LAW: a name is written one character at a time, the first character's +# writing followed by the rest's +law fq_cons: + for +c: U32 + for t: String + {Form.form.quote(SCon{Chr{c}, t}) == Form.form.one(c, Form.form.quote(t)) + : String} + +# LAW (RFC 7578 §4.2): `"` is written as %22 +law fq_dq: + for rest: String + {Form.form.one(34, rest) == "%22" ++ rest : String} + +# LAW (RFC 7578 §4.2): CR is written as %0D +law fq_cr: + for rest: String + {Form.form.one(13, rest) == "%0D" ++ rest : String} + +# LAW (RFC 7578 §4.2): LF is written as %0A +law fq_lf: + for rest: String + {Form.form.one(10, rest) == "%0A" ++ rest : String} + +# LAW: every other character is written as itself +law fq_other: + for +c: U32 + for e1: {False{} == U32.is_eq(c, 34) : Bool} + for e2: {False{} == U32.is_eq(c, 13) : Bool} + for e3: {False{} == U32.is_eq(c, 10) : Bool} + for rest: String + {Form.form.one(c, rest) == SCon{Chr{c}, rest} : String} diff --git a/laws/FormPartHeaders.bend b/laws/FormPartHeaders.bend new file mode 100644 index 0000000..662693a --- /dev/null +++ b/laws/FormPartHeaders.bend @@ -0,0 +1,57 @@ +# A field part named `n` (`form_field(n, v)`) has exactly the header line +# `Content-Disposition: form-data; name=""`; a file part +# (`form_file(n, f, t, bytes)`, `bytes` packed `Bytes`) has exactly +# `Content-Disposition: form-data; name=""; filename=""` and +# `Content-Type: ` (RFC 7578 §4.2, §4.4), each line ended by CRLF, `q` as +# laws/FormNamesEscaped.bend writes it. +import Base +import ../src/form.bend as Form +import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes + +# LAW: form_field makes a field part +law field_is: + for +d: String + for name: String + for value: String + {Pair.fst(List<&2, Form.Part>, List<&1, Bytes.Bytes>, + Form.form.split(d, [Form.form.field(name, value)])) + == [Form.Field{name, value}] : List<&2, Form.Part>} + +# LAW: a file part is planned with its names, its content type and whether +# the search of its bytes found the delimiter +law file_is: + for name: String + for filename: String + for ctype: String + for n: U32 + for buf: Array + for hit: Bool + {Pair.fst(List<&2, Form.Part>, List<&1, Bytes.Bytes>, + Form.form.split.file(name, filename, ctype, (Bytes.Bytes{n, buf}, hit), + ([], []))) + == [Form.File{name, filename, ctype, hit}] : List<&2, Form.Part>} + +# LAW (RFC 7578 §4.2): a field part's header is its Content-Disposition line +law head_field: + for name: String + for value: String + {Form.form.head(Form.Field{name, value}) + == Form.form.disposition(name) ++ "\r\n" : String} + +# LAW (RFC 7578 §4.2, §4.4): a file part's header is its Content-Disposition +# line with the filename, then its Content-Type line +law head_file: + for name: String + for filename: String + for ctype: String + for clash: Bool + {Form.form.head(Form.File{name, filename, ctype, clash}) + == Form.form.disposition(name) ++ "; filename=\"" ++ Form.form.quote(filename) + ++ "\"\r\n" ++ "Content-Type: " ++ ctype ++ "\r\n" : String} + +# LAW (RFC 7578 §4.2): the disposition names the part, quoted +law disposition: + for name: String + {Form.form.disposition(name) + == "Content-Disposition: form-data; name=\"" ++ Form.form.quote(name) + ++ "\"" : String} diff --git a/laws/FormRefusesBadRequests.bend b/laws/FormRefusesBadRequests.bend new file mode 100644 index 0000000..cad50b7 --- /dev/null +++ b/laws/FormRefusesBadRequests.bend @@ -0,0 +1,70 @@ +# `form.body(boundary, parts)` (`src/form.bend`, exported as `form_body`, +# with `form_field` and `form_file`) builds a `multipart/form-data` request +# body (RFC 7578) from text fields and file parts, and does no IO. A file +# part's content is packed `Bytes`. The builder first looks for the +# delimiter in each file's bytes (`Bytes.find`, trusted in src/wire.bend), +# which gives the parts' plan (`form.plan`): every decision is made there, +# over the parts with each file's content replaced by whether the delimiter +# occurs in it. The plan lays the body out as runs of octets and file +# contents in order, and the body (`Body.Octets`) is those runs and each +# file's bytes, unchanged, put together in that order (trusted in +# src/wire.bend), so a file part's bytes reach the wire unchanged +# (laws/OctetBodiesSentUnchanged.bend). The caller chooses the boundary and +# the builder refuses one that could be misread. Text is written as its +# UTF-8 octets (RFC 3629 §3). +# +# For every boundary `b` and list of parts, the builder refuses exactly +# these, in this order, each with its own reason: a boundary that is not +# valid (laws/FormBoundaryRules.bend); a boundary whose delimiter `--b` +# occurs in some part's content (laws/FormBoundaryRules.bend); a file part +# whose content type holds a CR or an LF. Every other request is planned: +# its Content-Type field value is exactly `multipart/form-data; +# boundary=` and its body is laid out as laws/FormBodyLayout.bend says. +import Base +import ../src/form.bend as Form + +# --- multipart/form-data bodies --- + +# LAW: a boundary that is not valid is refused +law form_bad_boundary: + for +b: String + for ps: List<&2, Form.Part> + for e: {False{} == Form.form.boundary.ok(b) : Bool} + {Form.form.plan(b, ps) + == Form.NoPlan{"the boundary must be 1 to 70 of A-Z a-z 0-9 - _ ."} + : Form.Plan} + +# LAW: a valid boundary whose delimiter occurs in a part's content is refused +law form_clash: + for +b: String + for +ps: List<&2, Form.Part> + for e1: {True{} == Form.form.boundary.ok(b) : Bool} + for e2: {True{} == Form.form.clash(b, ps) : Bool} + {Form.form.plan(b, ps) == Form.NoPlan{"the boundary occurs in a part's content"} + : Form.Plan} + +# LAW: a file part whose content type holds a CR or LF is refused +law form_bad_type: + for +b: String + for +ps: List<&2, Form.Part> + for e1: {True{} == Form.form.boundary.ok(b) : Bool} + for e2: {False{} == Form.form.clash(b, ps) : Bool} + for e3: {True{} == Form.form.badtype(ps) : Bool} + {Form.form.plan(b, ps) + == Form.NoPlan{"a file part's content type holds a CR or LF"} : Form.Plan} + +# LAW (RFC 7578 §4.1): every other form is planned: its Content-Type value +# and its parts' runs +law form_ok: + for +b: String + for +ps: List<&2, Form.Part> + for e1: {True{} == Form.form.boundary.ok(b) : Bool} + for e2: {False{} == Form.form.clash(b, ps) : Bool} + for e3: {False{} == Form.form.badtype(ps) : Bool} + {Form.form.plan(b, ps) == Form.Plan{Form.form.type(b), Form.form.parts(b, ps)} + : Form.Plan} + +# LAW (RFC 7578 §4.1): the Content-Type field value names the boundary +law form_type: + for b: String + {Form.form.type(b) == "multipart/form-data; boundary=" ++ b : String} diff --git a/laws/FreshUnderMaxAge.bend b/laws/FreshUnderMaxAge.bend new file mode 100644 index 0000000..0035707 --- /dev/null +++ b/laws/FreshUnderMaxAge.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §4.2.1): an age under max-age is fresh +law fresh_max_age_open: + {Cache.cache.fresh(Cache.cache.parse("max-age=10"), 0n) == True{} : Bool} diff --git a/laws/GetAsteriskTargetRejected.bend b/laws/GetAsteriskTargetRejected.bend new file mode 100644 index 0000000..daee653 --- /dev/null +++ b/laws/GetAsteriskTargetRejected.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §3.2.4): asterisk-form is not a GET target +law ask_get_asterisk_rejected: + {Http.ask("GET * HTTP/1.1\r\nHost: h\r\n\r\n") + == Http.Bad{"the request-target is not in origin-form"} : Http.Request} diff --git a/laws/GetIsIdempotent.bend b/laws/GetIsIdempotent.bend new file mode 100644 index 0000000..a12e381 --- /dev/null +++ b/laws/GetIsIdempotent.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.2.2): GET is idempotent +law method_idempotent_get: + {Http.method.idempotent("GET") == True{} : Bool} diff --git a/laws/GetIsSafe.bend b/laws/GetIsSafe.bend new file mode 100644 index 0000000..b71ed8a --- /dev/null +++ b/laws/GetIsSafe.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.2.1): GET, HEAD, and OPTIONS are safe; `get` is not GET +law method_safe_get: + {Http.method.safe("GET") == True{} : Bool} diff --git a/laws/GetOmitsContentLength.bend b/laws/GetOmitsContentLength.bend new file mode 100644 index 0000000..e12f4ae --- /dev/null +++ b/laws/GetOmitsContentLength.bend @@ -0,0 +1,13 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §8.6): an empty GET omits Content-Length +law get_omits_content_length: + {Http.get_req("h", "/", []) + == "GET / HTTP/1.1\r\n" + ++ "Host: h\r\n" + ++ "User-Agent: ezhttp\r\n" + ++ "Accept: */*\r\n" + ++ "Connection: close\r\n" + ++ "\r\n" + : String} diff --git a/laws/GetRequestLine.bend b/laws/GetRequestLine.bend new file mode 100644 index 0000000..e2627d5 --- /dev/null +++ b/laws/GetRequestLine.bend @@ -0,0 +1,10 @@ +import Base +import ../src/http.bend as Http + +# LAW: GET request-line opens with GET and the path +law get_req_names_path: + for +host: String + for +path: String + for hs: List<&2, Http.Header> + {String.starts_with(Http.get_req(host, path, hs), "GET " ++ path) + == True{} : Bool} diff --git a/laws/HandlerGetsRequestText.bend b/laws/HandlerGetsRequestText.bend new file mode 100644 index 0000000..ef73980 --- /dev/null +++ b/laws/HandlerGetsRequestText.bend @@ -0,0 +1,28 @@ +# For every request that parses, the server calls the handler with that +# request as text (`step.text`): its method, target, header names and values +# and body each decoded from the octets received as UTF-8 +# (laws/Utf8DecodesWithReplacement.bend, laws/OctetsDecodeAsUtf8.bend), and +# otherwise unchanged. +import Base +import ../src/http.bend as Http +import ../src/step.bend as Step +import ../src/qs.bend as Qs + +# LAW: a request that parses goes to the handler exactly as it was parsed +law step_call: + for method: String + for target: String + for hs: List<&2, Http.Header> + for body: String + {Step.step.of(Http.Request{method, target, hs, body}) + == Step.Call{Http.Request{method, target, hs, body}} : Step.Step} + +# LAW: a request is handed on as text, each part decoded +law step_text: + for method: String + for target: String + for hs: List<&2, Http.Header> + for body: String + {Step.step.text(Http.Request{method, target, hs, body}) + == Http.Request{Qs.octets.text(method), Qs.octets.text(target), + Qs.octets.headers(hs), Qs.octets.text(body)} : Http.Request} diff --git a/laws/HeadAndNoContentHaveNoBody.bend b/laws/HeadAndNoContentHaveNoBody.bend new file mode 100644 index 0000000..ef82fa1 --- /dev/null +++ b/laws/HeadAndNoContentHaveNoBody.bend @@ -0,0 +1,44 @@ +# For every reply, the server writes it with an empty body when the request +# method is exactly `HEAD` (RFC 9110 §9.3.2) or the status is 204 or 304 +# (RFC 9110 §15.3.5, §15.4.5), and writes its status, headers and body +# unchanged otherwise. +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.3.2): a reply to HEAD loses its body and keeps the rest +law shape_head: + for status: U32 + for hs: List<&2, Http.Header> + for body: String + {Http.reply.for("HEAD", Http.Reply{status, hs, body}) + == Http.Reply{status, hs, ""} : Http.Reply} + +# LAW: a reply to any method other than HEAD is left as it is +law shape_other: + for method: String + for e: {False{} == String.eq(method, "HEAD") : Bool} + for reply: Http.Reply + {Http.reply.for(method, reply) == reply : Http.Reply} + +# LAW (RFC 9110 §15.3.5): a 204 is written with no body +law respond_204: + for hs: List<&2, Http.Header> + for body: String + {Http.respond.of(Http.Reply{204, hs, body}) == Http.respond(204, hs, "") + : String} + +# LAW (RFC 9110 §15.4.5): a 304 is written with no body +law respond_304: + for hs: List<&2, Http.Header> + for body: String + {Http.respond.of(Http.Reply{304, hs, body}) == Http.respond(304, hs, "") + : String} + +# LAW: any other status is written with its headers and body as they are +law respond_other: + for +status: U32 + for e: {False{} == Http.status.nocontent(status) : Bool} + for hs: List<&2, Http.Header> + for body: String + {Http.respond.of(Http.Reply{status, hs, body}) + == Http.respond(status, hs, body) : String} diff --git a/laws/HeadIsIdempotent.bend b/laws/HeadIsIdempotent.bend new file mode 100644 index 0000000..c9e3718 --- /dev/null +++ b/laws/HeadIsIdempotent.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.2.2): HEAD is idempotent +law method_idempotent_head: + {Http.method.idempotent("HEAD") == True{} : Bool} diff --git a/laws/HeadIsSafe.bend b/laws/HeadIsSafe.bend new file mode 100644 index 0000000..601f2d3 --- /dev/null +++ b/laws/HeadIsSafe.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.2.1): HEAD is safe +law method_safe_head: + {Http.method.safe("HEAD") == True{} : Bool} diff --git a/laws/HeadPlannedBeforeBody.bend b/laws/HeadPlannedBeforeBody.bend new file mode 100644 index 0000000..cc34624 --- /dev/null +++ b/laws/HeadPlannedBeforeBody.bend @@ -0,0 +1,128 @@ +# The client plans a response from its head before it touches the body's +# octets, with the same decisions the text parse makes. For every status +# code `m` (or none) and head lines `ls`, the head's plan (`head.plan`) is +# torn with `the response has no status line` when there is no status code; +# torn with `Content-Length values disagree` when its Content-Length values +# are not all the same digits (RFC 9112 §6.3 item 5); torn with +# `Content-Length and Transfer-Encoding conflict` when Transfer-Encoding +# names `chunked` and a Content-Length is present (RFC 9112 §6.3 item 3); +# chunked when Transfer-Encoding names `chunked` (item 4); sized by the +# Content-Length when it reads as a number; and framed by the close +# otherwise (item 8); and for every body, `parse_response`'s reading of that +# head and body is exactly what the plan says of the body (`plan.text`). For +# every method flag, head text and text after it whose head is not skipped +# as interim (laws/InterimResponsesSkipped.bend), the client's reading of +# that head and text is exactly `plan.text` of the final head's plan +# (`head.final`: no body for `HEAD`, 1xx, 204 or 304, RFC 9110 §6.4.1). For +# every chunk-size line `h` and the text `r` after it, the chunked decoder's +# step is decided by the line alone (`chunk.size`) and then `r` +# (`chunk.next`): `chunk size is not hexadecimal` when its size is not hex +# digits, the last chunk when the size is 0, and otherwise the next chunk +# header when that many octets and a CRLF follow, else `the chunked body +# stopped short` (RFC 9112 §7.1). +import Base +import ../src/http.bend as Http + +# --- a response planned from its head --- + +# LAW (RFC 9112 §4): a final head with no status code is torn +law plan_none: + for ls: List<&2, String> + {Http.head.plan(None{}, ls) + == Http.Unplanned{"the response has no status line"} : Http.Plan} + +# LAW (RFC 9112 §6.3 item 5): Content-Length values that are not all the +# same digits are torn +law plan_disagree: + for n: U32 + for +ls: List<&2, String> + for e: {False{} == Http.clen.agree(Http.head.values(ls, "content-length")) + : Bool} + {Http.head.plan(Some{n}, ls) + == Http.Unplanned{"Content-Length values disagree"} : Http.Plan} + +# LAW (RFC 9112 §6.3 item 3): chunked Transfer-Encoding with a +# Content-Length is torn +law plan_conflict: + for n: U32 + for +ls: List<&2, String> + for e1: {True{} == Http.clen.agree(Http.head.values(ls, "content-length")) + : Bool} + for e2: {True{} == String.contains(String.to_lower(Http.head.go(ls, + "transfer-encoding")), "chunked") : Bool} + for e3: {True{} == Http.clen.any(Http.head.values(ls, "content-length")) + : Bool} + {Http.head.plan(Some{n}, ls) + == Http.Unplanned{"Content-Length and Transfer-Encoding conflict"} + : Http.Plan} + +# LAW (RFC 9112 §6.3 item 4): chunked Transfer-Encoding alone frames the +# body by its chunks +law plan_chunked: + for n: U32 + for +ls: List<&2, String> + for e1: {True{} == Http.clen.agree(Http.head.values(ls, "content-length")) + : Bool} + for e2: {True{} == String.contains(String.to_lower(Http.head.go(ls, + "transfer-encoding")), "chunked") : Bool} + for e3: {False{} == Http.clen.any(Http.head.values(ls, "content-length")) + : Bool} + {Http.head.plan(Some{n}, ls) + == Http.Planned{n, Http.head.list(ls), Http.Chunked{}} : Http.Plan} + +# LAW (RFC 9112 §6.3 item 6): otherwise a Content-Length that reads as a +# number sizes the body +law plan_sized: + for n: U32 + for +ls: List<&2, String> + for e1: {True{} == Http.clen.agree(Http.head.values(ls, "content-length")) + : Bool} + for e2: {False{} == String.contains(String.to_lower(Http.head.go(ls, + "transfer-encoding")), "chunked") : Bool} + for k: Nat + for e3: {Some{k} == Http.digits.read(Http.first(Http.head.values(ls, + "content-length"))) : Maybe<&2, Nat>} + {Http.head.plan(Some{n}, ls) + == Http.Planned{n, Http.head.list(ls), Http.Sized{k}} : Http.Plan} + +# LAW (RFC 9112 §6.3 item 8): otherwise the body runs to the close +law plan_closed: + for n: U32 + for +ls: List<&2, String> + for e1: {True{} == Http.clen.agree(Http.head.values(ls, "content-length")) + : Bool} + for e2: {False{} == String.contains(String.to_lower(Http.head.go(ls, + "transfer-encoding")), "chunked") : Bool} + for e3: {None{} == Http.digits.read(Http.first(Http.head.values(ls, + "content-length"))) : Maybe<&2, Nat>} + {Http.head.plan(Some{n}, ls) + == Http.Planned{n, Http.head.list(ls), Http.Closed{}} : Http.Plan} + +# LAW: parse_response reads a head and its body as the head's plan says +law plan_status: + for m: Maybe<&2, U32> + for +ls: List<&2, String> + for +body: String + {Http.parse.status(m, ls, body) == Http.plan.text(Http.head.plan(m, ls), body) + : Http.Reply} + +# LAW (RFC 9110 §6.4.1, §9.3.2): a final head is read as its plan says, with +# no body for HEAD or a 1xx, 204 or 304 status +law plan_final: + for f: Nat + for +head: Bool + for +before: String + for +after: String + for e: {False{} == Http.interim.skip(Http.status.of(String.lines(before)), + after) : Bool} + {Http.parse.to.go(1n+f, head, Http.Cut{before, after}) + == Http.plan.text(Http.head.final(head, String.lines(before)), after) + : Http.Reply} + +# LAW (RFC 9112 §7.1): the chunked decoder's step is decided by the chunk-size +# line alone, then by the octets after it +law size_next: + for +before: String + for +after: String + {Http.chunks.next(before, after) + == Http.chunk.next(Http.chunk.size(before), after) : Http.Next} diff --git a/laws/HeadReplyDropsBody.bend b/laws/HeadReplyDropsBody.bend new file mode 100644 index 0000000..344266d --- /dev/null +++ b/laws/HeadReplyDropsBody.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.3.2): a HEAD response is written with an empty body +law head_reply_drops_body: + {Http.reply.for("HEAD", Http.Reply{200, [], "secret"}) + == Http.Reply{200, [], ""} : Http.Reply} diff --git a/laws/HeadRequestLine.bend b/laws/HeadRequestLine.bend new file mode 100644 index 0000000..6fa1e90 --- /dev/null +++ b/laws/HeadRequestLine.bend @@ -0,0 +1,10 @@ +import Base +import ../src/http.bend as Http + +# LAW: HEAD request-line opens with HEAD and the path (RFC 9110 §9.3.2) +law head_req_names_path: + for +host: String + for +path: String + for hs: List<&2, Http.Header> + {String.starts_with(Http.head_req(host, path, hs), "HEAD " ++ path) + == True{} : Bool} diff --git a/laws/HtmlEscapeCharByChar.bend b/laws/HtmlEscapeCharByChar.bend new file mode 100644 index 0000000..a0ace2f --- /dev/null +++ b/laws/HtmlEscapeCharByChar.bend @@ -0,0 +1,28 @@ +# `html_escape(s)` (`src/html.bend`) escapes text for HTML the way +# markupsafe's `escape` and Jinja's autoescape do. +# +# For every string, the escape writes its characters in order, each one +# independently of the others, and nothing else. +import Base +import ../src/html.bend as Html + +# --- HTML escaping --- + +# LAW: escaping a string is escaping it in front of nothing +law html_escape_onto: + for s: String + {Html.escape(s) == Html.escape.onto(s, "") : String} + +# LAW: nothing is written for the empty string +law html_nil: + for rest: String + {Html.escape.onto("", rest) == rest : String} + +# LAW: a string is written one character at a time, the first character's +# writing followed by the rest's +law html_cons: + for +c: Char + for t: String + for rest: String + {Html.escape.onto(SCon{c, t}, rest) == Html.one(c, Html.escape.onto(t, rest)) + : String} diff --git a/laws/HtmlEscapeIsSafe.bend b/laws/HtmlEscapeIsSafe.bend new file mode 100644 index 0000000..f529045 --- /dev/null +++ b/laws/HtmlEscapeIsSafe.bend @@ -0,0 +1,90 @@ +# For every string, the output holds no `<`, `>`, `"` or `'`, and every `&` +# in it opens one of `&`, `<`, `>`, `"`, `'`. +import Base +import ../src/html.bend as Html + +# where the laws/HtmlEscapeIsSafe.bend frame is, reading left to right: +# between entities, after `&`, after `&#`, after ``, or owing exactly the +# rest of an entity +type Ent is Data: + Out{} + Amp{} + Hash{} + Three{} + Want{c: Char, rest: String} + +# between entities: anything but `<`, `>`, `"` and `'` +def ent.out.ok(lt: Bool, gt: Bool, dq: Bool, sq: Bool) -> Bool: + Bool.not(Bool.or(lt, Bool.or(gt, Bool.or(dq, sq)))) + +# between entities, an `&` opens one +def ent.out.next(amp: Bool) -> Ent: + Bool.pick(Ent, amp, Amp{}, Out{}) + +# what is owed after the rest of an entity's next character +def ent.want(rest: String) -> Ent: + match rest: + case SNil{}: + Out{} + case SCon{c, t}: + Want{c, t} + +# whether character c may come next +def ent.ok(+c: Char, st: Ent) -> Bool: + match st: + case Out{}: + ent.out.ok(Char.is_eq(c, '<'), Char.is_eq(c, '>'), Char.is_eq(c, '"'), + Char.is_eq(c, '\'')) + case Amp{}: + Bool.or(Char.is_eq(c, 'a'), Bool.or(Char.is_eq(c, 'l'), + Bool.or(Char.is_eq(c, 'g'), Char.is_eq(c, '#')))) + case Hash{}: + Char.is_eq(c, '3') + case Three{}: + Bool.or(Char.is_eq(c, '4'), Char.is_eq(c, '9')) + case Want{w, t}: + Char.is_eq(c, w) + +# where the frame is after character c +def ent.next(+c: Char, st: Ent) -> Ent: + match st: + case Out{}: + ent.out.next(Char.is_eq(c, '&')) + case Amp{}: + Bool.pick(Ent, Char.is_eq(c, 'a'), Want{'m', "p;"}, + Bool.pick(Ent, Char.is_eq(c, '#'), Hash{}, Want{'t', ";"})) + case Hash{}: + Three{} + case Three{}: + Want{';', ""} + case Want{w, t}: + ent.want(t) + +# nothing is owed at the end +def ent.end(st: Ent) -> Bool: + match st: + case Out{}: + True{} + case Amp{}: + False{} + case Hash{}: + False{} + case Three{}: + False{} + case Want{w, t}: + False{} + +# the laws/HtmlEscapeIsSafe.bend frame: no `<`, `>`, `"` or `'`, and every +# `&` opens one of `&`, `<`, `>`, `"`, `'` +def html.framed(str: String, +st: Ent) -> Bool: + match str: + case SNil{}: + ent.end(st) + case SCon{+c, tail}: + Bool.and(ent.ok(c, st), html.framed(tail, ent.next(c, st))) + +# LAW (frame): every escaped string holds no `<`, `>`, `"` or `'`, and each +# `&` in it opens one of the five entities +law html_frame: + for s: String + {html.framed(Html.escape(s), Out{}) == True{} : Bool} diff --git a/laws/HtmlEscapesFiveChars.bend b/laws/HtmlEscapesFiveChars.bend new file mode 100644 index 0000000..aabf0f3 --- /dev/null +++ b/laws/HtmlEscapesFiveChars.bend @@ -0,0 +1,29 @@ +# `&` is written as `&`, `<` as `<`, `>` as `>`, `"` as `"` +# and `'` as `'`. +import Base +import ../src/html.bend as Html + +# LAW: `&` is written as `&` +law html_amp: + for rest: String + {Html.one('&', rest) == "&" ++ rest : String} + +# LAW: `<` is written as `<` +law html_lt: + for rest: String + {Html.one('<', rest) == "<" ++ rest : String} + +# LAW: `>` is written as `>` +law html_gt: + for rest: String + {Html.one('>', rest) == ">" ++ rest : String} + +# LAW: `"` is written as `"` +law html_dq: + for rest: String + {Html.one('"', rest) == """ ++ rest : String} + +# LAW: `'` is written as `'` +law html_sq: + for rest: String + {Html.one('\'', rest) == "'" ++ rest : String} diff --git a/laws/HtmlKeepsOtherChars.bend b/laws/HtmlKeepsOtherChars.bend new file mode 100644 index 0000000..089147c --- /dev/null +++ b/laws/HtmlKeepsOtherChars.bend @@ -0,0 +1,14 @@ +# Every other character is written as itself. +import Base +import ../src/html.bend as Html + +# LAW: every other character is written as itself +law html_other: + for +c: Char + for e1: {False{} == Char.is_eq(c, '&') : Bool} + for e2: {False{} == Char.is_eq(c, '<') : Bool} + for e3: {False{} == Char.is_eq(c, '>') : Bool} + for e4: {False{} == Char.is_eq(c, '"') : Bool} + for e5: {False{} == Char.is_eq(c, '\'') : Bool} + for rest: String + {Html.one(c, rest) == SCon{c, rest} : String} diff --git a/laws/HttpsDefaultsTo443.bend b/laws/HttpsDefaultsTo443.bend new file mode 100644 index 0000000..d0d46b1 --- /dev/null +++ b/laws/HttpsDefaultsTo443.bend @@ -0,0 +1,6 @@ +import Base +import ../src/url.bend as Url + +# LAW: https implies 443 +law dflt_https: + {Url.dflt("https") == 443 : U32} diff --git a/laws/InterimResponsesSkipped.bend b/laws/InterimResponsesSkipped.bend new file mode 100644 index 0000000..4141027 --- /dev/null +++ b/laws/InterimResponsesSkipped.bend @@ -0,0 +1,142 @@ +# For every request method and every text an exchange answers with, the +# client reads the response with the method in mind: a head whose status is +# 1xx (RFC 9110 §15.2) with more text after it is an interim response and is +# skipped, the text after it read in its place, at most as many heads deep +# as the text has characters; the head it stops at is the final response. +# When the method is exactly `HEAD`, or the final status is 1xx, 204 or 304 +# (RFC 9110 §6.4.1, §9.3.2; RFC 9112 §6.3 item 1), the response is that +# status, its header fields and an empty body, whatever its Content-Length +# or Transfer-Encoding say; otherwise it is exactly what `parse_response` +# reads from that head and the text after it. A text whose head never ends +# is torn with `the response head has no blank line after it`, and a final +# head with no status code with `the response has no status line`. A failed +# exchange keeps its reason as laws/TimeoutsReadAsTimedOut.bend reports it. +# +# also rests on: laws/InterimStatusIs1xx.bend +import Base +import ../src/http.bend as Http +import ../src/wirespec.bend as Spec + +# LAW: when the head did end, parse uses everything before/after the blank line +law parse_splits_at_blank_line: + for +raw: String + for before: String + for after: String + for e: {Http.Cut{before, after} == Http.divide(raw, "\r\n\r\n") : Http.Cut} + {Http.parse(raw) == Http.parse.head(String.lines(before), after) + : Http.Reply} + +# --- the response, read by the request's method --- + +# LAW: a response is read head by head for the request's method, at most as +# many heads as the text has characters +law reply_to: + for +method: String + for +raw: String + {Http.parse.to(method, raw) + == Http.parse.to.go(String.length(raw), String.eq(method, "HEAD"), + Http.divide(raw, "\r\n\r\n")) : Http.Reply} + +# LAW: a read with no heads left found no final head +law reply_fuel: + for head: Bool + for c: Http.Cut + {Http.parse.to.go(0n, head, c) + == Http.Torn{"the response head has no blank line after it"} + : Http.Reply} + +# LAW (RFC 9112 §2.1): a head that never ends is torn +law reply_open: + for f: Nat + for head: Bool + {Http.parse.to.go(1n+f, head, Http.NoCut{}) + == Http.Torn{"the response head has no blank line after it"} + : Http.Reply} + +# LAW (RFC 9110 §15.2): an interim head with text after it is skipped, and the +# text after it is read in its place +law reply_interim: + for f: Nat + for +head: Bool + for +before: String + for +after: String + for e: {True{} == Http.interim.skip(Http.status.of(String.lines(before)), + after) : Bool} + {Http.parse.to.go(1n+f, head, Http.Cut{before, after}) + == Http.parse.to.go(f, head, Http.divide(after, "\r\n\r\n")) : Http.Reply} + +# LAW (RFC 9110 §6.4.1, §9.3.2; RFC 9112 §6.3 item 1): a final response to +# HEAD, or with a 1xx, 204 or 304 status, has its status, its fields and no +# body, whatever its framing fields say +law reply_bare: + for f: Nat + for +head: Bool + for +before: String + for +after: String + for e1: {False{} == Http.interim.skip(Http.status.of(String.lines(before)), + after) : Bool} + for e2: {True{} == Bool.or(head, + Http.status.empty(Http.status.of(String.lines(before)))) : Bool} + for n: U32 + for e3: {Some{n} == Http.status.of(String.lines(before)) : Maybe<&2, U32>} + {Http.parse.to.go(1n+f, head, Http.Cut{before, after}) + == Http.Reply{n, Http.head.list(String.lines(before)), ""} : Http.Reply} + +# LAW (RFC 9112 §4): a final head with no status code is torn +law reply_bare_none: + for f: Nat + for +head: Bool + for +before: String + for +after: String + for e1: {False{} == Http.interim.skip(Http.status.of(String.lines(before)), + after) : Bool} + for e2: {True{} == Bool.or(head, + Http.status.empty(Http.status.of(String.lines(before)))) : Bool} + for e3: {None{} == Http.status.of(String.lines(before)) : Maybe<&2, U32>} + {Http.parse.to.go(1n+f, head, Http.Cut{before, after}) + == Http.Torn{"the response has no status line"} : Http.Reply} + +# LAW: any other final response is read as parse_response reads that head and +# the text after it +law reply_framed: + for f: Nat + for +head: Bool + for +before: String + for +after: String + for e1: {False{} == Http.interim.skip(Http.status.of(String.lines(before)), + after) : Bool} + for e2: {False{} == Bool.or(head, + Http.status.empty(Http.status.of(String.lines(before)))) : Bool} + {Http.parse.to.go(1n+f, head, Http.Cut{before, after}) + == Http.parse.head(String.lines(before), after) : Http.Reply} + +# LAW (RFC 9110 §15.2): a head is skipped exactly when it is interim and text +# follows it +law interim_skip: + for m: Maybe<&2, U32> + for after: String + {Http.interim.skip(m, after) + == Bool.and(Http.status.interim(m), Bool.not(String.is_empty(after))) + : Bool} + +# LAW (RFC 9110 §6.4.1): a 1xx, 204 or 304 status has no body +law status_empty: + for +m: Maybe<&2, U32> + {Http.status.empty(m) + == Bool.or(Http.status.interim(m), Http.status.bare(m)) : Bool} + +# LAW: a successful exchange's text is read by the request's method +law heard_ok: + for +method: String + for +text: String + {Spec.wire.heard(method, "0\n" ++ text) == Http.parse.to(method, text) + : Http.Reply} + +# LAW: a failed exchange is torn with the reason it is reported with +law heard_fail: + for +method: String + for +raw: String + for e: {False{} == Spec.wire.ok(raw) : Bool} + {Spec.wire.heard(method, raw) + == Http.Torn{Spec.wire.why(Spec.wire.code(raw), Spec.wire.text(raw))} + : Http.Reply} diff --git a/laws/InterimStatusIs1xx.bend b/laws/InterimStatusIs1xx.bend new file mode 100644 index 0000000..37586d1 --- /dev/null +++ b/laws/InterimStatusIs1xx.bend @@ -0,0 +1,16 @@ +# A response's status is interim (RFC 9110 §15.2) exactly when it is 100 to +# 199; a status line that does not parse is not interim. +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §15.2): a status is interim exactly when it is 100 to 199 +law status_interim_some: + for +n: U32 + {Http.status.interim(Some{n}) + == Bool.and(U32.is_le(100, n), U32.is_lt(n, 200)) : Bool} + +# LAW: a status line that does not parse is not interim +law status_interim_none: + for m: Maybe<&2, U32> + for e: {None{} == m : Maybe<&2, U32>} + {Http.status.interim(m) == False{} : Bool} diff --git a/laws/JoinedPiecesAreText.bend b/laws/JoinedPiecesAreText.bend new file mode 100644 index 0000000..b556987 --- /dev/null +++ b/laws/JoinedPiecesAreText.bend @@ -0,0 +1,8 @@ +import Base +import ../src/wirespec.bend as Spec + +# LAW: the pieces put together in one pass are the text they stand for (the +# server puts a request's pieces together with it) +law join_joined: + for +ps: List<&2, String> + {Spec.wire.join(ps) == Spec.wire.joined(ps) : String} diff --git a/laws/JsonRejectsNonJson.bend b/laws/JsonRejectsNonJson.bend new file mode 100644 index 0000000..006ad7b --- /dev/null +++ b/laws/JsonRejectsNonJson.bend @@ -0,0 +1,6 @@ +import Base +import ../src/json.bend as Json + +# LAW: text that is not JSON is none +law json_parse_rejects: + {Json.json.as_str(Json.json.parse("nope")) == None{} : Maybe<&2, String>} diff --git a/laws/JsonStringIsTextBody.bend b/laws/JsonStringIsTextBody.bend new file mode 100644 index 0000000..f2594b3 --- /dev/null +++ b/laws/JsonStringIsTextBody.bend @@ -0,0 +1,9 @@ +import Base +import ../src/body.bend as Body +import ../src/json.bend as Json +import 0xa81e161008ecc2f297c0a3065cb9b188/main.bend as Ezjson + +# LAW: a JSON string is a text body of its compact encoding. The HTTP types +# stay text (the body is not a JSON value). +law json_string_is_text_body: + {Json.json.body(Ezjson.str("hi")) == Body.body.text("\"hi\"") : Body.Body} diff --git a/laws/JsonStringParsesBack.bend b/laws/JsonStringParsesBack.bend new file mode 100644 index 0000000..bce40ed --- /dev/null +++ b/laws/JsonStringParsesBack.bend @@ -0,0 +1,6 @@ +import Base +import ../src/json.bend as Json + +# LAW: a JSON string parses back to that string +law json_parse_string: + {Json.json.as_str(Json.json.parse("\"hi\"")) == Some{"hi"} : Maybe<&2, String>} diff --git a/laws/LocationResolvedBySort.bend b/laws/LocationResolvedBySort.bend new file mode 100644 index 0000000..5468d0a --- /dev/null +++ b/laws/LocationResolvedBySort.bend @@ -0,0 +1,22 @@ +# Against a URL that parses, a Location that is not an absolute `http://` +# or `https://` URL is resolved by its sort against the URL's scheme, host, +# port and path (RFC 3986 §5.2.2). +import Base +import ../src/url.bend as Url +import ../src/redirect.bend as Redirect + +# LAW (RFC 3986 §5.2.2): against a URL that parses, a Location that is not an +# absolute URL is resolved by its sort against the URL's parts +law target_against: + for url: String + for +loc: String + for e1: {False{} == Bool.or(String.starts_with(loc, "http://"), + String.starts_with(loc, "https://")) : Bool} + for scheme: String + for host: String + for port: U32 + for path: String + for e2: {Url.Loc{scheme, host, port, path} == Url.parse(url) : Url.Loc} + {Redirect.redirect.target(url, loc) + == Redirect.redirect.against(scheme, host, port, path, loc, + Redirect.redirect.sort(loc)) : Maybe<&2, String>} diff --git a/laws/LocationSplitsAtQuery.bend b/laws/LocationSplitsAtQuery.bend new file mode 100644 index 0000000..e246228 --- /dev/null +++ b/laws/LocationSplitsAtQuery.bend @@ -0,0 +1,23 @@ +# A Location's fragment is dropped, and what is left is cut at its first +# `?` into its path and its query (RFC 3986 §3.4, §3.5); with no `?` it is +# all path and no query. +import Base +import ../src/http.bend as Http +import ../src/url.bend as Url +import ../src/redirect.bend as Redirect + +# LAW (RFC 3986 §3.4, §3.5): a Location with a `?` is its path before it and +# its query from it, its fragment dropped +law split_query: + for +loc: String + for p: String + for q: String + for e: {Http.Cut{p, q} == Http.divide(Url.frag.drop(loc), "?") : Http.Cut} + {Redirect.redirect.split(loc) == Redirect.Ref{p, "?" ++ q} : Redirect.Ref} + +# LAW: a Location with no `?` is all path, its fragment dropped +law split_plain: + for +loc: String + for e: {Http.NoCut{} == Http.divide(Url.frag.drop(loc), "?") : Http.Cut} + {Redirect.redirect.split(loc) == Redirect.Ref{Url.frag.drop(loc), ""} + : Redirect.Ref} diff --git a/laws/LowercaseGetNotSafe.bend b/laws/LowercaseGetNotSafe.bend new file mode 100644 index 0000000..186a354 --- /dev/null +++ b/laws/LowercaseGetNotSafe.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW: a lower-case get is not the safe method GET +law method_safe_get_lower: + {Http.method.safe("get") == False{} : Bool} diff --git a/laws/MaxAgeOneFreshThenStale.bend b/laws/MaxAgeOneFreshThenStale.bend new file mode 100644 index 0000000..ba2ae6a --- /dev/null +++ b/laws/MaxAgeOneFreshThenStale.bend @@ -0,0 +1,8 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §4.2.1): max-age=1 is fresh at 0 and stale at 1 +law fresh_max_age_one: + {Bool.and(Cache.cache.fresh(Cache.cache.parse("max-age=1"), 0n), + Bool.not(Cache.cache.fresh(Cache.cache.parse("max-age=1"), 1n))) + == True{} : Bool} diff --git a/laws/MaxAgeOverridesExpires.bend b/laws/MaxAgeOverridesExpires.bend new file mode 100644 index 0000000..ca60ced --- /dev/null +++ b/laws/MaxAgeOverridesExpires.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §4.2.1): max-age overrides Expires for freshness +law fresh_max_age_overrides_expires: + {Cache.cache.fresh(Cache.cache.note("max-age=0", + "Wed, 21 Oct 2099 07:28:00 GMT"), 0n) == False{} : Bool} diff --git a/laws/MaxAgeZeroIsStale.bend b/laws/MaxAgeZeroIsStale.bend new file mode 100644 index 0000000..d1313e8 --- /dev/null +++ b/laws/MaxAgeZeroIsStale.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §4.2.1): max-age=0 is stale at age 0 +law fresh_max_age_zero: + {Cache.cache.fresh(Cache.cache.parse("max-age=0"), 0n) == False{} : Bool} diff --git a/laws/MethodTokensAreCaseSensitive.bend b/laws/MethodTokensAreCaseSensitive.bend new file mode 100644 index 0000000..e452fd6 --- /dev/null +++ b/laws/MethodTokensAreCaseSensitive.bend @@ -0,0 +1,5 @@ +import Base + +# LAW (RFC 9110 §9.3): method tokens are case-sensitive +law method_tokens_are_case_sensitive: + {String.eq("GET", "get") == False{} : Bool} diff --git a/laws/MissingFieldIsEmpty.bend b/laws/MissingFieldIsEmpty.bend new file mode 100644 index 0000000..52ba907 --- /dev/null +++ b/laws/MissingFieldIsEmpty.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §5.1): a missing field looks up as empty +law headers_find_missing: + {Http.headers.find([], "host") == "" : String} diff --git a/laws/NamedReferencesLongestMatch.bend b/laws/NamedReferencesLongestMatch.bend new file mode 100644 index 0000000..51b4542 --- /dev/null +++ b/laws/NamedReferencesLongestMatch.bend @@ -0,0 +1,116 @@ +# A named reference's text `s` (with its `;` when it has one) is replaced by +# the characters of the longest name in the HTML Standard's named character +# references table (§13.5, trusted in src/entities.bend) that `s` starts +# with, and the rest of `s` after that name is written as it is; so `&` +# is `&`, a legacy name such as `&` or `¬` is read without its `;` and +# `¬it;` is `¬it;`. With no such name, `&` and `s` are written as they +# are. Rows are compared in table order and a later row replaces the best +# only when it is longer. +import Base +import ../src/unescape.bend as Ref +import ../src/entities.bend as Ents + +# LAW (§13.2.5.73): a name is read against the whole table, with no best yet +law ref_named: + for +s: String + {Ref.named(s) == Ref.named.of(Ref.best(Ents.table(), s, None{}), s) + : String} + +# LAW: with no name the text starts with, `&` and the text are written as +# they are +law ref_named_none: + for s: String + {Ref.named.of(None{}, s) == SCon{'&', s} : String} + +# LAW: with one, its characters, then the text after it as written +law ref_named_some: + for +name: String + for first: U32 + for second: U32 + for s: String + {Ref.named.of(Some{Ents.Ref{name, first, second}}, s) + == Ref.chars(first, second) ++ String.drop(s, String.length(name)) + : String} + +# LAW: after the last row, the best so far +law ref_best_nil: + for s: String + for sofar: Maybe<&2, Ents.Ref> + {Ref.best([], s, sofar) == sofar : Maybe<&2, Ents.Ref>} + +# LAW: the rows are read in order, each against the best so far +law ref_best_cons: + for r: Ents.Ref + for t: List<&2, Ents.Ref> + for +s: String + for +sofar: Maybe<&2, Ents.Ref> + {Ref.best(r <> t, s, sofar) == Ref.best(t, s, Ref.best.step(sofar, r, s)) + : Maybe<&2, Ents.Ref>} + +# LAW: a row is a candidate when the text starts with its name +law ref_step: + for +sofar: Maybe<&2, Ents.Ref> + for +name: String + for first: U32 + for second: U32 + for +s: String + {Ref.best.step(sofar, Ents.Ref{name, first, second}, s) + == Ref.best.hit(sofar, Ents.Ref{name, first, second}, name, + String.starts_with(s, name)) : Maybe<&2, Ents.Ref>} + +# LAW: a row whose name the text does not start with keeps the best +law ref_hit_miss: + for sofar: Maybe<&2, Ents.Ref> + for r: Ents.Ref + for name: String + {Ref.best.hit(sofar, r, name, False{}) == sofar : Maybe<&2, Ents.Ref>} + +# LAW: a candidate is compared by length with the best +law ref_hit_take: + for +sofar: Maybe<&2, Ents.Ref> + for +r: Ents.Ref + for +name: String + {Ref.best.hit(sofar, r, name, True{}) + == Ref.best.take(sofar, r, Nat.is_lt(Ref.best.len(sofar), + String.length(name))) : Maybe<&2, Ents.Ref>} + +# LAW: a longer candidate is the new best +law ref_take_longer: + for sofar: Maybe<&2, Ents.Ref> + for r: Ents.Ref + {Ref.best.take(sofar, r, True{}) == Some{r} : Maybe<&2, Ents.Ref>} + +# LAW: any other keeps the best +law ref_take_keep: + for sofar: Maybe<&2, Ents.Ref> + for r: Ents.Ref + {Ref.best.take(sofar, r, False{}) == sofar : Maybe<&2, Ents.Ref>} + +# LAW: no best yet is length 0 +law ref_len_none: + for m: Maybe<&2, Ents.Ref> + for e: {None{} == m : Maybe<&2, Ents.Ref>} + {Ref.best.len(m) == 0n : Nat} + +# LAW: a best is its name's length +law ref_len_some: + for +name: String + for first: U32 + for second: U32 + {Ref.best.len(Some{Ents.Ref{name, first, second}}) == String.length(name) + : Nat} + +# LAW: a row with one code point stands for that character +law ref_chars_one: + for first: U32 + for +second: U32 + for e: {True{} == U32.is_eq(second, 0) : Bool} + {Ref.chars(first, second) == SCon{Chr{first}, ""} : String} + +# LAW: a row with two stands for both, in order +law ref_chars_two: + for first: U32 + for +second: U32 + for e: {False{} == U32.is_eq(second, 0) : Bool} + {Ref.chars(first, second) == SCon{Chr{first}, SCon{Chr{second}, ""}} + : String} diff --git a/laws/NoCacheNeverFresh.bend b/laws/NoCacheNeverFresh.bend new file mode 100644 index 0000000..dec3963 --- /dev/null +++ b/laws/NoCacheNeverFresh.bend @@ -0,0 +1,14 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §5.2.2.4): whatever else they say, directives with no-cache +# are never fresh +law fresh_no_cache_any: + for no_store: Bool + for public: Bool + for private: Bool + for max_age: String + for expires: String + for age: Nat + {Cache.cache.fresh(Cache.Cc{no_store, True{}, public, private, max_age, + expires}, age) == False{} : Bool} diff --git a/laws/NoCacheNotFresh.bend b/laws/NoCacheNotFresh.bend new file mode 100644 index 0000000..6f19e3e --- /dev/null +++ b/laws/NoCacheNotFresh.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §5.2.2.2): no-cache is not fresh +law fresh_no_cache: + {Cache.cache.fresh(Cache.cache.parse("no-cache, max-age=10"), 0n) + == False{} : Bool} diff --git a/laws/NoMaxAgeNotFresh.bend b/laws/NoMaxAgeNotFresh.bend new file mode 100644 index 0000000..1373b58 --- /dev/null +++ b/laws/NoMaxAgeNotFresh.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §4.2.1): without max-age the response is not treated as fresh +law fresh_public_only: + {Cache.cache.fresh(Cache.cache.parse("public"), 0n) == False{} : Bool} diff --git a/laws/NoStoreNeverFresh.bend b/laws/NoStoreNeverFresh.bend new file mode 100644 index 0000000..c607e38 --- /dev/null +++ b/laws/NoStoreNeverFresh.bend @@ -0,0 +1,14 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §5.2.2.5): whatever else they say, directives with no-store +# are never fresh +law fresh_no_store_any: + for no_cache: Bool + for public: Bool + for private: Bool + for max_age: String + for expires: String + for age: Nat + {Cache.cache.fresh(Cache.Cc{True{}, no_cache, public, private, max_age, + expires}, age) == False{} : Bool} diff --git a/laws/NoStoreNotFresh.bend b/laws/NoStoreNotFresh.bend new file mode 100644 index 0000000..f1e1b4c --- /dev/null +++ b/laws/NoStoreNotFresh.bend @@ -0,0 +1,7 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §5.2.2.3): no-store is not fresh +law fresh_no_store: + {Cache.cache.fresh(Cache.cache.parse("no-store, max-age=10"), 0n) + == False{} : Bool} diff --git a/laws/NonHeadReplyKeepsBody.bend b/laws/NonHeadReplyKeepsBody.bend new file mode 100644 index 0000000..454b2eb --- /dev/null +++ b/laws/NonHeadReplyKeepsBody.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW: any other method keeps the response body +law get_reply_keeps_body: + {Http.reply.for("GET", Http.Reply{200, [], "secret"}) + == Http.Reply{200, [], "secret"} : Http.Reply} diff --git a/laws/NonHexChunkSizeTorn.bend b/laws/NonHexChunkSizeTorn.bend new file mode 100644 index 0000000..c03984f --- /dev/null +++ b/laws/NonHexChunkSizeTorn.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): a non-hexadecimal chunk size is torn +law parse_rejects_non_hex_chunk: + {Http.parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nZ\r\n\r\n") + == Http.Torn{"chunk size is not hexadecimal"} : Http.Reply} diff --git a/laws/NumericReferencesDecode.bend b/laws/NumericReferencesDecode.bend new file mode 100644 index 0000000..b5e5501 --- /dev/null +++ b/laws/NumericReferencesDecode.bend @@ -0,0 +1,86 @@ +# A numeric reference's number `n` stands for (§13.2.5.80, Python's +# `_replace_charref`): U+FFFD for 0; U+000D for 0x0D; for 0x80 to 0x9F the +# character the §13.2.5.80 table names (Windows-1252: 0x80 U+20AC, 0x82 +# U+201A, 0x83 U+0192, 0x84 U+201E, 0x85 U+2026, 0x86 U+2020, 0x87 U+2021, +# 0x88 U+02C6, 0x89 U+2030, 0x8A U+0160, 0x8B U+2039, 0x8C U+0152, 0x8E +# U+017D, 0x91 U+2018, 0x92 U+2019, 0x93 U+201C, 0x94 U+201D, 0x95 U+2022, +# 0x96 U+2013, 0x97 U+2014, 0x98 U+02DC, 0x99 U+2122, 0x9A U+0161, 0x9B +# U+203A, 0x9C U+0153, 0x9E U+017E, 0x9F U+0178; 0x81, 0x8D, 0x8F, 0x90 and +# 0x9D themselves); U+FFFD for a surrogate (0xD800 to 0xDFFF) or a number +# past 0x10FFFF; nothing (Python drops what the standard calls a control or +# noncharacter reference error) for 0x01 to 0x08, 0x0B, 0x0E to 0x1F, 0x7F +# to 0x9F, 0xFDD0 to 0xFDEF and every number whose low 16 bits are 0xFFFE or +# 0xFFFF; and the character `n` for every other number. +import Base +import ../src/unescape.bend as Ref + +# LAW (§13.2.5.80): a number is read from 0 on +law ref_code: + for +n: U32 + {Ref.code(n) == Ref.code.zero(n, U32.is_eq(n, 0)) : String} + +# LAW (§13.2.5.80): 0 is U+FFFD +law ref_code_zero: + for +n: U32 + for e: {True{} == U32.is_eq(n, 0) : Bool} + {Ref.code(n) == SCon{Chr{65533}, ""} : String} + +# LAW (§13.2.5.80): 0x0D is a carriage return +law ref_code_cr: + for +n: U32 + for e1: {False{} == U32.is_eq(n, 0) : Bool} + for e2: {True{} == U32.is_eq(n, 13) : Bool} + {Ref.code(n) == "\r" : String} + +# LAW (§13.2.5.80): 0x80 to 0x9F are the table's +law ref_code_c1: + for +n: U32 + for e1: {False{} == U32.is_eq(n, 0) : Bool} + for e2: {False{} == U32.is_eq(n, 13) : Bool} + for e3: {True{} == Bool.and(U32.is_le(128, n), U32.is_le(n, 159)) : Bool} + {Ref.code(n) + == SCon{Chr{Ref.nth(Ref.w1252(), U32.to_nat(U32.sub(n, 128)))}, ""} + : String} + +# LAW (§13.2.5.80): a surrogate or a number past U+10FFFF is U+FFFD +law ref_code_bad: + for +n: U32 + for e1: {False{} == U32.is_eq(n, 0) : Bool} + for e2: {False{} == U32.is_eq(n, 13) : Bool} + for e3: {False{} == Bool.and(U32.is_le(128, n), U32.is_le(n, 159)) : Bool} + for e4: {True{} == Bool.or(Bool.and(U32.is_le(55296, n), + U32.is_le(n, 57343)), U32.is_lt(1114111, n)) : Bool} + {Ref.code(n) == SCon{Chr{65533}, ""} : String} + +# LAW: a control or noncharacter Python drops is nothing +law ref_code_dropped: + for +n: U32 + for e1: {False{} == U32.is_eq(n, 0) : Bool} + for e2: {False{} == U32.is_eq(n, 13) : Bool} + for e3: {False{} == Bool.and(U32.is_le(128, n), U32.is_le(n, 159)) : Bool} + for e4: {False{} == Bool.or(Bool.and(U32.is_le(55296, n), + U32.is_le(n, 57343)), U32.is_lt(1114111, n)) : Bool} + for e5: {True{} == Ref.dropped(n) : Bool} + {Ref.code(n) == "" : String} + +# LAW: every other number is its character +law ref_code_kept: + for +n: U32 + for e1: {False{} == U32.is_eq(n, 0) : Bool} + for e2: {False{} == U32.is_eq(n, 13) : Bool} + for e3: {False{} == Bool.and(U32.is_le(128, n), U32.is_le(n, 159)) : Bool} + for e4: {False{} == Bool.or(Bool.and(U32.is_le(55296, n), + U32.is_le(n, 57343)), U32.is_lt(1114111, n)) : Bool} + for e5: {False{} == Ref.dropped(n) : Bool} + {Ref.code(n) == SCon{Chr{n}, ""} : String} + +# LAW: what Python drops: 0x01 to 0x08, 0x0B, 0x0E to 0x1F, 0x7F to 0x9F, +# 0xFDD0 to 0xFDEF, and low 16 bits 0xFFFE or 0xFFFF +law ref_dropped: + for +n: U32 + {Ref.dropped(n) == Bool.or(Bool.and(U32.is_le(1, n), U32.is_le(n, 8)), + Bool.or(U32.is_eq(n, 11), + Bool.or(Bool.and(U32.is_le(14, n), U32.is_le(n, 31)), + Bool.or(Bool.and(U32.is_le(127, n), U32.is_le(n, 159)), + Bool.or(Bool.and(U32.is_le(64976, n), U32.is_le(n, 65007)), + U32.is_eq(U32.and(n, 65534), 65534)))))) : Bool} diff --git a/laws/ObsFoldRejected.bend b/laws/ObsFoldRejected.bend new file mode 100644 index 0000000..43338b5 --- /dev/null +++ b/laws/ObsFoldRejected.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §5.2): a field value that still holds obs-fold is rejected +law head_line_rejects_obs_fold: + {Http.head.line("A: b\r\n c") == None{} : Maybe<&2, Http.Header>} diff --git a/laws/OctetBodiesSentUnchanged.bend b/laws/OctetBodiesSentUnchanged.bend new file mode 100644 index 0000000..04e4444 --- /dev/null +++ b/laws/OctetBodiesSentUnchanged.bend @@ -0,0 +1,137 @@ +# A body is split into its shape and its octets (`body.shape`): a text body +# is its text and no octets, the empty body is the empty text, and an octets +# body (`Body.Octets`, packed `Bytes`) of `n` octets is `ShapeOctets{n}` and +# those octets. For every shape, method, headers and URL that parses, a +# `ShapeOctets{n}` exchange's request text is the request line, the header +# fields laws/RequestHeaderFieldsOrdered.bend gives for a body of `n` octets +# (so its Content-Length fields are exactly `Content-Length: `, and none +# when `n` is 0), and a blank line; a text exchange is the one +# laws/RequestLineAndHost.bend describes, its header fields the ones +# laws/RequestHeaderFieldsOrdered.bend gives for the text's UTF-8 octets. +# What is sent is the request text as its UTF-8 octets (`wire.payload`, RFC +# 3629 §3, each character as `Pct.utf8` writes it, +# laws/QuoteEscapesUtf8Bytes.bend), then the body's octets unchanged +# (trusted in src/wire.bend). +import Base +import ../src/http.bend as Http +import ../src/url.bend as Url +import ../src/body.bend as Body +import ../src/wirespec.bend as Spec +import ../src/pct.bend as Pct +import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes + +# --- octet bodies on the wire --- + +# LAW: a text body is sent inside the request text, as +# laws/RequestLineAndHost.bend says +law exchange_body_text: + for method: String + for l: Url.Loc + for hs: List<&2, Http.Header> + for text: String + {Spec.wire.exchange.body(method, l, hs, + Pair.fst(Body.Shape, Bytes.Bytes, Body.body.shape(Body.Plain{text}))) + == Spec.wire.exchange(method, l, hs, text) : Spec.Exchange} + +# LAW: an empty body is sent as the empty text body +law exchange_body_empty: + for method: String + for l: Url.Loc + for hs: List<&2, Http.Header> + {Spec.wire.exchange.body(method, l, hs, + Pair.fst(Body.Shape, Bytes.Bytes, Body.body.shape(Body.Empty{}))) + == Spec.wire.exchange(method, l, hs, "") : Spec.Exchange} + +# LAW: an octet body's request text is built for its number of octets +law exchange_body_octets: + for method: String + for l: Url.Loc + for hs: List<&2, Http.Header> + for +n: U32 + for buf: Array + {Spec.wire.exchange.body(method, l, hs, + Pair.fst(Body.Shape, Bytes.Bytes, + Body.body.shape(Body.Octets{Bytes.Bytes{n, buf}}))) + == Spec.wire.exchange.octets(method, l, hs, U32.to_nat(n)) + : Spec.Exchange} + +# LAW: a URL that does not parse sends nothing and keeps its reason +law exchange_octets_bad: + for method: String + for why: String + for hs: List<&2, Http.Header> + for n: Nat + {Spec.wire.exchange.octets(method, Url.Bad{why}, hs, n) == Spec.NoUrl{why} + : Spec.Exchange} + +# LAW: a URL that parses is sent to its host and port, over TLS exactly when +# its scheme is https, with the method it was given and the request text +# request.head.at writes for n octets +law exchange_octets_loc: + for +method: String + for +scheme: String + for +host: String + for +port: U32 + for path: String + for hs: List<&2, Http.Header> + for n: Nat + {Spec.wire.exchange.octets(method, Url.Loc{scheme, host, port, path}, hs, n) + == Spec.Exchange{String.eq(scheme, "https"), host, port, method, + Http.request.head.at(method, String.eq(scheme, "https"), host, port, + path, hs, n)} : Spec.Exchange} + +# LAW (RFC 9112 §3): the request text ahead of n octets is the request line, +# the header fields for n octets, and the blank line, nothing after it +law request_head_at: + for +method: String + for secure: Bool + for host: String + for port: U32 + for +path: String + for hs: List<&2, Http.Header> + for n: Nat + {Http.request.head.at(method, secure, host, port, path, hs, n) + == method ++ " " ++ path ++ " HTTP/1.1\r\n" + ++ Http.headers.lines( + Http.request.fields.n(Http.authority(secure, host, port), hs, n)) + ++ "\r\n" + : String} + +# LAW (RFC 9110 §8.6): a text body's header fields are the fields for its +# UTF-8 octets +law fields_n: + for host: String + for +hs: List<&2, Http.Header> + for +body: String + {Http.request.fields(host, hs, body) + == Http.request.fields.n(host, hs, Http.utf8.len(body)) + : List<&2, Http.Header>} + +# LAW (RFC 9110 §8.6): whatever the caller gave, the Content-Length fields for +# n octets are exactly `Content-Length: n`, and none for none +law fields_clen_n: + for +host: String + for +hs: List<&2, Http.Header> + for +n: Nat + {Http.fields.of(Http.FClen{}, Http.request.fields.n(host, hs, n)) + == Http.clen.field(n, Nat.is_eq(n, 0n)) : List<&2, Http.Header>} + +# LAW: what is sent ahead of the body's octets is the request text's octets +law payload_is: + for req: String + {Spec.wire.payload(req) == Spec.wire.payload.text(req, "") : String} + +# LAW: an empty request text writes no octets ahead of what follows it +law payload_nil: + for rest: String + {Spec.wire.payload.text("", rest) == rest : String} + +# LAW (RFC 3629 §3): every character of the request text is sent as its +# UTF-8 octets, in order, ahead of the rest +law payload_cons: + for +cp: U32 + for t: String + for rest: String + {Spec.wire.payload.text(SCon{Chr{cp}, t}, rest) + == Body.octets.chars(Pct.utf8(cp)) ++ Spec.wire.payload.text(t, rest) + : String} diff --git a/laws/OctetsDecodeAsUtf8.bend b/laws/OctetsDecodeAsUtf8.bend new file mode 100644 index 0000000..abf4cc0 --- /dev/null +++ b/laws/OctetsDecodeAsUtf8.bend @@ -0,0 +1,44 @@ +# For every octet text `o`, its text (`Qs.octets.text`) is exactly `o`'s +# octets decoded as UTF-8 by `Qs.utf.dec`, one octet a byte +# (laws/Utf8DecodesWithReplacement.bend: an ill-formed sequence's maximal +# subpart is one U+FFFD), and so is each header name and value +# (`Qs.octets.headers`), in order; the client answers a response's header +# fields and reason phrase as text this way, and its body as the packed +# octets themselves (`bytes`), which `body_text` decodes this way only when +# it is called (trusted in src/client.bend and src/packed.bend). +import Base +import ../src/http.bend as Http +import ../src/qs.bend as Qs + +# LAW: received octets as text are their UTF-8 decoding, one octet a byte +law octets_text: + for o: String + {Qs.octets.text(o) == Qs.utf.dec(Qs.octets.toks(o), Qs.Idle{}) : String} + +# LAW: no octets are no pieces to decode +law octets_toks_nil: + for s: String + for e: {SNil{} == s : String} + {Qs.octets.toks(s) == [] : List<&2, Qs.Tok>} + +# LAW: each octet is one byte for the decoder, in order +law octets_toks_cons: + for +c: Char + for t: String + {Qs.octets.toks(SCon{c, t}) == Qs.B{Char.to_u32(c)} <> Qs.octets.toks(t) + : List<&2, Qs.Tok>} + +# LAW: header fields as text are each name and value decoded, in order +law octets_headers_cons: + for name: String + for value: String + for t: List<&2, Http.Header> + {Qs.octets.headers(Http.H{name, value} <> t) + == Http.H{Qs.octets.text(name), Qs.octets.text(value)} + <> Qs.octets.headers(t) : List<&2, Http.Header>} + +# LAW: no header fields are no header fields +law octets_headers_nil: + for hs: List<&2, Http.Header> + for e: {[] == hs : List<&2, Http.Header>} + {Qs.octets.headers(hs) == [] : List<&2, Http.Header>} diff --git a/laws/OneOctetPerChar.bend b/laws/OneOctetPerChar.bend new file mode 100644 index 0000000..2a69a48 --- /dev/null +++ b/laws/OneOctetPerChar.bend @@ -0,0 +1,40 @@ +# What the client and the server receive is octets. The server turns each +# receive into text, one character per octet, by `Bytes.to_string` (trusted +# in src/wire.bend); the client keeps a response's octets packed, and turns +# into text only a receive it must judge by its text +# (laws/ReadsJudgedByCount.bend), its heads and its chunk-size lines +# (trusted in src/packed.bend). The octet walks in `src/http.bend` +# (`octs.len`, `octs.take`, `octs.drop`, and those built on them) count one +# octet per character, so every Content-Length and chunk size counts octets +# (RFC 9110 §8.6, RFC 9112 §6.3, §7.1). Text is decoded from octets only at +# the end, for the caller (`Qs.octets.text`, `step.text`), and a response +# body only when the caller asks (`body_text`). A text written onto the wire +# (a request's or a reply's text body) is still counted as its UTF-8 octets +# (`utf8.len`). +# +# For every received text `s` and count `n`, `octs.len(s)` is exactly +# `String.length(s)`, and `octs.take(s, n)` and `octs.drop(s, n)` are +# exactly `String.take(s, n)` and `String.drop(s, n)`: one octet per +# character. +import Base +import ../src/http.bend as Http + +# --- bodies as octets --- + +# LAW: a received text's octets are its characters, one each +law octs_len: + for s: String + {Http.octs.len(s) == String.length(s) : Nat} + +# LAW: the first n octets of a received text are its first n characters +law octs_take: + for s: String + for +n: Nat + {Http.octs.take(s, n) == String.take(s, n) : String} + +# LAW: what follows the first n octets is what follows its first n +# characters +law octs_drop: + for s: String + for +n: Nat + {Http.octs.drop(s, n) == String.drop(s, n) : String} diff --git a/laws/OnlyGetFollows.bend b/laws/OnlyGetFollows.bend new file mode 100644 index 0000000..876b1f5 --- /dev/null +++ b/laws/OnlyGetFollows.bend @@ -0,0 +1,19 @@ +# For every flag `follow`, a GET follows redirects exactly when `follow` is +# set; for every other method (method names are case-sensitive, RFC 9110 +# §9.1) it never does, whatever `follow` says. +import Base +import ../src/redirect.bend as Redirect + +# --- which requests follow redirects --- + +# LAW: a GET follows redirects exactly when it is asked to +law follows_get: + for follow: Bool + {Redirect.redirect.follows("GET", follow) == follow : Bool} + +# LAW (RFC 9110 §9.1): every other method never follows redirects +law follows_other: + for +method: String + for e: {False{} == String.eq(method, "GET") : Bool} + for follow: Bool + {Redirect.redirect.follows(method, follow) == False{} : Bool} diff --git a/laws/OptionsAsteriskNamesServer.bend b/laws/OptionsAsteriskNamesServer.bend new file mode 100644 index 0000000..877e081 --- /dev/null +++ b/laws/OptionsAsteriskNamesServer.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §3.2.4): OPTIONS asterisk-form names the whole server +law ask_options_asterisk: + {Http.ask("OPTIONS * HTTP/1.1\r\nHost: h\r\n\r\n") + == Http.Request{"OPTIONS", "*", [Http.H{"Host", "h"}], ""} : Http.Request} diff --git a/laws/OptionsIsIdempotent.bend b/laws/OptionsIsIdempotent.bend new file mode 100644 index 0000000..6da820a --- /dev/null +++ b/laws/OptionsIsIdempotent.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW: OPTIONS is idempotent because it is safe +law method_idempotent_options: + {Http.method.idempotent("OPTIONS") == True{} : Bool} diff --git a/laws/OptionsIsSafe.bend b/laws/OptionsIsSafe.bend new file mode 100644 index 0000000..6f302bf --- /dev/null +++ b/laws/OptionsIsSafe.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.2.1): OPTIONS is safe +law method_safe_options: + {Http.method.safe("OPTIONS") == True{} : Bool} diff --git a/laws/OptionsRequestLine.bend b/laws/OptionsRequestLine.bend new file mode 100644 index 0000000..be7ee92 --- /dev/null +++ b/laws/OptionsRequestLine.bend @@ -0,0 +1,10 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.3.7): OPTIONS request-line opens with OPTIONS and the path +law options_req_names_path: + for +host: String + for +path: String + for hs: List<&2, Http.Header> + {String.starts_with(Http.options_req(host, path, hs), "OPTIONS " ++ path) + == True{} : Bool} diff --git a/laws/OriginFormKeepsQuery.bend b/laws/OriginFormKeepsQuery.bend new file mode 100644 index 0000000..83da1bc --- /dev/null +++ b/laws/OriginFormKeepsQuery.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §3.2.1): origin-form keeps an absolute-path and its query +law ask_origin_form_keeps_query: + {Http.ask("GET /ok?a=1 HTTP/1.1\r\nHost: h\r\n\r\n") + == Http.Request{"GET", "/ok?a=1", [Http.H{"Host", "h"}], ""} : Http.Request} diff --git a/laws/OtherSchemesDefaultTo80.bend b/laws/OtherSchemesDefaultTo80.bend new file mode 100644 index 0000000..522fb8d --- /dev/null +++ b/laws/OtherSchemesDefaultTo80.bend @@ -0,0 +1,8 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.2.3): only https implies 443; other schemes get 80 +law dflt_plain: + for +scheme: String + for e: {False{} == String.eq(scheme, "https") : Bool} + {Url.dflt(scheme) == 80 : U32} diff --git a/laws/OtherStatusesStopChain.bend b/laws/OtherStatusesStopChain.bend new file mode 100644 index 0000000..6fca546 --- /dev/null +++ b/laws/OtherStatusesStopChain.bend @@ -0,0 +1,24 @@ +# Every other status stops the chain, and so does any response when no hops +# are left. +import Base +import ../src/redirect.bend as Redirect + +# LAW: every status other than 301, 302, 303, 307 and 308 stops the chain +law redir_other: + for +status: U32 + for e: {False{} == Bool.or(U32.is_eq(status, 301), Bool.or(U32.is_eq(status, + 302), Bool.or(U32.is_eq(status, 303), Bool.or(U32.is_eq(status, 307), + U32.is_eq(status, 308))))) : Bool} + for left: Nat + for url: String + for +loc: String + {Redirect.redirect.next(left, url, status, loc) == Redirect.Stop{} + : Redirect.Hop} + +# LAW: with no hops left the chain stops, whatever the response +law redir_zero: + for url: String + for +status: U32 + for +loc: String + {Redirect.redirect.next(0n, url, status, loc) == Redirect.Stop{} + : Redirect.Hop} diff --git a/laws/ParseQuerySplitsPairs.bend b/laws/ParseQuerySplitsPairs.bend new file mode 100644 index 0000000..235f632 --- /dev/null +++ b/laws/ParseQuerySplitsPairs.bend @@ -0,0 +1,54 @@ +# For every string, `parse_query` splits it on every `&` (WHATWG URL §5.1 +# step 1), skips each empty piece (step 3.1), and reads every other piece in +# order as a pair: cut at its first `=`, the name is the text before it and +# the value the text after it; a piece with no `=` is a name with an empty +# value (step 3.2); both are `unquote_plus`'d (step 3.3, 3.4). A blank value +# is kept, and every pair is kept in order, duplicate names included. +# +# also rests on: laws/CutAtFirstEquals.bend, laws/SplitAtCharacter.bend +import Base +import ../src/qs.bend as Qs + +# LAW (WHATWG URL §5.1 step 1): a query is split on every `&` +law qs_parse: + for text: String + {Qs.parse(text) == Qs.fields(String.split(text, '&')) + : List<&2, Qs.Kv()>} + +# LAW: no pieces, no pairs +law qs_fields_nil: + for ps: List<&2, String> + for e: {[] == ps : List<&2, String>} + {Qs.fields(ps) == [] : List<&2, Qs.Kv()>} + +# LAW (WHATWG URL §5.1 step 3.1): an empty piece is skipped +law qs_fields_skip: + for t: List<&2, String> + {Qs.fields("" <> t) == Qs.fields(t) : List<&2, Qs.Kv()>} + +# LAW: any other piece is a pair, in front of the rest's, in order +law qs_fields_keep: + for +p: String + for t: List<&2, String> + for e: {False{} == String.is_empty(p) : Bool} + {Qs.fields(p <> t) == Qs.field(p) <> Qs.fields(t) : List<&2, Qs.Kv()>} + +# LAW: a piece is read by where its first `=` is +law qs_field: + for +piece: String + {Qs.field(piece) == Qs.field.of(Qs.cut(piece), piece) : Qs.Kv()} + +# LAW (WHATWG URL §5.1 step 3.2): a piece with no `=` is a name with an +# empty value +law qs_field_none: + for whole: String + {Qs.field.of(None{}, whole) == (Qs.unquote.plus(whole), "") : Qs.Kv()} + +# LAW (WHATWG URL §5.1 steps 3.2 to 3.4): a piece cut at its first `=` is the +# name before it and the value after it, both unquote_plus'd +law qs_field_some: + for name: String + for value: String + for whole: String + {Qs.field.of(Some{(name, value)}, whole) + == (Qs.unquote.plus(name), Qs.unquote.plus(value)) : Qs.Kv()} diff --git a/laws/PatchNotIdempotent.bend b/laws/PatchNotIdempotent.bend new file mode 100644 index 0000000..2eea20a --- /dev/null +++ b/laws/PatchNotIdempotent.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.2.2): PATCH is not one of the idempotent methods we name +law method_idempotent_patch: + {Http.method.idempotent("PATCH") == False{} : Bool} diff --git a/laws/PathFormKeepsSlash.bend b/laws/PathFormKeepsSlash.bend new file mode 100644 index 0000000..d5b9537 --- /dev/null +++ b/laws/PathFormKeepsSlash.bend @@ -0,0 +1,23 @@ +# The path form writes `/` as itself and the component form writes it as +# `%2F`; on every other character the two forms agree. +import Base +import ../src/pct.bend as Pct + +# LAW: the path form writes "/" as itself +law path_keeps_slash: + for rest: String + {Pct.one(True{}, 47, rest) == SCon{'/', rest} : String} + +# LAW: the component form writes "/" as %2F +law component_escapes_slash: + for rest: String + {Pct.one(False{}, 47, rest) == SCon{'%', SCon{'2', SCon{'F', rest}}} + : String} + +# LAW: on every character but "/" the component form writes what the path +# form writes +law component_is_path_elsewhere: + for +c: U32 + for e: {False{} == U32.is_eq(c, 47) : Bool} + for rest: String + {Pct.one(False{}, c, rest) == Pct.one(True{}, c, rest) : String} diff --git a/laws/PhraseTableLookup.bend b/laws/PhraseTableLookup.bend new file mode 100644 index 0000000..c6c4b63 --- /dev/null +++ b/laws/PhraseTableLookup.bend @@ -0,0 +1,30 @@ +# A code's text in a table of (code, text) entries (`reason.in`) is the +# first entry's for that code, and empty when no entry is for it. The +# reason phrases and the errno names are both read from such a table. +import Base +import ../src/http.bend as Http + +# LAW: a list with no entries gives every status the empty phrase +law reason_in_nil: + for +code: U32 + {Http.reason.in([], code) == "" : String} + +# LAW: a list gives a status its first entry's phrase when that entry is for +# that status +law reason_in_hit: + for +code: U32 + for c: U32 + for text: String + for t: List<&2, Http.Phrase> + for e: {True{} == U32.is_eq(code, c) : Bool} + {Http.reason.in(Http.P{c, text} <> t, code) == text : String} + +# LAW: and the rest's phrase when it is for another status +law reason_in_miss: + for +code: U32 + for c: U32 + for text: String + for t: List<&2, Http.Phrase> + for e: {False{} == U32.is_eq(code, c) : Bool} + {Http.reason.in(Http.P{c, text} <> t, code) == Http.reason.in(t, code) + : String} diff --git a/laws/PostNotIdempotent.bend b/laws/PostNotIdempotent.bend new file mode 100644 index 0000000..b3dfa39 --- /dev/null +++ b/laws/PostNotIdempotent.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW: POST is not idempotent +law method_idempotent_post: + {Http.method.idempotent("POST") == False{} : Bool} diff --git a/laws/PostNotSafe.bend b/laws/PostNotSafe.bend new file mode 100644 index 0000000..1533cbb --- /dev/null +++ b/laws/PostNotSafe.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW: POST is not safe +law method_safe_post: + {Http.method.safe("POST") == False{} : Bool} diff --git a/laws/PostRequestLine.bend b/laws/PostRequestLine.bend new file mode 100644 index 0000000..f1753db --- /dev/null +++ b/laws/PostRequestLine.bend @@ -0,0 +1,11 @@ +import Base +import ../src/http.bend as Http + +# LAW: POST request-line opens with POST and the path +law post_req_names_path: + for +host: String + for +path: String + for hs: List<&2, Http.Header> + for +body: String + {String.starts_with(Http.post_req(host, path, hs, body), "POST " ++ path) + == True{} : Bool} diff --git a/laws/PostWritesContentLength.bend b/laws/PostWritesContentLength.bend new file mode 100644 index 0000000..ae30ed3 --- /dev/null +++ b/laws/PostWritesContentLength.bend @@ -0,0 +1,15 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §8.6): a non-empty request body is framed by Content-Length +law post_writes_content_length: + {Http.request("POST", "h", "/a", [], "hi") + == "POST /a HTTP/1.1\r\n" + ++ "Host: h\r\n" + ++ "User-Agent: ezhttp\r\n" + ++ "Accept: */*\r\n" + ++ "Connection: close\r\n" + ++ "Content-Length: 2\r\n" + ++ "\r\n" + ++ "hi" + : String} diff --git a/laws/PutIsIdempotent.bend b/laws/PutIsIdempotent.bend new file mode 100644 index 0000000..0ab47fa --- /dev/null +++ b/laws/PutIsIdempotent.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.2.2): PUT is idempotent +law method_idempotent_put: + {Http.method.idempotent("PUT") == True{} : Bool} diff --git a/laws/PutNotSafe.bend b/laws/PutNotSafe.bend new file mode 100644 index 0000000..a2513d6 --- /dev/null +++ b/laws/PutNotSafe.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.2.1): PUT is not safe +law method_safe_put: + {Http.method.safe("PUT") == False{} : Bool} diff --git a/laws/PutRequestLine.bend b/laws/PutRequestLine.bend new file mode 100644 index 0000000..c82057b --- /dev/null +++ b/laws/PutRequestLine.bend @@ -0,0 +1,11 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §9.3.4): PUT request-line opens with PUT and the path +law put_req_names_path: + for +host: String + for +path: String + for hs: List<&2, Http.Header> + for +body: String + {String.starts_with(Http.put_req(host, path, hs, body), "PUT " ++ path) + == True{} : Bool} diff --git a/laws/QueryGetLastWins.bend b/laws/QueryGetLastWins.bend new file mode 100644 index 0000000..bb7162b --- /dev/null +++ b/laws/QueryGetLastWins.bend @@ -0,0 +1,40 @@ +# For every list of pairs and name, `query_get` answers the value of the +# last pair whose name is exactly that name, and none when no pair has it +# (last wins, as `dict(parse_qsl(...))` does). +import Base +import ../src/qs.bend as Qs + +# LAW: no pairs give no value +law qs_get_nil: + for name: String + {Qs.get([], name) == None{} : Maybe<&2, String>} + +# LAW: a later pair with the name wins +law qs_get_later: + for key: String + for value: String + for t: List<&2, Qs.Kv()> + for +name: String + for x: String + for e: {Some{x} == Qs.get(t, name) : Maybe<&2, String>} + {Qs.get((key, value) <> t, name) == Some{x} : Maybe<&2, String>} + +# LAW: with no later pair with the name, this one's value when it has it +law qs_get_here: + for key: String + for value: String + for t: List<&2, Qs.Kv()> + for +name: String + for e1: {None{} == Qs.get(t, name) : Maybe<&2, String>} + for e2: {True{} == String.eq(key, name) : Bool} + {Qs.get((key, value) <> t, name) == Some{value} : Maybe<&2, String>} + +# LAW: and none when it does not +law qs_get_miss: + for key: String + for value: String + for t: List<&2, Qs.Kv()> + for +name: String + for e1: {None{} == Qs.get(t, name) : Maybe<&2, String>} + for e2: {False{} == String.eq(key, name) : Bool} + {Qs.get((key, value) <> t, name) == None{} : Maybe<&2, String>} diff --git a/laws/QuoteEscapesUtf8Bytes.bend b/laws/QuoteEscapesUtf8Bytes.bend new file mode 100644 index 0000000..0914ccf --- /dev/null +++ b/laws/QuoteEscapesUtf8Bytes.bend @@ -0,0 +1,79 @@ +# Every character a form does not keep is written as `%XX` for each byte of +# its UTF-8 encoding, in order (RFC 3986 §2.1, RFC 3629 §3), `XX` being the +# byte's high then low nibble as hex digits `0`-`9` and upper-case `A`-`F`. +import Base +import ../src/pct.bend as Pct + +# LAW (RFC 3986 §2.1): a character the form does not keep is written as the +# %XX triples of its UTF-8 bytes +law one_escaped: + for slash: Bool + for +c: U32 + for e: {False{} == Pct.safe(slash, c) : Bool} + for rest: String + {Pct.one(slash, c, rest) == Pct.pct(Pct.utf8(c), rest) : String} + +# LAW: no bytes write nothing +law pct_nil: + for rest: String + {Pct.pct([], rest) == rest : String} + +# LAW (RFC 3986 §2.1): a byte is written as "%", its high nibble, its low +# nibble, before the bytes after it +law pct_cons: + for +b: U32 + for bs: List<&2, U32> + for rest: String + {Pct.pct(b <> bs, rest) + == SCon{'%', SCon{Pct.hex(U32.shrn(b, 4n)), + SCon{Pct.hex(U32.and(b, 15)), Pct.pct(bs, rest)}}} : String} + +# LAW: a nibble below 10 is the digit '0' + n +law hex_digit: + for +n: U32 + for e: {True{} == U32.is_lt(n, 10) : Bool} + {Pct.hex(n) == Chr{U32.add(48, n)} : Char} + +# LAW (RFC 3986 §2.1, upper case): a nibble of 10 or more is the letter +# 'A' + (n - 10), that is code 55 + n +law hex_letter: + for +n: U32 + for e: {False{} == U32.is_lt(n, 10) : Bool} + {Pct.hex(n) == Chr{U32.add(55, n)} : Char} + +# LAW (RFC 3629 §3): U+0000 to U+007F is one byte, 0xxxxxxx +law utf8_one: + for +c: U32 + for e: {True{} == U32.is_lt(c, 128) : Bool} + {Pct.utf8(c) == [c] : List<&2, U32>} + +# LAW (RFC 3629 §3): U+0080 to U+07FF is two bytes, 110xxxxx 10xxxxxx +law utf8_two: + for +c: U32 + for e1: {False{} == U32.is_lt(c, 128) : Bool} + for e2: {True{} == U32.is_lt(c, 2048) : Bool} + {Pct.utf8(c) == [U32.or(192, U32.shrn(c, 6n)), + U32.or(128, U32.and(c, 63))] : List<&2, U32>} + +# LAW (RFC 3629 §3): U+0800 to U+FFFF is three bytes, +# 1110xxxx 10xxxxxx 10xxxxxx +law utf8_three: + for +c: U32 + for e1: {False{} == U32.is_lt(c, 128) : Bool} + for e2: {False{} == U32.is_lt(c, 2048) : Bool} + for e3: {True{} == U32.is_lt(c, 65536) : Bool} + {Pct.utf8(c) == [U32.or(224, U32.shrn(c, 12n)), + U32.or(128, U32.and(U32.shrn(c, 6n), 63)), + U32.or(128, U32.and(c, 63))] : List<&2, U32>} + +# LAW (RFC 3629 §3): U+10000 to U+10FFFF is four bytes, +# 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx +law utf8_four: + for +c: U32 + for e1: {False{} == U32.is_lt(c, 128) : Bool} + for e2: {False{} == U32.is_lt(c, 2048) : Bool} + for e3: {False{} == U32.is_lt(c, 65536) : Bool} + {Pct.utf8(c) == [U32.or(240, U32.shrn(c, 18n)), + U32.or(128, U32.and(U32.shrn(c, 12n), 63)), + U32.or(128, U32.and(U32.shrn(c, 6n), 63)), + U32.or(128, U32.and(c, 63))] : List<&2, U32>} diff --git a/laws/QuoteKeepsUnreserved.bend b/laws/QuoteKeepsUnreserved.bend new file mode 100644 index 0000000..b0ee3cf --- /dev/null +++ b/laws/QuoteKeepsUnreserved.bend @@ -0,0 +1,35 @@ +# `quote_path(s)` and `quote_component(s)` (`src/pct.bend`) percent-encode a +# string as RFC 3986 §2.1 describes, the way Python's +# `urllib.parse.quote(s)` and `urllib.parse.quote(s, safe="")` do. A +# character is a Unicode scalar value (U+0000 to U+10FFFF, no surrogates). +# Its UTF-8 encoding is the one RFC 3629 §3 tabulates. +# +# For every string, both forms write its characters in order, each one +# independently of the others; an unreserved character (RFC 3986 §2.3: +# `A`-`Z`, `a`-`z`, `0`-`9`, `-`, `.`, `_`, `~`) is written as itself. +import Base +import ../src/pct.bend as Pct + +# --- percent-encoding --- + +# LAW: the empty string is written as the empty string, in both forms +law quote_nil: + for +slash: Bool + {Pct.quote(slash, "") == "" : String} + +# LAW: a string is written one character at a time, the first character's +# writing followed by the rest's +law quote_cons: + for +slash: Bool + for +c: U32 + for t: String + {Pct.quote(slash, SCon{Chr{c}, t}) + == Pct.one(slash, c, Pct.quote(slash, t)) : String} + +# LAW (RFC 3986 §2.3): an unreserved character is written as itself +law one_unreserved: + for slash: Bool + for +c: U32 + for e: {True{} == Pct.unreserved(c) : Bool} + for rest: String + {Pct.one(slash, c, rest) == SCon{Chr{c}, rest} : String} diff --git a/laws/QuoteOutputIsFramed.bend b/laws/QuoteOutputIsFramed.bend new file mode 100644 index 0000000..7f5880a --- /dev/null +++ b/laws/QuoteOutputIsFramed.bend @@ -0,0 +1,63 @@ +# For every string, the output holds only unreserved characters, `/` (path +# form only) and `%HH` triples whose `H` are `0`-`9` or `A`-`F`. +# +# pending: the laws here prove part of this, not all of it. +# +# proved so far: `frame_quote`: the whole output of either form is in the +# frame (kept characters, and `%` followed by exactly two upper-case hex +# digits, read left to right with nothing owed at the end), given `nibbles`: +# every character the form does not keep has UTF-8 bytes whose high and low +# nibbles `hex` writes as `0`-`9` or `A`-`F`. `frame_kept` and `frame_bytes` +# are its steps. Left to prove: `nibbles` for every string of Unicode scalar +# values: each byte `utf8` yields for a code point up to U+10FFFF is below +# 256, so both nibbles are below 16, and `hex` of a value below 16 is a hex +# digit. This is bit arithmetic on `U32` (shifts, masks, comparisons on +# symbolic words) that Base has no lemmas for yet. +import Base +import ../src/pct.bend as Pct +import ./defs/PercentFrame.bend as D1 + +# both nibbles of every byte are written as hex digits +def hexes(bs: List<&2, U32>) -> Bool: + match bs: + case []: + True{} + case +byte <> tail: + Bool.and(D1.hexdig(Pct.hex(U32.shrn(byte, 4n))), + Bool.and(D1.hexdig(Pct.hex(U32.and(byte, 15))), hexes(tail))) + +# LAW (partial): a kept character stays in the frame +law frame_kept: + for +slash: Bool + for +c: U32 + for +e: {True{} == Pct.safe(slash, c) : Bool} + for rest: String + {D1.framed(slash, Pct.one(slash, c, rest), 0n) == D1.framed(slash, rest, 0n) + : Bool} + +# LAW (partial): %XX triples stay in the frame when their nibbles are hex +# digits +law frame_bytes: + for +slash: Bool + for bs: List<&2, U32> + for +e: {hexes(bs) == True{} : Bool} + for +rest: String + {D1.framed(slash, Pct.pct(bs, rest), 0n) == D1.framed(slash, rest, 0n) : Bool} + +# every character the form does not keep has UTF-8 bytes whose nibbles are +# written as hex digits +def nibbles(+slash: Bool, str: String) -> Bool: + match str: + case SNil{}: + True{} + case SCon{Chr{+cp}, tail}: + Bool.and(Bool.or(Pct.safe(slash, cp), hexes(Pct.utf8(cp))), + nibbles(slash, tail)) + +# LAW (partial): the whole output is in the frame, given that every escaped +# character's nibbles are written as hex digits +law frame_quote: + for +slash: Bool + for +str: String + for +e: {nibbles(slash, str) == True{} : Bool} + {D1.framed(slash, Pct.quote(slash, str), 0n) == True{} : Bool} diff --git a/laws/QuotePlusWritesSpaces.bend b/laws/QuotePlusWritesSpaces.bend new file mode 100644 index 0000000..fea7b67 --- /dev/null +++ b/laws/QuotePlusWritesSpaces.bend @@ -0,0 +1,26 @@ +# For every string, `quote_plus` writes its characters in order, each +# independently of the others: a space as `+`, every other character as +# `quote_component` writes it (laws/QuoteKeepsUnreserved.bend to 3), so `+` +# is `%2B`; this is Python's `quote_plus(s)` (safe `""`). +import Base +import ../src/pct.bend as Pct +import ../src/qs.bend as Qs + +# LAW: quote_plus writes nothing for the empty string +law qs_qp_nil: + for s: String + for e: {"" == s : String} + {Qs.quote.plus(s) == "" : String} + +# LAW: a space is written `+` +law qs_qp_space: + for t: String + {Qs.quote.plus(SCon{' ', t}) == SCon{'+', Qs.quote.plus(t)} : String} + +# LAW: every other character is written as quote_component writes it +law qs_qp_other: + for +cp: U32 + for t: String + for e: {False{} == U32.is_eq(cp, 32) : Bool} + {Qs.quote.plus(SCon{Chr{cp}, t}) == Pct.one(False{}, cp, Qs.quote.plus(t)) + : String} diff --git a/laws/ReadStateMatchesWholeText.bend b/laws/ReadStateMatchesWholeText.bend new file mode 100644 index 0000000..512a022 --- /dev/null +++ b/laws/ReadStateMatchesWholeText.bend @@ -0,0 +1,48 @@ +# For every request method and texts `a` and `b`, the state the client keeps +# between reads for `a` (`Http.hear.of(method, a)`), advanced by `b` +# (`Http.hear.more`), is exactly the state for `a ++ b`; and for every text +# the state says the response is complete (`Http.hear.done`) exactly when +# laws/ResponseCompleteByFraming.bend says that text is (`Http.whole`), so +# judging a response read by read decides exactly what judging all the text +# received after every read decides. Inside it, a chunked body's walk +# (`Http.walk`) from a chunk header with some fuel has ended exactly when +# `chunks.ended` says it has, and the walk over a text and more text is the +# walk over the text, advanced by the more. +import Base +import ../src/http.bend as Http + +# --- judging a response read by read --- + +# LAW: the state for a text says the response is complete exactly when the +# whole-text check (laws/ResponseCompleteByFraming.bend) says that text is +law hear_done: + for +method: String + for +raw: String + {Http.hear.done(Http.hear.of(method, raw)) == Http.whole(method, raw) + : Bool} + +# LAW: the state for a text, advanced by more text, is the state for the two +# together +law hear_more: + for +method: String + for +a: String + for +b: String + {Http.hear.more(Http.hear.of(method, a), b) == Http.hear.of(method, a ++ b) + : Http.Hear} + +# LAW (RFC 9112 §7.1): a chunked body's walk from a chunk header has ended +# exactly when chunks.ended says, with the same fuel +law walk_done: + for f: Nat + for +rest: String + {Http.walk.done(Http.walk(f, rest)) + == Http.chunks.ended(f, Http.divide(rest, "\r\n")) : Bool} + +# LAW: the walk over a text and more text, with fuel for both, is the walk +# over the text advanced by the more +law walk_more: + for f: Nat + for +rest: String + for +b: String + {Http.walk(Nat.add(f, String.length(b)), rest ++ b) + == Http.walk.more(Http.walk(f, rest), String.length(b), b) : Http.Walk} diff --git a/laws/ReadsJudgedByCount.bend b/laws/ReadsJudgedByCount.bend new file mode 100644 index 0000000..96ab91a --- /dev/null +++ b/laws/ReadsJudgedByCount.bend @@ -0,0 +1,76 @@ +# The client keeps the octets it has received packed (trusted in +# src/client.bend) and carries from one read to the next only the state +# `Http.hear` (laws/ReadStateMatchesWholeText.bend), starting from +# `wire.got(method)`, the state for no text. For every state `h`: a read of +# no octets (the connection closed) answers `Heard` (`wire.read.count(h, +# 0)`). For every state `h` and read text `b` that is not empty, when the +# state can be advanced by `b`'s count of octets alone (`Http.hear.counts`: +# the response is already whole, its body is framed by the close or by +# Content-Length, or a chunk's data still owes at least that many octets), +# `wire.read.count` answers exactly what `wire.read.next` answers for `b` +# itself, since advancing the state by the count (`Http.hear.more.n`) is +# exactly advancing it by `b` (`Http.hear.more`); walking `n` octets off a +# text of at most `n` octets leaves `n` minus its length still owed. For +# every method, text `a` received so far and read text `t` that is not +# empty, `wire.read.next` from the state for `a` answers `Heard` when `a ++ +# t` is complete (laws/ResponseCompleteByFraming.bend), and otherwise reads +# again with the state for `a ++ t`. +import Base +import ../src/http.bend as Http +import ../src/wirespec.bend as Spec + +# --- reading until the response is whole --- + +# LAW: a read of no octets finds the connection closed, and the answer is +# everything received +law read_closed: + for h: Http.Hear + {Spec.wire.read.count(h, 0n) == Some{Spec.Heard{}} : Maybe<&2, Spec.Read>} + +# LAW: a read whose count alone advances the state is judged by its count +# exactly as by its text +law read_count: + for +h: Http.Hear + for +b: String + for e1: {False{} == String.is_empty(b) : Bool} + for e2: {True{} == Http.hear.counts(h, String.length(b)) : Bool} + {Spec.wire.read.count(h, String.length(b)) + == Some{Spec.wire.read.next(h, b)} : Maybe<&2, Spec.Read>} + +# LAW: a state the count alone advances is advanced by a text's count +# exactly as by the text +law hear_count: + for +h: Http.Hear + for +b: String + for e: {True{} == Http.hear.counts(h, String.length(b)) : Bool} + {Http.hear.more.n(h, String.length(b)) == Http.hear.more(h, b) : Http.Hear} + +# LAW: walking n octets off a text of at most n octets leaves n less its +# length owed +law skip_short: + for +b: String + for +n: Nat + for e: {False{} == Nat.is_lt(n, String.length(b)) : Bool} + {Http.octs.skip(b, n) == Http.Short{Nat.sub(n, String.length(b))} + : Http.Skip} + +# LAW: a read whose text completes the response is heard +law read_whole: + for +method: String + for +a: String + for +text: String + for e1: {False{} == String.is_empty(text) : Bool} + for e2: {True{} == Http.whole(method, a ++ text) : Bool} + {Spec.wire.read.next(Http.hear.of(method, a), text) == Spec.Heard{} + : Spec.Read} + +# LAW: a read whose text does not complete the response reads again, in the +# state for what has been received with it +law read_more: + for +method: String + for +a: String + for +text: String + for e1: {False{} == String.is_empty(text) : Bool} + for e2: {False{} == Http.whole(method, a ++ text) : Bool} + {Spec.wire.read.next(Http.hear.of(method, a), text) + == Spec.Again{Http.hear.of(method, a ++ text)} : Spec.Read} diff --git a/laws/Reason200IsOk.bend b/laws/Reason200IsOk.bend new file mode 100644 index 0000000..d820360 --- /dev/null +++ b/laws/Reason200IsOk.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §4): 200 uses the OK reason-phrase +law reason_200: + {Http.reason(200) == "OK" : String} diff --git a/laws/Reason201IsCreated.bend b/laws/Reason201IsCreated.bend new file mode 100644 index 0000000..e3f027d --- /dev/null +++ b/laws/Reason201IsCreated.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §15.3.2): 201 Created +law reason_201: + {Http.reason(201) == "Created" : String} diff --git a/laws/Reason204IsNoContent.bend b/laws/Reason204IsNoContent.bend new file mode 100644 index 0000000..3e75ae7 --- /dev/null +++ b/laws/Reason204IsNoContent.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §15.3.5): 204 No Content +law reason_204: + {Http.reason(204) == "No Content" : String} diff --git a/laws/Reason400IsBadRequest.bend b/laws/Reason400IsBadRequest.bend new file mode 100644 index 0000000..40bef94 --- /dev/null +++ b/laws/Reason400IsBadRequest.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §15.5.1): 400 Bad Request +law reason_400: + {Http.reason(400) == "Bad Request" : String} diff --git a/laws/Reason404IsNotFound.bend b/laws/Reason404IsNotFound.bend new file mode 100644 index 0000000..73e2f57 --- /dev/null +++ b/laws/Reason404IsNotFound.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §15.5.5): 404 Not Found +law reason_404: + {Http.reason(404) == "Not Found" : String} diff --git a/laws/Reason405IsMethodNotAllowed.bend b/laws/Reason405IsMethodNotAllowed.bend new file mode 100644 index 0000000..5b0d35f --- /dev/null +++ b/laws/Reason405IsMethodNotAllowed.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §15.5.6): 405 Method Not Allowed +law reason_405: + {Http.reason(405) == "Method Not Allowed" : String} diff --git a/laws/Reason500IsInternalServerError.bend b/laws/Reason500IsInternalServerError.bend new file mode 100644 index 0000000..45c4f92 --- /dev/null +++ b/laws/Reason500IsInternalServerError.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §15.6.1): 500 Internal Server Error +law reason_500: + {Http.reason(500) == "Internal Server Error" : String} diff --git a/laws/ReasonPhraseFromFinalHead.bend b/laws/ReasonPhraseFromFinalHead.bend new file mode 100644 index 0000000..c50eef3 --- /dev/null +++ b/laws/ReasonPhraseFromFinalHead.bend @@ -0,0 +1,100 @@ +# For every text an exchange answers with, the response's reason phrase +# (`Client.Response`'s `reason`, and `reason_phrase(raw)` on a response's +# text) is read from the status line of the head +# laws/InterimResponsesSkipped.bend stops at, an interim head with text +# after it skipped by the same rule (RFC 9110 §15.2), and is empty when no +# head ends. RFC 9112 §4 writes a status line as `HTTP-version SP +# status-code SP [ reason-phrase ]`, and a reason-phrase holds no CR, so for +# every version `v` and code `c` that hold no SP and every reason `r` that +# holds no CR, the line ` ` has reason exactly `r`, also when it +# still ends in the CR of its CRLF; a line with one SP or none has an empty +# reason. +import Base +import ../src/http.bend as Http +import ../src/wirespec.bend as Spec +import ./defs/CharacterAbsent.bend as D1 + +# --- a response's reason phrase --- + +# LAW: a text's reason phrase is read head by head, at most as many heads as +# it has characters +law phrase_to: + for +raw: String + {Http.phrase.to(raw) + == Http.phrase.to.go(String.length(raw), Http.divide(raw, "\r\n\r\n")) + : String} + +# LAW: a read with no heads left found no reason +law phrase_fuel: + for c: Http.Cut + {Http.phrase.to.go(0n, c) == "" : String} + +# LAW (RFC 9112 §2.1): a head that never ends has no reason +law phrase_open: + for f: Nat + {Http.phrase.to.go(1n+f, Http.NoCut{}) == "" : String} + +# LAW (RFC 9110 §15.2): an interim head with text after it is skipped by the +# same rule laws/InterimResponsesSkipped.bend skips it by, and the reason +# read from the text after it +law phrase_interim: + for f: Nat + for +before: String + for +after: String + for e: {True{} == Http.interim.skip(Http.status.of(String.lines(before)), + after) : Bool} + {Http.phrase.to.go(1n+f, Http.Cut{before, after}) + == Http.phrase.to.go(f, Http.divide(after, "\r\n\r\n")) : String} + +# LAW: the head laws/InterimResponsesSkipped.bend stops at gives the reason +# on its status line +law phrase_final: + for f: Nat + for +before: String + for +after: String + for e: {False{} == Http.interim.skip(Http.status.of(String.lines(before)), + after) : Bool} + {Http.phrase.to.go(1n+f, Http.Cut{before, after}) + == Http.phrase.line(Http.first(String.lines(before))) : String} + +# LAW (RFC 9112 §4): a status line ` SP SP ` whose version and code +# hold no SP and whose reason holds no CR has reason exactly r +law phrase_line: + for +v: String + for +c: String + for +r: String + for e1: {True{} == D1.avoids(v, ' ') : Bool} + for e2: {True{} == D1.avoids(c, ' ') : Bool} + for e3: {True{} == D1.avoids(r, '\r') : Bool} + {Http.phrase.line(v ++ SCon{' ', c ++ SCon{' ', r}}) == r : String} + +# LAW (RFC 9112 §4): the CR that ends a status line's CRLF is not part of +# its reason +law phrase_line_cr: + for +v: String + for +c: String + for +r: String + for e1: {True{} == D1.avoids(v, ' ') : Bool} + for e2: {True{} == D1.avoids(c, ' ') : Bool} + for e3: {True{} == D1.avoids(r, '\r') : Bool} + {Http.phrase.line(v ++ SCon{' ', c ++ SCon{' ', r ++ "\r"}}) == r : String} + +# LAW: a status line with one SP has an empty reason +law phrase_line_one: + for +v: String + for +c: String + for e1: {True{} == D1.avoids(v, ' ') : Bool} + for e2: {True{} == D1.avoids(c, ' ') : Bool} + {Http.phrase.line(v ++ SCon{' ', c}) == "" : String} + +# LAW: a status line with no SP has an empty reason +law phrase_line_none: + for +v: String + for e: {True{} == D1.avoids(v, ' ') : Bool} + {Http.phrase.line(v) == "" : String} + +# LAW: the client reads the reason from the same text it reads the response +# from +law phrase_heard: + for +text: String + {Spec.wire.phrase("0\n" ++ text) == Http.phrase.to(text) : String} diff --git a/laws/ReasonPhrasesFollowRfc.bend b/laws/ReasonPhrasesFollowRfc.bend new file mode 100644 index 0000000..9cc5fcf --- /dev/null +++ b/laws/ReasonPhrasesFollowRfc.bend @@ -0,0 +1,55 @@ +# For every status, the reason-phrase the server writes after it is exactly +# the one RFC 9110 §15 names (RFC 6585 §4 for 429): 200 `OK`, 201 `Created`, +# 202 `Accepted`, 204 `No Content`, 301 `Moved Permanently`, 302 `Found`, +# 303 `See Other`, 304 `Not Modified`, 307 `Temporary Redirect`, 308 +# `Permanent Redirect`, 400 `Bad Request`, 401 `Unauthorized`, 403 +# `Forbidden`, 404 `Not Found`, 405 `Method Not Allowed`, 409 `Conflict`, +# 410 `Gone`, 413 `Content Too Large`, 415 `Unsupported Media Type`, 422 +# `Unprocessable Content`, 429 `Too Many Requests`, 500 `Internal Server +# Error`, 501 `Not Implemented`, 502 `Bad Gateway`, 503 `Service +# Unavailable`, 504 `Gateway Timeout`; for every other status it is empty. +# +# also rests on: laws/PhraseTableLookup.bend +import Base +import ../src/http.bend as Http +import ../src/redirect.bend as Redirect + +# --- RFC 9112 request and response writers --- + +# LAW (RFC 9112 §4, §9.6): a response status-line is HTTP/1.1 plus the +# reason-phrase, and the connection closes +law respond_writes_reason_and_close: + for +status: U32 + for hs: List<&2, Http.Header> + for +body: String + {Http.respond(status, hs, body) + == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" + ++ "Connection: close\r\n" + ++ Http.headers.lines(hs) + ++ Http.clen.line(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)) + ++ "\r\n" + ++ body + : String} + +# the reason-phrases this file opens with, as it words them +def phrases() -> List<&2, Http.Phrase>: + [Http.P{200, "OK"}, Http.P{201, "Created"}, Http.P{202, "Accepted"}, + Http.P{204, "No Content"}, Http.P{301, "Moved Permanently"}, + Http.P{302, "Found"}, Http.P{303, "See Other"}, + Http.P{304, "Not Modified"}, Http.P{307, "Temporary Redirect"}, + Http.P{308, "Permanent Redirect"}, Http.P{400, "Bad Request"}, + Http.P{401, "Unauthorized"}, Http.P{403, "Forbidden"}, + Http.P{404, "Not Found"}, Http.P{405, "Method Not Allowed"}, + Http.P{409, "Conflict"}, Http.P{410, "Gone"}, + Http.P{413, "Content Too Large"}, Http.P{415, "Unsupported Media Type"}, + Http.P{422, "Unprocessable Content"}, Http.P{429, "Too Many Requests"}, + Http.P{500, "Internal Server Error"}, Http.P{501, "Not Implemented"}, + Http.P{502, "Bad Gateway"}, Http.P{503, "Service Unavailable"}, + Http.P{504, "Gateway Timeout"}] + +# LAW: every status's reason-phrase is the one +# laws/ReasonPhrasesFollowRfc.bend lists for it, and empty for a status it +# does not list +law reason_phrases: + for +code: U32 + {Http.reason(code) == Http.reason.in(phrases(), code) : String} diff --git a/laws/RedirectsFollowLocation.bend b/laws/RedirectsFollowLocation.bend new file mode 100644 index 0000000..39c0c57 --- /dev/null +++ b/laws/RedirectsFollowLocation.bend @@ -0,0 +1,57 @@ +# `http.get_opts` with `follow` set follows redirects, and so does +# `http.request_opts` when the method is GET. GET is the only method that +# follows them (a POST, PUT or DELETE answers a redirect as its response; +# 303-to-GET is not done), so every hop is a GET with the caller's headers +# and timeout; the first carries the caller's body and the hops after it +# none. A chain is at most 10 hops (httpx allows 20); a chain that runs out +# answers its last response, a redirect. A Location is resolved against the +# current URL as RFC 3986 §5.2 resolves a reference against a base URI. The +# decisions live in `src/redirect.bend`, which does no IO; `client.follow` +# in `src/client.bend` acts on them (trusted in src/client.bend). +# +# For every status 301, 302, 303, 307 or 308 (RFC 9110 §15.4), with hops +# left, whose Location header (read case-insensitively, empty when absent) +# has a target, the next hop is a GET to exactly that target; a Location +# with no target stops the chain. +import Base +import ../src/http.bend as Http +import ../src/redirect.bend as Redirect + +# --- redirects --- + +# LAW: a redirect status, with hops left, whose Location has a target goes to +# exactly that target +law redir_go: + for +status: U32 + for e: {True{} == Bool.or(U32.is_eq(status, 301), Bool.or(U32.is_eq(status, + 302), Bool.or(U32.is_eq(status, 303), Bool.or(U32.is_eq(status, 307), + U32.is_eq(status, 308))))) : Bool} + for n: Nat + for url: String + for +loc: String + for next: String + for et: {Some{next} == Redirect.redirect.target(url, loc) + : Maybe<&2, String>} + {Redirect.redirect.next(1n+n, url, status, loc) == Redirect.Go{next} + : Redirect.Hop} + +# LAW: a redirect whose Location has no target stops the chain +law redir_no_target: + for +status: U32 + for left: Nat + for url: String + for +loc: String + for et: {None{} == Redirect.redirect.target(url, loc) : Maybe<&2, String>} + {Redirect.redirect.next(left, url, status, loc) == Redirect.Stop{} + : Redirect.Hop} + +# LAW: the Location the planner reads is the response's Location header +# (case-insensitive, "" when absent) +law redir_plan_location: + for left: Nat + for url: String + for +status: U32 + for hs: List<&2, Http.Header> + {Redirect.redirect.plan(left, url, status, hs) + == Redirect.redirect.next(left, url, status, + Http.headers.find(hs, "location")) : Redirect.Hop} diff --git a/laws/RelativeLocationsMerge.bend b/laws/RelativeLocationsMerge.bend new file mode 100644 index 0000000..2227c85 --- /dev/null +++ b/laws/RelativeLocationsMerge.bend @@ -0,0 +1,105 @@ +# Against a current URL that parses into scheme, host, port and path, a +# Location that starts `?` is the scheme, `://`, host, `:`, port, the +# current path without its query, then the Location; any other relative +# Location (a relative-path reference, such as `web/`) is the scheme, `://`, +# host, `:`, port, then the merge of the current path (without its query) +# and the Location's path (RFC 3986 §5.2.3: the current path's segments but +# its last, then the Location's) with dot segments removed, then the +# Location's query unchanged, its fragment dropped. +# +# also rests on: laws/LocationResolvedBySort.bend, +# laws/LocationSplitsAtQuery.bend +import Base +import ../src/url.bend as Url +import ../src/redirect.bend as Redirect + +# LAW (RFC 3986 §5.2.2): a query keeps the origin and the current path +law against_query: + for scheme: String + for host: String + for port: U32 + for path: String + for +loc: String + {Redirect.redirect.against(scheme, host, port, path, loc, Redirect.Query{}) + == Some{scheme ++ "://" ++ host ++ ":" ++ U32.show(port) + ++ Redirect.redirect.base(path) ++ loc} : Maybe<&2, String>} + +# LAW (RFC 3986 §5.2.2): a relative path keeps the origin and is merged with +# the current path +law against_rel: + for scheme: String + for host: String + for port: U32 + for path: String + for +loc: String + {Redirect.redirect.against(scheme, host, port, path, loc, Redirect.Rel{}) + == Some{scheme ++ "://" ++ host ++ ":" ++ U32.show(port) + ++ Redirect.redirect.rel.of(Redirect.redirect.base(path), + Redirect.redirect.split(loc))} : Maybe<&2, String>} + +# LAW (RFC 3986 §4.2): any other Location that starts `?` is a query +law sort_query: + for +loc: String + for e1: {False{} == String.starts_with(loc, "//") : Bool} + for e2: {False{} == String.starts_with(loc, "/") : Bool} + for e3: {False{} == Redirect.redirect.stays(loc) : Bool} + for e4: {True{} == String.starts_with(loc, "?") : Bool} + {Redirect.redirect.sort(loc) == Redirect.Query{} : Redirect.Sort} + +# LAW (RFC 3986 §4.2): and every other one is a relative path +law sort_rel: + for +loc: String + for e1: {False{} == String.starts_with(loc, "//") : Bool} + for e2: {False{} == String.starts_with(loc, "/") : Bool} + for e3: {False{} == Redirect.redirect.stays(loc) : Bool} + for e4: {False{} == String.starts_with(loc, "?") : Bool} + {Redirect.redirect.sort(loc) == Redirect.Rel{} : Redirect.Sort} + +# LAW (RFC 3986 §5.2.3): a relative path follows every segment of the current +# path but its last, then dot segments are removed, then its query +law rel_of: + for base: String + for path: String + for query: String + {Redirect.redirect.rel.of(base, Redirect.Ref{path, query}) + == Redirect.redirect.clean(List.tail(&2, String, + List.append(&2, String, Redirect.redirect.init(String.split(base, '/')), + String.split(path, '/')))) ++ query : String} + +# LAW: the current path, for merging, is the part before its `?` +law base_path: + for path: String + {Redirect.redirect.base(path) == Url.query.hier(String.split(path, '?')) + : String} + +# LAW: the first piece of a split, empty when there is none +law hier_first: + for h: String + for t: List<&2, String> + {Url.query.hier(h <> t) == h : String} + +# LAW: no segments have no segments but the last +law init_nil: + for ss: List<&2, String> + for e: {True{} == List.is_empty(&2, String, ss) : Bool} + {Redirect.redirect.init(ss) == [] : List<&2, String>} + +# LAW: segments but the last start from the first +law init_cons: + for s: String + for t: List<&2, String> + {Redirect.redirect.init(s <> t) == Redirect.redirect.init.of(t, s) + : List<&2, String>} + +# LAW: the last segment is dropped +law init_last: + for s: String + {Redirect.redirect.init.of([], s) == [] : List<&2, String>} + +# LAW: every segment before the last is kept, in order +law init_keep: + for s: String + for u: String + for r: List<&2, String> + {Redirect.redirect.init.of(u <> r, s) == s <> Redirect.redirect.init.of(r, u) + : List<&2, String>} diff --git a/laws/RelativeTargetRejected.bend b/laws/RelativeTargetRejected.bend new file mode 100644 index 0000000..1650683 --- /dev/null +++ b/laws/RelativeTargetRejected.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §3.2.1): a request-target without a leading `/` is not +# origin-form +law ask_rejects_relative_target: + {Http.ask("GET relative HTTP/1.1\r\nHost: h\r\n\r\n") + == Http.Bad{"the request-target is not in origin-form"} : Http.Request} diff --git a/laws/ReplyHeadFramesBody.bend b/laws/ReplyHeadFramesBody.bend new file mode 100644 index 0000000..e0a83d6 --- /dev/null +++ b/laws/ReplyHeadFramesBody.bend @@ -0,0 +1,42 @@ +# For every status, header fields and count `n`, the head of a reply whose +# body is `n` octets (`respond.head`) is `HTTP/1.1 ` CRLF +# (laws/ReasonPhrasesFollowRfc.bend), `Connection: close` CRLF, the header +# lines, `Content-Length: ` CRLF when `n` is not 0, and CRLF; a packed +# octets reply is that head and then its octets unchanged (trusted in +# src/server.bend), and a text reply is exactly the octets reply of its +# body's UTF-8 octets (`respond` is `respond.head` onto the body). +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §4, §6.3): the head of a reply of n octets +law respond_head: + for +status: U32 + for hs: List<&2, Http.Header> + for +n: Nat + {Http.respond.head(status, hs, n) + == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" + ++ "Connection: close\r\n" + ++ Http.headers.lines(hs) + ++ Http.clen.line(n, Nat.is_eq(n, 0n)) + ++ "\r\n" ++ "" : String} + +# LAW: a text reply is the head of its UTF-8 octets onto the body +law respond_text_is: + for +status: U32 + for hs: List<&2, Http.Header> + for +body: String + {Http.respond(status, hs, body) + == Http.respond.head.onto(status, hs, Http.utf8.len(body), body) : String} + +# LAW: the head written onto what follows it is the head, then that +law respond_head_onto: + for +status: U32 + for hs: List<&2, Http.Header> + for +n: Nat + for rest: String + {Http.respond.head.onto(status, hs, n, rest) + == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" + ++ "Connection: close\r\n" + ++ Http.headers.lines(hs) + ++ Http.clen.line(n, Nat.is_eq(n, 0n)) + ++ "\r\n" ++ rest : String} diff --git a/laws/RequestBodyOfContentLengthKept.bend b/laws/RequestBodyOfContentLengthKept.bend new file mode 100644 index 0000000..0c24653 --- /dev/null +++ b/laws/RequestBodyOfContentLengthKept.bend @@ -0,0 +1,11 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §8.6): a request body whose Content-Length matches comes back +law ask_clen_exact: + for +method: String + for +target: String + for hs: List<&2, Http.Header> + for +body: String + {Http.ask.clen(method, target, hs, Http.octs.len(body), body) + == Http.Request{method, target, hs, body} : Http.Request} diff --git a/laws/RequestDisagreeingContentLengthsRejected.bend b/laws/RequestDisagreeingContentLengthsRejected.bend new file mode 100644 index 0000000..b5bd35f --- /dev/null +++ b/laws/RequestDisagreeingContentLengthsRejected.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §6.3): Content-Length values that differ are a bad request +law ask_rejects_disagreeing_cl: + {Http.ask("GET / HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 2\r\n\r\nab") + == Http.Bad{"Content-Length values disagree"} : Http.Request} diff --git a/laws/RequestEqualContentLengthsFrameOneBody.bend b/laws/RequestEqualContentLengthsFrameOneBody.bend new file mode 100644 index 0000000..870d220 --- /dev/null +++ b/laws/RequestEqualContentLengthsFrameOneBody.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §6.3): identical request Content-Length values frame one body +law ask_equal_content_lengths: + {Http.ask("POST / HTTP/1.1\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\nhello!!") + == Http.Request{"POST", "/", [Http.H{"Content-Length", "5"}, + Http.H{"Content-Length", "5"}], "hello"} : Http.Request} diff --git a/laws/RequestHeaderFieldsOrdered.bend b/laws/RequestHeaderFieldsOrdered.bend new file mode 100644 index 0000000..03567cd --- /dev/null +++ b/laws/RequestHeaderFieldsOrdered.bend @@ -0,0 +1,203 @@ +# A field name is compared case-insensitively (RFC 9110 §5.1): it is Host, +# User-Agent, Accept, Connection or Content-Length exactly when its +# lower-case form is `host`, `user-agent`, `accept`, `connection` or +# `content-length`. For every header list and body, the request's header +# fields are, in order, its Host, User-Agent and Accept fields, `Connection: +# close`, the caller's other fields, then its Content-Length field. For each +# of Host, User-Agent and Accept, the request's fields of that name are +# exactly the caller's fields of that name, unchanged and in order, when the +# caller gave one or more (so exactly one, the caller's, when it gave one), +# and exactly the default (`Host: ` as +# laws/RequestLineAndHost.bend writes it, `User-Agent: ezhttp`, `Accept: +# */*`) when it gave none. Connection and Content-Length are not +# overridable: the request's Connection fields are exactly `Connection: +# close`, and its Content-Length fields exactly `Content-Length: ` when the body is not empty and none when it is empty; a +# caller's field of either name is not sent. The caller's fields of every +# other name are sent unchanged and in order. A caller that names none of +# the five gets exactly the request ezhttp 0.12 wrote. +import Base +import ../src/http.bend as Http + +# --- the request's header fields --- + +# LAW (RFC 9110 §5.1): a field name whose lower-case form is `host` is Host +law field_host: + for +name: String + for e: {"host" == String.to_lower(name) : String} + {Http.field.name(name) == Http.FHost{} : Http.Field} + +# LAW (RFC 9110 §5.1): a field name whose lower-case form is `user-agent` is +# User-Agent +law field_agent: + for +name: String + for e: {"user-agent" == String.to_lower(name) : String} + {Http.field.name(name) == Http.FAgent{} : Http.Field} + +# LAW (RFC 9110 §5.1): a field name whose lower-case form is `accept` is +# Accept +law field_accept: + for +name: String + for e: {"accept" == String.to_lower(name) : String} + {Http.field.name(name) == Http.FAccept{} : Http.Field} + +# LAW (RFC 9110 §5.1): a field name whose lower-case form is `connection` is +# Connection +law field_conn: + for +name: String + for e: {"connection" == String.to_lower(name) : String} + {Http.field.name(name) == Http.FConn{} : Http.Field} + +# LAW (RFC 9110 §5.1): a field name whose lower-case form is +# `content-length` is Content-Length +law field_clen: + for +name: String + for e: {"content-length" == String.to_lower(name) : String} + {Http.field.name(name) == Http.FClen{} : Http.Field} + +# LAW (RFC 9110 §5.1): a field name whose lower-case form is none of the five +# is another field +law field_other: + for +name: String + for eh: {False{} == String.eq(String.to_lower(name), "host") : Bool} + for ea: {False{} == String.eq(String.to_lower(name), "user-agent") : Bool} + for ec: {False{} == String.eq(String.to_lower(name), "accept") : Bool} + for en: {False{} == String.eq(String.to_lower(name), "connection") : Bool} + for el: {False{} == String.eq(String.to_lower(name), "content-length") + : Bool} + {Http.field.name(name) == Http.FOther{} : Http.Field} + +# LAW (RFC 9112 §3): the request is its request line, its header fields, a +# blank line and the body +law request_text: + for +method: String + for +host: String + for +path: String + for hs: List<&2, Http.Header> + for +body: String + {Http.request(method, host, path, hs, body) + == method ++ " " ++ path ++ " HTTP/1.1\r\n" + ++ Http.headers.lines(Http.request.fields(host, hs, body)) + ++ "\r\n" + ++ body + : String} + +# LAW: a caller's Host fields replace the default: the request's Host fields +# are exactly the caller's, in order +law fields_host_caller: + for +host: String + for +hs: List<&2, Http.Header> + for +body: String + for +h: Http.Header + for +t: List<&2, Http.Header> + for +e: {Http.fields.of(Http.FHost{}, hs) == h <> t : List<&2, Http.Header>} + {Http.fields.of(Http.FHost{}, Http.request.fields(host, hs, body)) + == h <> t : List<&2, Http.Header>} + +# LAW (RFC 9110 §7.2): with no Host field from the caller, the request's Host +# fields are exactly the default one +law fields_host_dflt: + for +host: String + for +hs: List<&2, Http.Header> + for +body: String + for +e: {Http.fields.of(Http.FHost{}, hs) == [] : List<&2, Http.Header>} + {Http.fields.of(Http.FHost{}, Http.request.fields(host, hs, body)) + == [Http.H{"Host", host}] : List<&2, Http.Header>} + +# LAW: a caller's User-Agent fields replace the default: the request's +# User-Agent fields are exactly the caller's, in order +law fields_agent_caller: + for +host: String + for +hs: List<&2, Http.Header> + for +body: String + for +h: Http.Header + for +t: List<&2, Http.Header> + for +e: {Http.fields.of(Http.FAgent{}, hs) == h <> t : List<&2, Http.Header>} + {Http.fields.of(Http.FAgent{}, Http.request.fields(host, hs, body)) + == h <> t : List<&2, Http.Header>} + +# LAW: with no User-Agent field from the caller, the request's User-Agent +# fields are exactly `User-Agent: ezhttp` +law fields_agent_dflt: + for +host: String + for +hs: List<&2, Http.Header> + for +body: String + for +e: {Http.fields.of(Http.FAgent{}, hs) == [] : List<&2, Http.Header>} + {Http.fields.of(Http.FAgent{}, Http.request.fields(host, hs, body)) + == [Http.H{"User-Agent", "ezhttp"}] : List<&2, Http.Header>} + +# LAW: a caller's Accept fields replace the default: the request's Accept +# fields are exactly the caller's, in order +law fields_accept_caller: + for +host: String + for +hs: List<&2, Http.Header> + for +body: String + for +h: Http.Header + for +t: List<&2, Http.Header> + for +e: {Http.fields.of(Http.FAccept{}, hs) == h <> t + : List<&2, Http.Header>} + {Http.fields.of(Http.FAccept{}, Http.request.fields(host, hs, body)) + == h <> t : List<&2, Http.Header>} + +# LAW: with no Accept field from the caller, the request's Accept fields are +# exactly `Accept: */*` +law fields_accept_dflt: + for +host: String + for +hs: List<&2, Http.Header> + for +body: String + for +e: {Http.fields.of(Http.FAccept{}, hs) == [] : List<&2, Http.Header>} + {Http.fields.of(Http.FAccept{}, Http.request.fields(host, hs, body)) + == [Http.H{"Accept", "*/*"}] : List<&2, Http.Header>} + +# LAW (RFC 9112 §9.6): whatever the caller gave, the request's Connection +# fields are exactly `Connection: close` +law fields_conn: + for +host: String + for +hs: List<&2, Http.Header> + for +body: String + {Http.fields.of(Http.FConn{}, Http.request.fields(host, hs, body)) + == [Http.H{"Connection", "close"}] : List<&2, Http.Header>} + +# LAW (RFC 9110 §8.6): whatever the caller gave, the request's Content-Length +# fields are exactly the one computed from the body +law fields_clen: + for +host: String + for +hs: List<&2, Http.Header> + for +body: String + {Http.fields.of(Http.FClen{}, Http.request.fields(host, hs, body)) + == Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)) + : List<&2, Http.Header>} + +# LAW: the caller's fields of every other name are sent unchanged and in +# order, and the request adds none of its own +law fields_other: + for +host: String + for +hs: List<&2, Http.Header> + for +body: String + {Http.fields.of(Http.FOther{}, Http.request.fields(host, hs, body)) + == Http.fields.of(Http.FOther{}, hs) : List<&2, Http.Header>} + +# LAW (RFC 9110 §7.2, RFC 9112 §3, §9.6): a caller that names none of the +# five defaulted fields gets the request ezhttp has always written: Host, +# User-Agent, Accept, Connection: close, its own fields, Content-Length +law request_plain: + for +method: String + for +host: String + for +path: String + for +hs: List<&2, Http.Header> + for +body: String + for +eh: {Http.fields.of(Http.FHost{}, hs) == [] : List<&2, Http.Header>} + for +ea: {Http.fields.of(Http.FAgent{}, hs) == [] : List<&2, Http.Header>} + for +ec: {Http.fields.of(Http.FAccept{}, hs) == [] : List<&2, Http.Header>} + for +eo: {Http.fields.of(Http.FOther{}, hs) == hs : List<&2, Http.Header>} + {Http.request(method, host, path, hs, body) + == method ++ " " ++ path ++ " HTTP/1.1\r\n" + ++ "Host: " ++ host ++ "\r\n" + ++ "User-Agent: ezhttp\r\n" + ++ "Accept: */*\r\n" + ++ "Connection: close\r\n" + ++ Http.headers.lines(hs) + ++ Http.clen.line(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)) + ++ "\r\n" + ++ body + : String} diff --git a/laws/RequestKeepsFieldNameSpelling.bend b/laws/RequestKeepsFieldNameSpelling.bend new file mode 100644 index 0000000..fd88e37 --- /dev/null +++ b/laws/RequestKeepsFieldNameSpelling.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §5.1): the wire spelling of a field name is kept +law ask_keeps_field_name_spelling: + {Http.ask("GET / HTTP/1.1\r\nhOsT: Ex.COM\r\n\r\n") + == Http.Request{"GET", "/", [Http.H{"hOsT", "Ex.COM"}], ""} : Http.Request} diff --git a/laws/RequestLineAndHost.bend b/laws/RequestLineAndHost.bend new file mode 100644 index 0000000..cab252f --- /dev/null +++ b/laws/RequestLineAndHost.bend @@ -0,0 +1,112 @@ +# `src/wirespec.bend` decides what the client sends, when it has heard a +# whole answer, and what a failed exchange is reported as, and does no IO. +# It judges the answer read by read: the state it carries from one read to +# the next (`Http.hear`, in `src/http.bend`) is advanced by each read's text +# alone, and is proved to decide exactly what judging all the text received +# would (laws/ReadStateMatchesWholeText.bend). `wire.exchange` turns a +# method, a parsed URL, headers and a body into the scheme, host, port, +# method and request text that is sent, and `src/http.bend` decides the +# request's header fields; `src/client.bend` and `src/wire.bend` send it +# over bend-kit-wire's sockets and read the answer as it decides (trusted in +# src/wire.bend, trusted in src/client.bend). The answer is received as +# octets, one character each (laws/OneOctetPerChar.bend). +# +# An exchange's answer is the response's octets, packed, or the reason the +# exchange failed: a non-zero status on its first line, then the reason +# (laws/FailuresReportedByPhase.bend). The client judges each receive by its +# count of octets when that decides it, and by its text otherwise +# (laws/ReadsJudgedByCount.bend), plans the response from its head with +# `src/http.bend`'s decisions (laws/HeadPlannedBeforeBody.bend), and copies +# the body out of the packed octets without turning it into text (trusted in +# src/packed.bend). `http.get_opts`, `http.request_opts`, `http.post_opts`, +# `http.put_opts` and `http.delete_opts` take a timeout in milliseconds, +# which becomes the deadline of every socket call the exchange makes +# (laws/DeadlineIsTimeout.bend, laws/TimeoutZeroIsOneMs.bend). +# +# For every method, scheme, host, port, path, headers and body, the request +# the client writes for that URL is the request line ` +# HTTP/1.1`, its header fields (laws/RequestHeaderFieldsOrdered.bend), a +# blank line and the body, and its default Host field names the host as RFC +# 9110 §7.2 asks: exactly `Host: ` when the port is the scheme's +# default (80 for `http`, 443 for `https`), and exactly `Host: +# :` for every other port. The exchange goes over TLS exactly +# when the scheme is `https`, to that host and port; a URL that does not +# parse sends nothing and is reported with its reason. +import Base +import ../src/http.bend as Http +import ../src/url.bend as Url +import ../src/wirespec.bend as Spec + +# --- the client's requests --- + +# LAW (RFC 9110 §7.2): on port 80 an http request's Host is the host alone +law host_http_default: + for host: String + {Http.authority(False{}, host, 80) == host : String} + +# LAW (RFC 9110 §7.2): on port 443 an https request's Host is the host alone +law host_https_default: + for host: String + {Http.authority(True{}, host, 443) == host : String} + +# LAW (RFC 9110 §7.2): on any other port an http request's Host is +# `host:port` +law host_http_other: + for host: String + for +port: U32 + for e: {False{} == U32.is_eq(port, 80) : Bool} + {Http.authority(False{}, host, port) == host ++ ":" ++ U32.show(port) + : String} + +# LAW (RFC 9110 §7.2): on any other port an https request's Host is +# `host:port` +law host_https_other: + for host: String + for +port: U32 + for e: {False{} == U32.is_eq(port, 443) : Bool} + {Http.authority(True{}, host, port) == host ++ ":" ++ U32.show(port) + : String} + +# LAW (RFC 9110 §7.2, RFC 9112 §3): the request a client writes for a host +# and port is its request line, the header fields request.fields gives for +# that Host value, a blank line and the body +law request_at: + for +method: String + for secure: Bool + for host: String + for port: U32 + for +path: String + for hs: List<&2, Http.Header> + for +body: String + {Http.request.at(method, secure, host, port, path, hs, body) + == method ++ " " ++ path ++ " HTTP/1.1\r\n" + ++ Http.headers.lines( + Http.request.fields(Http.authority(secure, host, port), hs, body)) + ++ "\r\n" + ++ body + : String} + +# LAW: a URL that does not parse sends nothing and keeps its reason +law exchange_bad: + for method: String + for why: String + for hs: List<&2, Http.Header> + for body: String + {Spec.wire.exchange(method, Url.Bad{why}, hs, body) == Spec.NoUrl{why} + : Spec.Exchange} + +# LAW: a URL that parses is sent to its host and port, over TLS exactly when +# its scheme is https, with the method it was given and the request +# request.at writes for them +law exchange_loc: + for method: String + for +scheme: String + for +host: String + for +port: U32 + for path: String + for hs: List<&2, Http.Header> + for body: String + {Spec.wire.exchange(method, Url.Loc{scheme, host, port, path}, hs, body) + == Spec.Exchange{String.eq(scheme, "https"), host, port, method, + Http.request.at(method, String.eq(scheme, "https"), host, port, path, hs, + body)} : Spec.Exchange} diff --git a/laws/RequestLineOpensWithMethod.bend b/laws/RequestLineOpensWithMethod.bend new file mode 100644 index 0000000..d16a2a9 --- /dev/null +++ b/laws/RequestLineOpensWithMethod.bend @@ -0,0 +1,12 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §3.2.1): the request-line opens with the method it was given +law request_names_method: + for +method: String + for +host: String + for +path: String + for hs: List<&2, Http.Header> + for +body: String + {String.starts_with(Http.request(method, host, path, hs, body), method) + == True{} : Bool} diff --git a/laws/RequestNeedsBlankLine.bend b/laws/RequestNeedsBlankLine.bend new file mode 100644 index 0000000..57df137 --- /dev/null +++ b/laws/RequestNeedsBlankLine.bend @@ -0,0 +1,9 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §2.1): a request whose header section never ended is bad +law ask_needs_blank_line: + for +raw: String + for e: {Http.NoCut{} == Http.divide(raw, "\r\n\r\n") : Http.Cut} + {Http.ask(raw) == Http.Bad{"the request head has no blank line after it"} + : Http.Request} diff --git a/laws/RequestNeedsRequestLine.bend b/laws/RequestNeedsRequestLine.bend new file mode 100644 index 0000000..e7a501b --- /dev/null +++ b/laws/RequestNeedsRequestLine.bend @@ -0,0 +1,8 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §3): an empty request-line list is bad +law ask_needs_request_line: + for body: String + {Http.ask.head([], body) == Http.Bad{"the request has no request line"} + : Http.Request} diff --git a/laws/RequestWithBothFramingsRejected.bend b/laws/RequestWithBothFramingsRejected.bend new file mode 100644 index 0000000..5b05cc6 --- /dev/null +++ b/laws/RequestWithBothFramingsRejected.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §6.3): a request with both framings is bad +law ask_rejects_cl_and_te: + {Http.ask("POST / HTTP/1.1\r\nContent-Length: 1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n") + == Http.Bad{"Content-Length and Transfer-Encoding conflict"} : Http.Request} diff --git a/laws/Respond200KeepsBody.bend b/laws/Respond200KeepsBody.bend new file mode 100644 index 0000000..27d6856 --- /dev/null +++ b/laws/Respond200KeepsBody.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §15.3.1): a 200 response keeps its body +law respond_200_keeps_body: + {Http.respond.of(Http.Reply{200, [], "ok"}) == Http.respond(200, [], "ok") + : String} diff --git a/laws/Respond204DropsBody.bend b/laws/Respond204DropsBody.bend new file mode 100644 index 0000000..ad442ea --- /dev/null +++ b/laws/Respond204DropsBody.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §15.3.5): a 204 response is written with an empty body +law respond_204_drops_body: + {Http.respond.of(Http.Reply{204, [], "nope"}) == Http.respond(204, [], "") + : String} diff --git a/laws/Respond304DropsBody.bend b/laws/Respond304DropsBody.bend new file mode 100644 index 0000000..b47a56b --- /dev/null +++ b/laws/Respond304DropsBody.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §15.4.5): a 304 response is written with an empty body +law respond_304_drops_body: + {Http.respond.of(Http.Reply{304, [], "nope"}) == Http.respond(304, [], "") + : String} diff --git a/laws/RespondOpensWithVersion.bend b/laws/RespondOpensWithVersion.bend new file mode 100644 index 0000000..abce0e8 --- /dev/null +++ b/laws/RespondOpensWithVersion.bend @@ -0,0 +1,10 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §4): a response message opens with HTTP/1.1 +law respond_opens_with_version: + for +status: U32 + for hs: List<&2, Http.Header> + for +body: String + {String.starts_with(Http.respond(status, hs, body), "HTTP/1.1 ") + == True{} : Bool} diff --git a/laws/ResponseCompleteByFraming.bend b/laws/ResponseCompleteByFraming.bend new file mode 100644 index 0000000..8da3c52 --- /dev/null +++ b/laws/ResponseCompleteByFraming.bend @@ -0,0 +1,231 @@ +# For every request method and every text received so far, the response is +# complete exactly when its header section has ended (RFC 9112 §2.1) and +# then: when the head's status is 1xx it is an interim response (RFC 9110 +# §15.2), and the response is complete exactly when the text after that head +# is, read the same way (so never while nothing follows it), at most as many +# heads deep as the text has characters; otherwise its framing (RFC 9112 +# §6.3) says the body has arrived: the request was `HEAD` or the status is +# 204 or 304, so there is no body; otherwise Transfer-Encoding names +# `chunked` and the body has reached the end of its last chunk, read chunk +# by chunk (each a hexadecimal size, CRLF, that many octets, CRLF), meaning +# a zero size followed by an ended trailer section, or a size that is not +# hexadecimal (the parse then rejects it); otherwise every Content-Length +# value is the same decimal `n` and the body holds at least `n` octets. A +# response with none of these framings (no Content-Length, or values that +# disagree or do not parse) is never complete before the server closes the +# connection. +# +# also rests on: laws/InterimStatusIs1xx.bend +import Base +import ../src/http.bend as Http + +# --- when a response is whole --- + +# LAW: a text is read head by head for the request's method, at most as many +# heads as it has characters +law whole_go: + for +method: String + for +raw: String + {Http.whole(method, raw) + == Http.whole.go(String.length(raw), String.eq(method, "HEAD"), + Http.divide(raw, "\r\n\r\n")) : Bool} + +# LAW: a read with no heads left is not whole +law whole_fuel: + for asked: Bool + for c: Http.Cut + {Http.whole.go(0n, asked, c) == False{} : Bool} + +# LAW (RFC 9112 §2.1): a response whose header section has not ended is not +# whole +law whole_open: + for f: Nat + for asked: Bool + {Http.whole.go(1n+f, asked, Http.NoCut{}) == False{} : Bool} + +# LAW (RFC 9110 §15.2): after an interim (1xx) head, the response is whole +# exactly when the text after that head is +law whole_interim: + for f: Nat + for +asked: Bool + for +before: String + for +after: String + for e: {True{} == Http.status.interim(Http.status.of(String.lines(before))) + : Bool} + {Http.whole.go(1n+f, asked, Http.Cut{before, after}) + == Http.whole.go(f, asked, Http.divide(after, "\r\n\r\n")) : Bool} + +# LAW (RFC 9112 §6.3): once a final header section has ended, a response is +# whole exactly when its body is, under the framing its head and the +# request's method give +law whole_head: + for f: Nat + for +asked: Bool + for +before: String + for +after: String + for e: {False{} == Http.status.interim(Http.status.of(String.lines(before))) + : Bool} + {Http.whole.go(1n+f, asked, Http.Cut{before, after}) + == Http.whole.of(Http.frame(Bool.or(asked, + Http.status.bare(Http.status.of(String.lines(before)))), + Http.head.go(String.lines(before), "transfer-encoding"), + Http.head.values(String.lines(before), "content-length")), after) + : Bool} + +# LAW (RFC 9112 §4): a status line's code is its second word, as a number +law status_of: + for ls: List<&2, String> + {Http.status.of(ls) == U32.read(Http.second(String.split(Http.first(ls), ' '))) + : Maybe<&2, U32>} + +# LAW (RFC 9110 §15.3.5, §15.4.5): a 204 or a 304 has no body +law status_bare_some: + for +n: U32 + {Http.status.bare(Some{n}) == Bool.or(U32.is_eq(n, 204), U32.is_eq(n, 304)) + : Bool} + +# LAW: a status line that does not parse leaves the framing to the headers +law status_bare_none: + for m: Maybe<&2, U32> + for e: {None{} == m : Maybe<&2, U32>} + {Http.status.bare(m) == False{} : Bool} + +# LAW (RFC 9110 §9.3.2, RFC 9112 §6.3 item 1): a reply to HEAD, a 204 and a +# 304 have no body, whatever their fields say +law frame_bare: + for te: String + for lens: List<&2, String> + {Http.frame(True{}, te, lens) == Http.Bare{} : Http.Frame} + +# LAW (RFC 9112 §6.3 items 3, 4): Transfer-Encoding chunked frames the body, +# Content-Length or not +law frame_chunked: + for te: String + for +lens: List<&2, String> + for e: {True{} == String.contains(String.to_lower(te), "chunked") : Bool} + {Http.frame(False{}, te, lens) == Http.Chunked{} : Http.Frame} + +# LAW (RFC 9112 §6.3 item 6): otherwise Content-Length values that agree on +# a number frame the body at that many octets +law frame_sized: + for te: String + for +lens: List<&2, String> + for e1: {False{} == String.contains(String.to_lower(te), "chunked") : Bool} + for e2: {True{} == Http.clen.agree(lens) : Bool} + for n: Nat + for e3: {Some{n} == Http.digits.read(Http.first(lens)) : Maybe<&2, Nat>} + {Http.frame(False{}, te, lens) == Http.Sized{n} : Http.Frame} + +# LAW (RFC 9112 §6.3 items 5, 8): Content-Length values that disagree leave +# the body to the close +law frame_disagree: + for te: String + for +lens: List<&2, String> + for e1: {False{} == String.contains(String.to_lower(te), "chunked") : Bool} + for e2: {False{} == Http.clen.agree(lens) : Bool} + {Http.frame(False{}, te, lens) == Http.Closed{} : Http.Frame} + +# LAW (RFC 9112 §6.3 item 8): no Content-Length, or one that is not a +# number, leaves the body to the close +law frame_unsized: + for te: String + for +lens: List<&2, String> + for e1: {False{} == String.contains(String.to_lower(te), "chunked") : Bool} + for e2: {True{} == Http.clen.agree(lens) : Bool} + for e3: {None{} == Http.digits.read(Http.first(lens)) : Maybe<&2, Nat>} + {Http.frame(False{}, te, lens) == Http.Closed{} : Http.Frame} + +# LAW: a response with no body is whole as soon as its head is +law whole_bare: + for +body: String + {Http.whole.of(Http.Bare{}, body) == True{} : Bool} + +# LAW (RFC 9110 §8.6): a sized body is whole once it holds that many octets +law whole_sized: + for n: Nat + for +body: String + {Http.whole.of(Http.Sized{n}, body) + == Bool.not(Nat.is_lt(Http.octs.len(body), n)) : Bool} + +# LAW (RFC 9112 §7.1): a chunked body is whole once it reaches the end of its +# last chunk, read from its first chunk header +law whole_chunked: + for +body: String + {Http.whole.of(Http.Chunked{}, body) + == Http.chunks.ended(String.length(body), Http.divide(body, "\r\n")) + : Bool} + +# LAW (RFC 9112 §6.3 item 8): a body framed by the close is never whole +# before the close +law whole_closed: + for +body: String + {Http.whole.of(Http.Closed{}, body) == False{} : Bool} + +# LAW: a chunk walk with no fuel left has not ended +law ended_fuel: + for c: Http.Cut + {Http.chunks.ended(0n, c) == False{} : Bool} + +# LAW (RFC 9112 §7.1): a chunk header that has not all arrived has not ended +law ended_open: + for f: Nat + {Http.chunks.ended(1n+f, Http.NoCut{}) == False{} : Bool} + +# LAW (RFC 9112 §7.1): a chunk size that is not hexadecimal ends the read, +# and the parse rejects it +law ended_bad: + for f: Nat + for +before: String + for +after: String + for e: {False{} == Http.hex.well(Http.chunks.token(before)) : Bool} + {Http.chunks.ended(1n+f, Http.Cut{before, after}) == True{} : Bool} + +# LAW (RFC 9112 §7.1.2): the zero-size chunk ends the body once its trailer +# section has ended +law ended_last: + for f: Nat + for +before: String + for +after: String + for e1: {True{} == Http.hex.well(Http.chunks.token(before)) : Bool} + for e2: {True{} == Nat.is_eq(Http.hexlen(before), 0n) : Bool} + {Http.chunks.ended(1n+f, Http.Cut{before, after}) + == Http.chunks.trailer(after) : Bool} + +# LAW (RFC 9112 §7.1): a chunk that has all arrived, with its CRLF, moves on +# to the next chunk header +law ended_next: + for f: Nat + for +before: String + for +after: String + for e1: {True{} == Http.hex.well(Http.chunks.token(before)) : Bool} + for e2: {False{} == Nat.is_eq(Http.hexlen(before), 0n) : Bool} + for e3: {True{} == Http.chunks.ready(after, Http.hexlen(before)) : Bool} + {Http.chunks.ended(1n+f, Http.Cut{before, after}) + == Http.chunks.ended(f, Http.divide(Http.octs.drop(after, + Nat.add(Http.hexlen(before), 2n)), "\r\n")) : Bool} + +# LAW (RFC 9112 §7.1): a chunk that has not all arrived has not ended +law ended_short: + for f: Nat + for +before: String + for +after: String + for e1: {True{} == Http.hex.well(Http.chunks.token(before)) : Bool} + for e2: {False{} == Nat.is_eq(Http.hexlen(before), 0n) : Bool} + for e3: {False{} == Http.chunks.ready(after, Http.hexlen(before)) : Bool} + {Http.chunks.ended(1n+f, Http.Cut{before, after}) == False{} : Bool} + +# LAW (RFC 9112 §7.1.2): a trailer section has ended at once (CRLF), or after +# its fields (a blank line) +law trailer_ended: + for +after: String + {Http.chunks.trailer(after) == Bool.or(String.starts_with(after, "\r\n"), + String.contains(after, "\r\n\r\n")) : Bool} + +# LAW (RFC 9112 §7.1): a chunk of size n has arrived when n octets and a +# CRLF follow its header +law chunk_ready: + for +after: String + for +n: Nat + {Http.chunks.ready(after, n) + == Bool.and(Bool.not(Nat.is_lt(Http.octs.len(after), Nat.add(n, 2n))), + String.starts_with(Http.octs.drop(after, n), "\r\n")) : Bool} diff --git a/laws/ResponseDisagreeingContentLengthsTorn.bend b/laws/ResponseDisagreeingContentLengthsTorn.bend new file mode 100644 index 0000000..f785fc1 --- /dev/null +++ b/laws/ResponseDisagreeingContentLengthsTorn.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §6.3): a response whose Content-Length values differ is torn +law parse_rejects_disagreeing_cl: + {Http.parse("HTTP/1.1 200 OK\r\nContent-Length: 1\r\nContent-Length: 2\r\n\r\nab") + == Http.Torn{"Content-Length values disagree"} : Http.Reply} diff --git a/laws/ResponseNeedsBlankLine.bend b/laws/ResponseNeedsBlankLine.bend new file mode 100644 index 0000000..de2c1c4 --- /dev/null +++ b/laws/ResponseNeedsBlankLine.bend @@ -0,0 +1,9 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §2.1): a response whose header section never ended is torn +law parse_needs_blank_line: + for +raw: String + for e: {Http.NoCut{} == Http.divide(raw, "\r\n\r\n") : Http.Cut} + {Http.parse(raw) == Http.Torn{"the response head has no blank line after it"} + : Http.Reply} diff --git a/laws/ResponseNeedsStatus.bend b/laws/ResponseNeedsStatus.bend new file mode 100644 index 0000000..66ec64c --- /dev/null +++ b/laws/ResponseNeedsStatus.bend @@ -0,0 +1,12 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §4): a head whose first line holds no status code is torn +law parse_needs_status: + for +ls: List<&2, String> + for body: String + for e: {None{} == U32.read(Http.second(String.split(Http.first(ls), ' '))) + : Maybe<&2, U32>} + {Http.parse.head(ls, body) + == Http.Torn{"the response has no status line"} + : Http.Reply} diff --git a/laws/ResponseWithBothFramingsTorn.bend b/laws/ResponseWithBothFramingsTorn.bend new file mode 100644 index 0000000..b6cf2d7 --- /dev/null +++ b/laws/ResponseWithBothFramingsTorn.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §6.3): Content-Length and Transfer-Encoding together are torn +law parse_rejects_cl_and_te: + {Http.parse("HTTP/1.1 200 OK\r\nContent-Length: 1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n") + == Http.Torn{"Content-Length and Transfer-Encoding conflict"} : Http.Reply} diff --git a/laws/SchemeBeforeFirstColon.bend b/laws/SchemeBeforeFirstColon.bend new file mode 100644 index 0000000..e3bd3ec --- /dev/null +++ b/laws/SchemeBeforeFirstColon.bend @@ -0,0 +1,11 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.1): the scheme is everything before the first colon +law parse_scheme_first: + for +url: String + for h: String + for t: List<&2, String> + for e: {h <> t == String.split(url, ':') : List<&2, String>} + {Url.parse(url) == Url.parse.rest(String.to_lower(h), String.join(t, ":")) + : Url.Loc} diff --git a/laws/SecondIsNextWord.bend b/laws/SecondIsNextWord.bend new file mode 100644 index 0000000..acfca9b --- /dev/null +++ b/laws/SecondIsNextWord.bend @@ -0,0 +1,12 @@ +import Base +import ../src/http.bend as Http +import ./defs/CharacterAbsent.bend as D1 + +# LAW: the second piece is the first word of what follows (status code on +# `HTTP/1.1 200 OK`, RFC 9112 §4) +law second_is_next_word: + for +w: String + for r: String + for +e: {D1.avoids(w, ' ') == True{} : Bool} + {Http.second(String.split(String.append(w, SCon{' ', r}), ' ')) + == Http.first(String.split(r, ' ')) : String} diff --git a/laws/SetCookieFieldsSelected.bend b/laws/SetCookieFieldsSelected.bend new file mode 100644 index 0000000..1b45b44 --- /dev/null +++ b/laws/SetCookieFieldsSelected.bend @@ -0,0 +1,32 @@ +# For every list of header fields, `cookie_from_headers` reads the values of +# exactly the fields named Set-Cookie (compared case-insensitively, RFC 9110 +# §5.1), in order. +import Base +import ../src/http.bend as Http +import ../src/cookie.bend as Cookie +import ../src/echo.bend as Echo + +# LAW: no header fields, no Set-Cookie values +law ck_values_nil: + for hs: List<&2, Http.Header> + for e: {[] == hs : List<&2, Http.Header>} + {Echo.values(hs) == [] : List<&2, String>} + +# LAW (RFC 9110 §5.1): a field named Set-Cookie in any case gives its value, +# in order +law ck_values_hit: + for +name: String + for value: String + for t: List<&2, Http.Header> + for e: {True{} == String.eq(String.to_lower(name), "set-cookie") : Bool} + {Echo.values(Http.H{name, value} <> t) == value <> Echo.values(t) + : List<&2, String>} + +# LAW: any other field gives nothing +law ck_values_miss: + for +name: String + for value: String + for t: List<&2, Http.Header> + for e: {False{} == String.eq(String.to_lower(name), "set-cookie") : Bool} + {Echo.values(Http.H{name, value} <> t) == Echo.values(t) + : List<&2, String>} diff --git a/laws/SetCookieParsesBack.bend b/laws/SetCookieParsesBack.bend new file mode 100644 index 0000000..eca0bb4 --- /dev/null +++ b/laws/SetCookieParsesBack.bend @@ -0,0 +1,9 @@ +import Base +import ../src/cookie.bend as Cookie + +# LAW (RFC 6265 §4.1.1): Set-Cookie parses back to those attributes +law cookie_parse_round_trip: + {Cookie.cookie.parse("a=b; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Max-Age=12; Domain=example.com; Path=/app; Secure; HttpOnly; SameSite=Lax") + == Some{Cookie.Cookie{"a", "b", "Wed, 21 Oct 2015 07:28:00 GMT", "12", + "example.com", "/app", True{}, True{}, "Lax", False{}}} + : Maybe<&2, Cookie.Cookie>} diff --git a/laws/SetCookieReadsPair.bend b/laws/SetCookieReadsPair.bend new file mode 100644 index 0000000..a821019 --- /dev/null +++ b/laws/SetCookieReadsPair.bend @@ -0,0 +1,82 @@ +# For every Set-Cookie field value (RFC 6265 §5.2): its name-value-pair is +# the text before its first `;`, all of it when it has none (step 1); a pair +# with no `=` sets nothing (step 2); otherwise the name is the text before +# its first `=` and the value the text after it (step 3), each with its +# leading and trailing SP and HTAB removed (step 4); an empty name sets +# nothing (step 5). +# +# also rests on: laws/CutAtFirstEquals.bend, laws/SplitAtCharacter.bend +import Base +import ../src/http.bend as Http +import ../src/cookie.bend as Cookie +import ../src/qs.bend as Qs +import ../src/echo.bend as Echo + +# --- cookies sent back --- + +# LAW (RFC 6265 §5.2 step 1): a Set-Cookie value's name-value-pair is the +# text before its first `;`, cut at its first `=` +law ck_pair: + for line: String + {Echo.pair(line) == Echo.pair.cut(Qs.cut(Http.first(String.split(line, ';')))) + : Maybe<&2, Qs.Kv()>} + +# LAW (§5.2 step 2): a pair with no `=` sets nothing +law ck_pair_none: + for u: Maybe<&2, Qs.Kv()> + for e: {None{} == u : Maybe<&2, Qs.Kv()>} + {Echo.pair.cut(u) == None{} : Maybe<&2, Qs.Kv()>} + +# LAW (§5.2 steps 3, 4): the name and the value without leading and trailing +# WSP +law ck_pair_some: + for before: String + for after: String + {Echo.pair.cut(Some{(before, after)}) + == Echo.pair.named(Echo.wsp.trim(before), Echo.wsp.trim(after), + String.is_empty(Echo.wsp.trim(before))) : Maybe<&2, Qs.Kv()>} + +# LAW (§5.2 step 5): an empty name sets nothing +law ck_named_empty: + for name: String + for value: String + {Echo.pair.named(name, value, True{}) == None{} : Maybe<&2, Qs.Kv()>} + +# LAW: any other name sets the cookie +law ck_named_kept: + for name: String + for value: String + {Echo.pair.named(name, value, False{}) == Some{(name, value)} + : Maybe<&2, Qs.Kv()>} + +# LAW: trimming strips leading WSP, then (reversed) trailing WSP +law ck_trim: + for s: String + {Echo.wsp.trim(s) + == String.reverse(Echo.wsp.lead(String.reverse(Echo.wsp.lead(s)))) + : String} + +# LAW: the empty string has no WSP to strip +law ck_lead_nil: + for s: String + for e: {"" == s : String} + {Echo.wsp.lead(s) == "" : String} + +# LAW: a leading WSP character is dropped +law ck_lead_wsp: + for +h: Char + for +t: String + for e: {True{} == Echo.wsp(h) : Bool} + {Echo.wsp.lead(SCon{h, t}) == Echo.wsp.lead(t) : String} + +# LAW: stripping stops at the first other character +law ck_lead_other: + for +h: Char + for +t: String + for e: {False{} == Echo.wsp(h) : Bool} + {Echo.wsp.lead(SCon{h, t}) == SCon{h, t} : String} + +# LAW (RFC 5234 WSP): whitespace is SP or HTAB +law ck_wsp: + for +c: Char + {Echo.wsp(c) == Bool.or(Char.is_eq(c, ' '), Char.is_eq(c, '\t')) : Bool} diff --git a/laws/ShortRequestBodyIsBad.bend b/laws/ShortRequestBodyIsBad.bend new file mode 100644 index 0000000..b7bd01c --- /dev/null +++ b/laws/ShortRequestBodyIsBad.bend @@ -0,0 +1,14 @@ +import Base +import ../src/http.bend as Http + +# LAW: a request body shorter than Content-Length is bad +law ask_clen_short: + for +method: String + for +target: String + for hs: List<&2, Http.Header> + for +n: Nat + for +body: String + for e: {True{} == Nat.is_lt(Http.octs.len(body), n) : Bool} + {Http.ask.clen(method, target, hs, n, body) + == Http.Bad{"the request body stopped before its Content-Length"} + : Http.Request} diff --git a/laws/ShowReplyOpensWithStatus.bend b/laws/ShowReplyOpensWithStatus.bend new file mode 100644 index 0000000..86dce88 --- /dev/null +++ b/laws/ShowReplyOpensWithStatus.bend @@ -0,0 +1,10 @@ +import Base +import ../src/http.bend as Http + +# LAW: a reply opens with the status it holds +law show_reply_opens_with_status: + for +status: U32 + for hs: List<&2, Http.Header> + for body: String + {String.starts_with(Http.show(Http.Reply{status, hs, body}), U32.show(status)) + == True{} : Bool} diff --git a/laws/ShowTornIsMarked.bend b/laws/ShowTornIsMarked.bend new file mode 100644 index 0000000..99b9019 --- /dev/null +++ b/laws/ShowTornIsMarked.bend @@ -0,0 +1,7 @@ +import Base +import ../src/http.bend as Http + +# LAW: a torn response says so before it says anything else +law show_torn_is_marked: + for why: String + {String.starts_with(Http.show(Http.Torn{why}), "torn ") == True{} : Bool} diff --git a/laws/SplitAtCharacter.bend b/laws/SplitAtCharacter.bend new file mode 100644 index 0000000..0448503 --- /dev/null +++ b/laws/SplitAtCharacter.bend @@ -0,0 +1,21 @@ +# `String.split` cuts a string at every occurrence of a character: a string +# that holds it nowhere is one piece, and one that holds it first at a +# known place splits there. +import Base +import ./defs/CharacterAbsent.bend as D1 + +# LAW: a string that holds the character nowhere splits into itself +law split_whole: + for +s: String + for +c: Char + for +e: {D1.avoids(s, c) == True{} : Bool} + {String.split(s, c) == [s] : List<&2, String>} + +# LAW: a string that holds the character first at a known place splits there +law split_cut: + for +s: String + for +c: Char + for +r: String + for +e: {D1.avoids(s, c) == True{} : Bool} + {String.split(String.append(s, SCon{c, r}), c) == s <> String.split(r, c) + : List<&2, String>} diff --git a/laws/StaleAtMaxAge.bend b/laws/StaleAtMaxAge.bend new file mode 100644 index 0000000..799b3e6 --- /dev/null +++ b/laws/StaleAtMaxAge.bend @@ -0,0 +1,6 @@ +import Base +import ../src/cache.bend as Cache + +# LAW (RFC 9111 §4.2.1): an age equal to max-age is stale +law fresh_max_age_limit: + {Cache.cache.fresh(Cache.cache.parse("max-age=10"), 10n) == False{} : Bool} diff --git a/laws/StreamedRepliesAreChunked.bend b/laws/StreamedRepliesAreChunked.bend new file mode 100644 index 0000000..6ab5c1a --- /dev/null +++ b/laws/StreamedRepliesAreChunked.bend @@ -0,0 +1,70 @@ +# For every status and header fields, a streamed reply's head +# (`respond.stream`) is the status line, `Connection: close` CRLF and the +# header lines, then `Transfer-Encoding: chunked` CRLF unless the status is +# 204 or 304 (RFC 9112 §6.1), then CRLF; each chunk of `n` octets is written +# (`chunk.frame`) as its size in 8 lowercase hexadecimal digits +# (`chunk.line`) and CRLF, its octets, and CRLF, and a chunk of 0 octets +# writes nothing, since it would end the body; the body ends with `0` CRLF +# CRLF (RFC 9112 §7.1). +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §7.1): a streamed reply with a body is chunked +law stream_head: + for +status: U32 + for e: {False{} == Http.status.nocontent(status) : Bool} + for hs: List<&2, Http.Header> + {Http.respond.stream(status, hs) + == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" + ++ "Connection: close\r\n" ++ Http.headers.lines(hs) + ++ "Transfer-Encoding: chunked\r\n" ++ "\r\n" : String} + +# LAW (RFC 9112 §6.1): a streamed 204 or 304 sends no Transfer-Encoding +law stream_head_bare: + for +status: U32 + for e: {True{} == Http.status.nocontent(status) : Bool} + for hs: List<&2, Http.Header> + {Http.respond.stream(status, hs) + == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" + ++ "Connection: close\r\n" ++ Http.headers.lines(hs) ++ "" + ++ "\r\n" : String} + +# LAW: a chunk's size line is its 8 hex digits, high first, then CRLF +law chunk_line: + for +n: U32 + {Http.chunk.line(n) + == SCon{Http.hex.at(n, 28n), SCon{Http.hex.at(n, 24n), + SCon{Http.hex.at(n, 20n), SCon{Http.hex.at(n, 16n), + SCon{Http.hex.at(n, 12n), SCon{Http.hex.at(n, 8n), + SCon{Http.hex.at(n, 4n), SCon{Http.hex.at(n, 0n), "\r\n"}}}}}}}} + : String} + +# LAW: a hex digit is the 4 bits at its shift, 0-9 then lowercase a-f +law hex_at: + for +n: U32 + for k: Nat + {Http.hex.at(n, k) == Http.hex.nib(U32.and(U32.shrn(n, k), 15)) : Char} + +# LAW: a value below 10 is its decimal digit, any other `a` onwards +law hex_nib: + for +v: U32 + {Http.hex.nib(v) == Char.from_u32(Bool.pick(U32, U32.is_lt(v, 10), + U32.add(v, 48), U32.add(v, 87))) : Char} + +# LAW: an empty chunk is not written +law chunk_empty: + for +n: U32 + for e: {True{} == U32.is_eq(n, 0) : Bool} + {Http.chunk.frame(n) == Http.NoChunk{} : Http.Chunk} + +# LAW: any other chunk is written after its size line +law chunk_some: + for +n: U32 + for e: {False{} == U32.is_eq(n, 0) : Bool} + {Http.chunk.frame(n) == Http.Chunk{Http.chunk.line(n)} : Http.Chunk} + +# LAW (RFC 9112 §7.1): the body ends with the last chunk and no trailer +law chunk_end: + for s: String + for e: {"" == s : String} + {Http.chunk.end() ++ s == "0\r\n\r\n" : String} diff --git a/laws/TakeDropSplitAtOneOffset.bend b/laws/TakeDropSplitAtOneOffset.bend new file mode 100644 index 0000000..dd297f4 --- /dev/null +++ b/laws/TakeDropSplitAtOneOffset.bend @@ -0,0 +1,9 @@ +import Base +import ../src/http.bend as Http + +# LAW: take and drop partition a string at the same UTF-8 byte offset +# (Content-Length octets, RFC 9110 §8.6). +law utf8_split: + for s: String + for +n: Nat + {String.append(Http.octs.take(s, n), Http.octs.drop(s, n)) == s : String} diff --git a/laws/TimeoutZeroIsOneMs.bend b/laws/TimeoutZeroIsOneMs.bend new file mode 100644 index 0000000..54dc766 --- /dev/null +++ b/laws/TimeoutZeroIsOneMs.bend @@ -0,0 +1,17 @@ +# For every timeout `ms` of 1 or more, the deadline is exactly `ms`; for a +# timeout of 0 it is 1 ms, since bend-kit-wire reads a deadline of 0 as +# none. +import Base +import ../src/wirespec.bend as Spec + +# LAW: a timeout of 1 ms or more is the deadline every call gets, unchanged +law ms_given: + for +ms: U32 + for e: {False{} == U32.is_eq(ms, 0) : Bool} + {Spec.wire.ms.of(Some{ms}) == ms : U32} + +# LAW: a timeout of 0 is a deadline of 1 ms (bend-kit-wire reads 0 as none) +law ms_zero: + for +ms: U32 + for e: {True{} == U32.is_eq(ms, 0) : Bool} + {Spec.wire.ms.of(Some{ms}) == 1 : U32} diff --git a/laws/TimeoutsReadAsTimedOut.bend b/laws/TimeoutsReadAsTimedOut.bend new file mode 100644 index 0000000..2f8820f --- /dev/null +++ b/laws/TimeoutsReadAsTimedOut.bend @@ -0,0 +1,30 @@ +# For every reason `why`, a failed exchange with wire status 28 (ran out of +# time) is reported as `Err` with reason exactly `timed out: `, which +# `http.timed_out` recognizes; a failed exchange with any other status, or +# none, is reported with `why` unchanged. +import Base +import ../src/wirespec.bend as Spec + +# --- what a failed socket call is reported as --- + +# LAW: a failed exchange with status 28 is reported as `timed out: ` +law why_late: + for why: String + {Spec.wire.why(Some{28}, why) == "timed out: " ++ why : String} + +# LAW: a failed exchange with any other status keeps its reason unchanged +law why_other: + for +n: U32 + for e: {False{} == U32.is_eq(n, 28) : Bool} + for why: String + {Spec.wire.why(Some{n}, why) == why : String} + +# LAW: a failed exchange with no status keeps its reason unchanged +law why_none: + for why: String + {Spec.wire.why(None{}, why) == why : String} + +# LAW: the reason reported for status 28 is one timed_out recognizes +law why_late_timed_out: + for why: String + {Spec.wire.timed_out(Spec.wire.why(Some{28}, why)) == True{} : Bool} diff --git a/laws/UnescapeFindsReferences.bend b/laws/UnescapeFindsReferences.bend new file mode 100644 index 0000000..31a656d --- /dev/null +++ b/laws/UnescapeFindsReferences.bend @@ -0,0 +1,277 @@ +# `html_unescape(s)` (`src/unescape.bend`) replaces the character references +# in a text the way Python's `html.unescape` does, which follows the HTML +# Standard's character reference states (§13.2.5.72 to §13.2.5.80) and keeps +# its own choices where the tokenizer would report a parse error. +# +# For every string, `html_unescape` reads it left to right and writes every +# character as itself except where `&` opens a reference: `&#` and one or +# more decimal digits, or `&#x` or `&#X` and one or more hex digits +# (§13.2.5.75 to .78), then an optional `;`, is a numeric reference +# (laws/NumericReferencesDecode.bend); `&` and 1 to 32 characters other than +# TAB, LF, FF, space, `<`, `&`, `#` and `;`, then an optional `;` +# (§13.2.5.73), is a named reference +# (laws/NamedReferencesLongestMatch.bend); reading goes on after the +# reference. A digit adds to the number as `n·base + d`, held at 0x110000 +# (`grow`). An `&`, `&#` or `&#x` that opens no reference is written as it +# is. +import Base +import ../src/unescape.bend as Ref + +# --- HTML character references --- + +# LAW: html_unescape scans a text from text +law ref_unescape: + for s: String + {Ref.unescape(s) == Ref.scan(s, Ref.Txt{}) : String} + +# LAW: at the end of the text, what is open ends there +law ref_scan_nil: + for st: Ref.Scan + {Ref.scan("", st) == Ref.end(st) : String} + +# LAW: the text is scanned one character at a time +law ref_scan_cons: + for +c: Char + for t: String + for st: Ref.Scan + {Ref.scan(SCon{c, t}, st) == Ref.step(st, c, st2 => Ref.scan(t, st2)) + : String} + +# LAW: in text, `&` may open a reference +law ref_text_amp: + for +c: Char + for k: Ref.Scan -> String + for e: {True{} == Char.is_eq(c, '&') : Bool} + {Ref.step(Ref.Txt{}, c, k) == k(Ref.Amp{}) : String} + +# LAW: in text, every other character is itself +law ref_text_other: + for +c: Char + for k: Ref.Scan -> String + for e: {False{} == Char.is_eq(c, '&') : Bool} + {Ref.step(Ref.Txt{}, c, k) == SCon{c, k(Ref.Txt{})} : String} + +# LAW (§13.2.5.72): after `&`, `#` opens a number +law ref_amp_hash: + for +c: Char + for k: Ref.Scan -> String + for e: {True{} == Char.is_eq(c, '#') : Bool} + {Ref.step(Ref.Amp{}, c, k) == k(Ref.Hash{}) : String} + +# LAW (§13.2.5.73): after `&`, a name character opens a name +law ref_amp_name: + for +c: Char + for k: Ref.Scan -> String + for e1: {False{} == Char.is_eq(c, '#') : Bool} + for e2: {True{} == Ref.name.char(c) : Bool} + {Ref.step(Ref.Amp{}, c, k) == k(Ref.Name{SCon{c, ""}, 1n}) : String} + +# LAW: after `&`, anything else leaves the `&` as text and is read in text +law ref_amp_other: + for +c: Char + for k: Ref.Scan -> String + for e1: {False{} == Char.is_eq(c, '#') : Bool} + for e2: {False{} == Ref.name.char(c) : Bool} + {Ref.step(Ref.Amp{}, c, k) == SCon{'&', Ref.step(Ref.Txt{}, c, k)} + : String} + +# LAW (§13.2.5.78): after `&#`, a decimal digit opens a decimal number +law ref_hash_dec: + for +c: Char + for k: Ref.Scan -> String + for e: {True{} == U32.is_lt(Ref.digit(False{}, c), 16) : Bool} + {Ref.step(Ref.Hash{}, c, k) == k(Ref.Num{False{}, Ref.digit(False{}, c)}) + : String} + +# LAW (§13.2.5.76): after `&#`, `x` or `X` may open a hex number +law ref_hash_x: + for +c: Char + for k: Ref.Scan -> String + for e1: {False{} == U32.is_lt(Ref.digit(False{}, c), 16) : Bool} + for e2: {True{} == Bool.or(Char.is_eq(c, 'x'), Char.is_eq(c, 'X')) : Bool} + {Ref.step(Ref.Hash{}, c, k) == k(Ref.HashX{c}) : String} + +# LAW: after `&#`, anything else leaves `&#` as text and is read in text +law ref_hash_other: + for +c: Char + for k: Ref.Scan -> String + for e1: {False{} == U32.is_lt(Ref.digit(False{}, c), 16) : Bool} + for e2: {False{} == Bool.or(Char.is_eq(c, 'x'), Char.is_eq(c, 'X')) : Bool} + {Ref.step(Ref.Hash{}, c, k) == "&#" ++ Ref.step(Ref.Txt{}, c, k) : String} + +# LAW (§13.2.5.77): after `&#x`, a hex digit opens a hex number +law ref_hashx_digit: + for +x: Char + for +c: Char + for k: Ref.Scan -> String + for e: {True{} == U32.is_lt(Ref.digit(True{}, c), 16) : Bool} + {Ref.step(Ref.HashX{x}, c, k) == k(Ref.Num{True{}, Ref.digit(True{}, c)}) + : String} + +# LAW: after `&#x`, anything else leaves `&#x` as text and is read in text +law ref_hashx_other: + for +x: Char + for +c: Char + for k: Ref.Scan -> String + for e: {False{} == U32.is_lt(Ref.digit(True{}, c), 16) : Bool} + {Ref.step(Ref.HashX{x}, c, k) + == SCon{'&', SCon{'#', SCon{x, Ref.step(Ref.Txt{}, c, k)}}} : String} + +# LAW: inside a number, a digit of its base grows it +law ref_num_digit: + for +hex: Bool + for +n: U32 + for +c: Char + for k: Ref.Scan -> String + for e: {True{} == U32.is_lt(Ref.digit(hex, c), 16) : Bool} + {Ref.step(Ref.Num{hex, n}, c, k) + == k(Ref.Num{hex, Ref.grow(hex, n, Ref.digit(hex, c))}) : String} + +# LAW (§13.2.5.79): inside a number, `;` ends the reference with it +law ref_num_semi: + for +hex: Bool + for +n: U32 + for +c: Char + for k: Ref.Scan -> String + for e1: {False{} == U32.is_lt(Ref.digit(hex, c), 16) : Bool} + for e2: {True{} == Char.is_eq(c, ';') : Bool} + {Ref.step(Ref.Num{hex, n}, c, k) == Ref.code(n) ++ k(Ref.Txt{}) : String} + +# LAW: inside a number, anything else ends the reference and is read in text +law ref_num_other: + for +hex: Bool + for +n: U32 + for +c: Char + for k: Ref.Scan -> String + for e1: {False{} == U32.is_lt(Ref.digit(hex, c), 16) : Bool} + for e2: {False{} == Char.is_eq(c, ';') : Bool} + {Ref.step(Ref.Num{hex, n}, c, k) == Ref.code(n) ++ Ref.step(Ref.Txt{}, c, k) + : String} + +# LAW: inside a name, `;` ends the reference with it +law ref_name_semi: + for +s: String + for +len: Nat + for +c: Char + for k: Ref.Scan -> String + for e: {True{} == Char.is_eq(c, ';') : Bool} + {Ref.step(Ref.Name{s, len}, c, k) == Ref.named(s ++ ";") ++ k(Ref.Txt{}) + : String} + +# LAW: inside a name of fewer than 32 characters, a name character joins it +law ref_name_join: + for +s: String + for +len: Nat + for +c: Char + for k: Ref.Scan -> String + for e1: {False{} == Char.is_eq(c, ';') : Bool} + for e2: {True{} == Bool.and(Ref.name.char(c), Nat.is_lt(len, 32n)) : Bool} + {Ref.step(Ref.Name{s, len}, c, k) == k(Ref.Name{s ++ SCon{c, ""}, 1n+len}) + : String} + +# LAW: inside a name, anything else ends the reference and is read in text +law ref_name_stop: + for +s: String + for +len: Nat + for +c: Char + for k: Ref.Scan -> String + for e1: {False{} == Char.is_eq(c, ';') : Bool} + for e2: {False{} == Bool.and(Ref.name.char(c), Nat.is_lt(len, 32n)) : Bool} + {Ref.step(Ref.Name{s, len}, c, k) == Ref.named(s) ++ Ref.step(Ref.Txt{}, c, k) + : String} + +# LAW: text ends with nothing open +law ref_end_text: + for st: Ref.Scan + for e: {Ref.Txt{} == st : Ref.Scan} + {Ref.end(st) == "" : String} + +# LAW: an `&` at the end is text +law ref_end_amp: + for st: Ref.Scan + for e: {Ref.Amp{} == st : Ref.Scan} + {Ref.end(st) == "&" : String} + +# LAW: an `&#` at the end is text +law ref_end_hash: + for st: Ref.Scan + for e: {Ref.Hash{} == st : Ref.Scan} + {Ref.end(st) == "&#" : String} + +# LAW: an `&#x` at the end is text +law ref_end_hashx: + for x: Char + {Ref.end(Ref.HashX{x}) == SCon{'&', SCon{'#', SCon{x, ""}}} : String} + +# LAW: a number at the end ends its reference +law ref_end_num: + for hex: Bool + for +n: U32 + {Ref.end(Ref.Num{hex, n}) == Ref.code(n) : String} + +# LAW: a name at the end ends its reference +law ref_end_name: + for +s: String + for len: Nat + {Ref.end(Ref.Name{s, len}) == Ref.named(s) : String} + +# LAW (§13.2.5.73): a name character is anything but TAB, LF, FF, space, `<`, +# `&`, `#` and `;` +law ref_name_char: + for +c: Char + {Ref.name.char(c) == Bool.not(Bool.or(Char.is_eq(c, '\t'), + Bool.or(Char.is_eq(c, '\n'), Bool.or(Char.is_eq(c, '\u{C}'), + Bool.or(Char.is_eq(c, ' '), Bool.or(Char.is_eq(c, '<'), + Bool.or(Char.is_eq(c, '&'), Bool.or(Char.is_eq(c, '#'), + Char.is_eq(c, ';'))))))))) : Bool} + +# LAW: a decimal digit `0`-`9` is worth 0 to 9 +law ref_digit_dec: + for +u: U32 + for e: {True{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} + {Ref.digit(False{}, Chr{u}) == U32.sub(u, 48) : U32} + +# LAW: any other character is no decimal digit +law ref_digit_dec_none: + for +u: U32 + for e: {False{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} + {Ref.digit(False{}, Chr{u}) == 16 : U32} + +# LAW: a hex digit `0`-`9` is worth 0 to 9 +law ref_digit_hex_dec: + for +u: U32 + for e: {True{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} + {Ref.digit(True{}, Chr{u}) == U32.sub(u, 48) : U32} + +# LAW: a hex digit `A`-`F` is worth 10 to 15 +law ref_digit_hex_upper: + for +u: U32 + for e1: {False{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} + for e2: {True{} == Bool.and(U32.is_le(65, u), U32.is_le(u, 70)) : Bool} + {Ref.digit(True{}, Chr{u}) == U32.sub(u, 55) : U32} + +# LAW: a hex digit `a`-`f` is worth 10 to 15 +law ref_digit_hex_lower: + for +u: U32 + for e1: {False{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} + for e2: {False{} == Bool.and(U32.is_le(65, u), U32.is_le(u, 70)) : Bool} + for e3: {True{} == Bool.and(U32.is_le(97, u), U32.is_le(u, 102)) : Bool} + {Ref.digit(True{}, Chr{u}) == U32.sub(u, 87) : U32} + +# LAW: any other character is no hex digit +law ref_digit_hex_none: + for +u: U32 + for e1: {False{} == Bool.and(U32.is_le(48, u), U32.is_le(u, 57)) : Bool} + for e2: {False{} == Bool.and(U32.is_le(65, u), U32.is_le(u, 70)) : Bool} + for e3: {False{} == Bool.and(U32.is_le(97, u), U32.is_le(u, 102)) : Bool} + {Ref.digit(True{}, Chr{u}) == 16 : U32} + +# LAW: a digit grows a number as n times the base plus the digit, held at +# 0x110000 +law ref_grow: + for +hex: Bool + for +n: U32 + for +d: U32 + {Ref.grow(hex, n, d) + == U32.min(U32.add(U32.mul(n, Bool.pick(U32, hex, 16, 10)), d), 1114112) + : U32} diff --git a/laws/UnescapeUndoesEscape.bend b/laws/UnescapeUndoesEscape.bend new file mode 100644 index 0000000..feaab10 --- /dev/null +++ b/laws/UnescapeUndoesEscape.bend @@ -0,0 +1,13 @@ +# For every string, `html_unescape` of its `html_escape` +# (laws/HtmlEscapeCharByChar.bend) is the string: `&`, `<`, `>`, +# `"` and `'` read back as `&`, `<`, `>`, `"` and `'`, and every +# other character as itself. +import Base +import ../src/html.bend as Html +import ../src/unescape.bend as Ref + +# LAW: unescaping undoes html_escape: for every string, html_unescape of its +# escape is the string +law ref_round: + for s: String + {Ref.unescape(Html.escape(s)) == s : String} diff --git a/laws/UnframedBodyIsRest.bend b/laws/UnframedBodyIsRest.bend new file mode 100644 index 0000000..81fb860 --- /dev/null +++ b/laws/UnframedBodyIsRest.bend @@ -0,0 +1,11 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9112 §6.3): with neither framing header the rest of the connection +# is the body (Connection: close) +law unframed_is_rest: + for st: U32 + for hs: List<&2, Http.Header> + for body: String + {Http.parse.len(st, hs, None{}, body) == Http.Reply{st, hs, body} + : Http.Reply} diff --git a/laws/UnknownStatusHasEmptyReason.bend b/laws/UnknownStatusHasEmptyReason.bend new file mode 100644 index 0000000..e22f668 --- /dev/null +++ b/laws/UnknownStatusHasEmptyReason.bend @@ -0,0 +1,6 @@ +import Base +import ../src/http.bend as Http + +# LAW (RFC 9110 §15): a status we do not name has an empty reason-phrase +law reason_unknown_empty: + {Http.reason(418) == "" : String} diff --git a/laws/UnquotePlusReadsSpaces.bend b/laws/UnquotePlusReadsSpaces.bend new file mode 100644 index 0000000..6872933 --- /dev/null +++ b/laws/UnquotePlusReadsSpaces.bend @@ -0,0 +1,20 @@ +# `unquote_plus` is `unquote` with every `+` outside an escape read as the +# byte of a space (WHATWG URL §5.1 step 3.3: "replace any 0x2B (+) in name +# and value with 0x20 (SP)"), as Python replaces `+` before it unquotes; +# `%2B` stays `+`. +# +# also rests on: laws/UnquoteReadsEscapes.bend +import Base +import ../src/qs.bend as Qs + +# LAW: unquote_plus is the same reading with `+` as a space +law qs_unquote_plus: + for s: String + {Qs.unquote.plus(s) + == Qs.utf.dec(Qs.toks(s, True{}, Qs.Plain{}), Qs.Idle{}) : String} + +# LAW (WHATWG URL §5.1 step 3.3): with `plus`, `+` is the byte of a space +law qs_byte_plus: + for +c: Char + for e: {True{} == Char.is_eq(c, '+') : Bool} + {Qs.tok.byte(True{}, c) == 32 : U32} diff --git a/laws/UnquoteReadsEscapes.bend b/laws/UnquoteReadsEscapes.bend new file mode 100644 index 0000000..c04eed4 --- /dev/null +++ b/laws/UnquoteReadsEscapes.bend @@ -0,0 +1,179 @@ +# `unquote(s)` and `unquote_plus(s)` (`src/qs.bend`) percent-decode a string +# as RFC 3986 §2.1 describes ("a percent-encoded octet is encoded as a +# character triplet, consisting of the percent character "%" followed by the +# two hexadecimal digits representing that octet's numeric value"; upper- +# and lower-case hex digits are equivalent), the way Python's +# `urllib.parse.unquote(s)` and `urllib.parse.unquote_plus(s)` do; the bytes +# are read as UTF-8 (RFC 3629 §4) with `errors="replace"`. `quote_plus`, +# `urlencode`, `parse_query` and `query_get` read and write +# `application/x-www-form-urlencoded` query strings (WHATWG URL §5) the way +# `urllib.parse.quote_plus`, `urlencode(pairs)` and `parse_qsl(text, +# keep_blank_values=True)` do. A pair is `(name, value)`, a reusable `String +# & String` (`Kv()`). +# +# For every string, `unquote` reads it left to right into bytes and +# characters (`Qs.toks`) and decodes those as UTF-8 +# (laws/Utf8DecodesWithReplacement.bend). A character outside ASCII is kept +# as itself and ends the run of bytes before it, so each maximal ASCII run +# is decoded on its own, as Python decodes it. In a run, `%` followed by two +# hex digits (`0`-`9`, `A`-`F`, `a`-`f`, RFC 3986 §2.1) is the byte they +# name, high digit first; a `%` not followed by two hex digits is the byte +# `%` (and a single hex digit after it its own byte), the escape left as +# written; every other ASCII character is its own byte. +import Base +import ../src/http.bend as Http +import ../src/qs.bend as Qs + +# --- percent-decoding and query strings --- + +# LAW: unquote reads a string into bytes and characters from plain text, then +# decodes them as UTF-8 from between characters +law qs_unquote: + for s: String + {Qs.unquote(s) == Qs.utf.dec(Qs.toks(s, False{}, Qs.Plain{}), Qs.Idle{}) + : String} + +# LAW: at the end of the text, what is left open is written as it stands +law qs_toks_nil: + for plus: Bool + for st: Qs.Esc + {Qs.toks("", plus, st) == Qs.tok.end(st) : List<&2, Qs.Tok>} + +# LAW: the text is read one character at a time, from where the reading stands +law qs_toks_cons: + for +c: Char + for t: String + for +plus: Bool + for st: Qs.Esc + {Qs.toks(SCon{c, t}, plus, st) + == Qs.tok.step(plus, st, c, st2 => Qs.toks(t, plus, st2)) + : List<&2, Qs.Tok>} + +# LAW: in plain text, a character outside ASCII is kept as itself +law qs_tok_wide: + for plus: Bool + for +c: Char + for k: Qs.Esc -> List<&2, Qs.Tok> + for e: {True{} == U32.is_ge(Char.to_u32(c), 128) : Bool} + {Qs.tok.step(plus, Qs.Plain{}, c, k) == Qs.W{c} <> k(Qs.Plain{}) + : List<&2, Qs.Tok>} + +# LAW (RFC 3986 §2.1): in plain text, `%` opens an escape +law qs_tok_open: + for plus: Bool + for +c: Char + for k: Qs.Esc -> List<&2, Qs.Tok> + for e1: {False{} == U32.is_ge(Char.to_u32(c), 128) : Bool} + for e2: {True{} == Char.is_eq(c, '%') : Bool} + {Qs.tok.step(plus, Qs.Plain{}, c, k) == k(Qs.Pct{}) : List<&2, Qs.Tok>} + +# LAW: in plain text, every other ASCII character is a byte +law qs_tok_byte: + for plus: Bool + for +c: Char + for k: Qs.Esc -> List<&2, Qs.Tok> + for e1: {False{} == U32.is_ge(Char.to_u32(c), 128) : Bool} + for e2: {False{} == Char.is_eq(c, '%') : Bool} + {Qs.tok.step(plus, Qs.Plain{}, c, k) + == Qs.B{Qs.tok.byte(plus, c)} <> k(Qs.Plain{}) : List<&2, Qs.Tok>} + +# LAW: any other plain character is the byte of its own code +law qs_byte_other: + for plus: Bool + for +c: Char + for e: {False{} == Bool.and(plus, Char.is_eq(c, '+')) : Bool} + {Qs.tok.byte(plus, c) == Char.to_u32(c) : U32} + +# LAW (RFC 3986 §2.1): after `%`, a hex digit is held for the next one +law qs_tok_pct_hex: + for plus: Bool + for +c: Char + for k: Qs.Esc -> List<&2, Qs.Tok> + for e: {True{} == Qs.tok.hex(c) : Bool} + {Qs.tok.step(plus, Qs.Pct{}, c, k) == k(Qs.Pct1{c}) : List<&2, Qs.Tok>} + +# LAW: after `%`, any other character leaves the `%` as written and is read +# in plain text +law qs_tok_pct_other: + for +plus: Bool + for +c: Char + for k: Qs.Esc -> List<&2, Qs.Tok> + for e: {False{} == Qs.tok.hex(c) : Bool} + {Qs.tok.step(plus, Qs.Pct{}, c, k) + == Qs.B{37} <> Qs.tok.step(plus, Qs.Plain{}, c, k) : List<&2, Qs.Tok>} + +# LAW (RFC 3986 §2.1): `%` and two hex digits are the byte they name, the +# first digit high +law qs_tok_pct1_hex: + for plus: Bool + for +d: Char + for +c: Char + for k: Qs.Esc -> List<&2, Qs.Tok> + for e: {True{} == Qs.tok.hex(c) : Bool} + {Qs.tok.step(plus, Qs.Pct1{d}, c, k) + == Qs.B{U32.add(U32.mul(Http.hex.val(d), 16), Http.hex.val(c))} + <> k(Qs.Plain{}) : List<&2, Qs.Tok>} + +# LAW: `%` and one hex digit before anything else are written as they stand +# and the character is read in plain text +law qs_tok_pct1_other: + for +plus: Bool + for +d: Char + for +c: Char + for k: Qs.Esc -> List<&2, Qs.Tok> + for e: {False{} == Qs.tok.hex(c) : Bool} + {Qs.tok.step(plus, Qs.Pct1{d}, c, k) + == Qs.B{37} <> (Qs.B{Char.to_u32(d)} + <> Qs.tok.step(plus, Qs.Plain{}, c, k)) : List<&2, Qs.Tok>} + +# LAW: plain text ends with nothing left open +law qs_tok_end_plain: + for st: Qs.Esc + for e: {Qs.Plain{} == st : Qs.Esc} + {Qs.tok.end(st) == [] : List<&2, Qs.Tok>} + +# LAW: a `%` at the end is written as it stands +law qs_tok_end_pct: + for st: Qs.Esc + for e: {Qs.Pct{} == st : Qs.Esc} + {Qs.tok.end(st) == [Qs.B{37}] : List<&2, Qs.Tok>} + +# LAW: a `%` and one hex digit at the end are written as they stand +law qs_tok_end_pct1: + for +d: Char + {Qs.tok.end(Qs.Pct1{d}) == [Qs.B{37}, Qs.B{Char.to_u32(d)}] + : List<&2, Qs.Tok>} + +# LAW: a hex digit is a character whose hex value is below 16 +law qs_tok_hex: + for c: Char + {Qs.tok.hex(c) == U32.is_lt(Http.hex.val(c), 16) : Bool} + +# LAW: `0`-`9` are worth 0 to 9 +law qs_hexval_dec: + for +u: U32 + for e: {True{} == Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)) : Bool} + {Http.hex.val(Chr{u}) == U32.sub(u, 48) : U32} + +# LAW: `A`-`F` are worth 10 to 15 +law qs_hexval_upper: + for +u: U32 + for e1: {False{} == Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)) : Bool} + for e2: {False{} == Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)) : Bool} + for e3: {True{} == Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)) : Bool} + {Http.hex.val(Chr{u}) == U32.sub(u, 55) : U32} + +# LAW: `a`-`f` are worth 10 to 15 +law qs_hexval_lower: + for +u: U32 + for e1: {False{} == Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)) : Bool} + for e2: {True{} == Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)) : Bool} + {Http.hex.val(Chr{u}) == U32.sub(u, 87) : U32} + +# LAW: every other character is no hex digit (16) +law qs_hexval_none: + for +u: U32 + for e1: {False{} == Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)) : Bool} + for e2: {False{} == Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)) : Bool} + for e3: {False{} == Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)) : Bool} + {Http.hex.val(Chr{u}) == 16 : U32} diff --git a/laws/UrlBadPortUsesDefault.bend b/laws/UrlBadPortUsesDefault.bend new file mode 100644 index 0000000..f70975f --- /dev/null +++ b/laws/UrlBadPortUsesDefault.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.2.3): a non-numeric port falls back to the scheme default +law url_bad_port_uses_default: + {Url.parse("http://example.com:abc/x") + == Url.Loc{"http", "example.com", 80, "/x"} : Url.Loc} diff --git a/laws/UrlEmptyPath.bend b/laws/UrlEmptyPath.bend new file mode 100644 index 0000000..85b703a --- /dev/null +++ b/laws/UrlEmptyPath.bend @@ -0,0 +1,9 @@ +import Base +import ../src/url.bend as Url + +# --- RFC 3986 authority, query, fragment --- + +# LAW (RFC 3986 §3.3): an empty path-abempty is the origin-form `/` +law url_empty_path: + {Url.parse("http://example.com") + == Url.Loc{"http", "example.com", 80, "/"} : Url.Loc} diff --git a/laws/UrlFragmentHidesQuery.bend b/laws/UrlFragmentHidesQuery.bend new file mode 100644 index 0000000..fc70d84 --- /dev/null +++ b/laws/UrlFragmentHidesQuery.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.5): a `?` inside the fragment is not a query +law url_fragment_hides_query: + {Url.parse("http://example.com/a#b?c") + == Url.Loc{"http", "example.com", 80, "/a"} : Url.Loc} diff --git a/laws/UrlHttpDefaultPort.bend b/laws/UrlHttpDefaultPort.bend new file mode 100644 index 0000000..0ae26dc --- /dev/null +++ b/laws/UrlHttpDefaultPort.bend @@ -0,0 +1,9 @@ +import Base +import ../src/url.bend as Url + +# --- thicker fence: more closed witnesses of the same RFCs --- + +# LAW (RFC 3986 §3.2.3): http with no port is 80 +law url_http_default_port: + {Url.parse("http://example.com/a") + == Url.Loc{"http", "example.com", 80, "/a"} : Url.Loc} diff --git a/laws/UrlHttpsDefaultPort.bend b/laws/UrlHttpsDefaultPort.bend new file mode 100644 index 0000000..93631c3 --- /dev/null +++ b/laws/UrlHttpsDefaultPort.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.2.3 / RFC 2818): https with no port is 443 +law url_https_default_port: + {Url.parse("https://example.com/a") + == Url.Loc{"https", "example.com", 443, "/a"} : Url.Loc} diff --git a/laws/UrlHttpsExplicitPort.bend b/laws/UrlHttpsExplicitPort.bend new file mode 100644 index 0000000..e4f4229 --- /dev/null +++ b/laws/UrlHttpsExplicitPort.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.2.3): an explicit https port is kept +law url_https_explicit_port: + {Url.parse("https://example.com:8443/a") + == Url.Loc{"https", "example.com", 8443, "/a"} : Url.Loc} diff --git a/laws/UrlIpv4Host.bend b/laws/UrlIpv4Host.bend new file mode 100644 index 0000000..f5fb39b --- /dev/null +++ b/laws/UrlIpv4Host.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.2.2): an IPv4 literal is the host +law url_ipv4_host: + {Url.parse("http://192.0.2.1/a") + == Url.Loc{"http", "192.0.2.1", 80, "/a"} : Url.Loc} diff --git a/laws/UrlIpv4Port.bend b/laws/UrlIpv4Port.bend new file mode 100644 index 0000000..5c37e25 --- /dev/null +++ b/laws/UrlIpv4Port.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.2.3): an explicit port wins over the scheme default +law url_ipv4_port: + {Url.parse("http://192.0.2.1:8080/a") + == Url.Loc{"http", "192.0.2.1", 8080, "/a"} : Url.Loc} diff --git a/laws/UrlNeedsSlashes.bend b/laws/UrlNeedsSlashes.bend new file mode 100644 index 0000000..08f35a2 --- /dev/null +++ b/laws/UrlNeedsSlashes.bend @@ -0,0 +1,11 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3): hier-part without "//" is refused +law parse_needs_slashes: + for +scheme: String + for +rest: String + for e: {False{} == String.starts_with(rest, "//") : Bool} + {Url.parse.rest(scheme, rest) + == Url.Bad{scheme ++ ":" ++ rest ++ " has no //"} + : Url.Loc} diff --git a/laws/UrlPathIsAbsolute.bend b/laws/UrlPathIsAbsolute.bend new file mode 100644 index 0000000..1bfd5b8 --- /dev/null +++ b/laws/UrlPathIsAbsolute.bend @@ -0,0 +1,9 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.3 path-absolute / path-abempty with authority): the path a +# URL comes apart into is always absolute. The request-target is an +# absolute-path (RFC 9112 §3.2.1). +law path_absolute: + for ps: List<&2, String> + {String.starts_with(Url.path.of(ps), "/") == True{} : Bool} diff --git a/laws/UrlQueryDropsFragment.bend b/laws/UrlQueryDropsFragment.bend new file mode 100644 index 0000000..c7e54cc --- /dev/null +++ b/laws/UrlQueryDropsFragment.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.4, §3.5): the query stays on the path; the fragment does not +law url_query_drops_fragment: + {Url.parse("http://example.com/a/b?x=1#frag") + == Url.Loc{"http", "example.com", 80, "/a/b?x=1"} : Url.Loc} diff --git a/laws/UrlQueryKeepsQuestion.bend b/laws/UrlQueryKeepsQuestion.bend new file mode 100644 index 0000000..f9daef2 --- /dev/null +++ b/laws/UrlQueryKeepsQuestion.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.4): a later `?` stays inside the query +law url_query_keeps_question: + {Url.parse("http://example.com/a?x=1?y=2") + == Url.Loc{"http", "example.com", 80, "/a?x=1?y=2"} : Url.Loc} diff --git a/laws/UrlQueryOnEmptyPath.bend b/laws/UrlQueryOnEmptyPath.bend new file mode 100644 index 0000000..e3c4d3b --- /dev/null +++ b/laws/UrlQueryOnEmptyPath.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.3, §3.4): an empty path with a query is `/?query` +law url_query_on_empty_path: + {Url.parse("http://example.com?x=1") + == Url.Loc{"http", "example.com", 80, "/?x=1"} : Url.Loc} diff --git a/laws/UrlSchemeLowercased.bend b/laws/UrlSchemeLowercased.bend new file mode 100644 index 0000000..5d8d50a --- /dev/null +++ b/laws/UrlSchemeLowercased.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.1): the scheme is lowercased; the host is not +law url_scheme_lowercased: + {Url.parse("HTTPS://Example.com/x") + == Url.Loc{"https", "Example.com", 443, "/x"} : Url.Loc} diff --git a/laws/UrlShowBadIsMarked.bend b/laws/UrlShowBadIsMarked.bend new file mode 100644 index 0000000..b75aac6 --- /dev/null +++ b/laws/UrlShowBadIsMarked.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW: a url that would not come apart says so first +law url_show_bad_is_marked: + for why: String + {String.starts_with(Url.show(Url.Bad{why}), "bad ") == True{} : Bool} diff --git a/laws/UrlShowOpensWithScheme.bend b/laws/UrlShowOpensWithScheme.bend new file mode 100644 index 0000000..a14c1c5 --- /dev/null +++ b/laws/UrlShowOpensWithScheme.bend @@ -0,0 +1,11 @@ +import Base +import ../src/url.bend as Url + +# LAW: a url that came apart opens with its scheme +law url_show_opens_with_scheme: + for +scheme: String + for host: String + for port: U32 + for path: String + {String.starts_with(Url.show(Url.Loc{scheme, host, port, path}), scheme) + == True{} : Bool} diff --git a/laws/UrlTrailingSlash.bend b/laws/UrlTrailingSlash.bend new file mode 100644 index 0000000..102e31e --- /dev/null +++ b/laws/UrlTrailingSlash.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.3): a trailing slash is part of the path +law url_trailing_slash: + {Url.parse("http://example.com/a/") + == Url.Loc{"http", "example.com", 80, "/a/"} : Url.Loc} diff --git a/laws/UrlUserinfoLastAt.bend b/laws/UrlUserinfoLastAt.bend new file mode 100644 index 0000000..37ccf03 --- /dev/null +++ b/laws/UrlUserinfoLastAt.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.2.1): the last `@` piece is the hostport +law url_userinfo_last_at: + {Url.parse("http://a@b@example.com/z") + == Url.Loc{"http", "example.com", 80, "/z"} : Url.Loc} diff --git a/laws/UrlUserinfoStripped.bend b/laws/UrlUserinfoStripped.bend new file mode 100644 index 0000000..fd9ec3b --- /dev/null +++ b/laws/UrlUserinfoStripped.bend @@ -0,0 +1,7 @@ +import Base +import ../src/url.bend as Url + +# LAW (RFC 3986 §3.2.1): userinfo is stripped; it is not the host +law url_userinfo_stripped: + {Url.parse("http://user:pass@example.com/a") + == Url.Loc{"http", "example.com", 80, "/a"} : Url.Loc} diff --git a/laws/UrlencodeJoinsPairs.bend b/laws/UrlencodeJoinsPairs.bend new file mode 100644 index 0000000..4fea371 --- /dev/null +++ b/laws/UrlencodeJoinsPairs.bend @@ -0,0 +1,41 @@ +# For every list of pairs, `urlencode` writes each pair as +# `quote_plus(name)`, `=`, `quote_plus(value)`, in order, with `&` between +# two pairs and nothing for no pairs (WHATWG URL §5.2 serializer's shape, +# Python's `urlencode(pairs)` escaping). +import Base +import ../src/qs.bend as Qs + +# LAW: no pairs are the empty query +law qs_ue_nil: + for ps: List<&2, Qs.Kv()> + for e: {[] == ps : List<&2, Qs.Kv()>} + {Qs.urlencode(ps) == "" : String} + +# LAW: the first pair, then the others each after `&` +law qs_ue_cons: + for name: String + for value: String + for t: List<&2, Qs.Kv()> + {Qs.urlencode((name, value) <> t) == Qs.form.pair(name, value) + ++ Qs.form.more(t) : String} + +# LAW: after the last pair, nothing +law qs_more_nil: + for ps: List<&2, Qs.Kv()> + for e: {[] == ps : List<&2, Qs.Kv()>} + {Qs.form.more(ps) == "" : String} + +# LAW: each pair after the first is written after `&` +law qs_more_cons: + for name: String + for value: String + for t: List<&2, Qs.Kv()> + {Qs.form.more((name, value) <> t) + == "&" ++ Qs.form.pair(name, value) ++ Qs.form.more(t) : String} + +# LAW: a pair is its quote_plus'd name, `=` and its quote_plus'd value +law qs_pair: + for name: String + for value: String + {Qs.form.pair(name, value) + == Qs.quote.plus(name) ++ "=" ++ Qs.quote.plus(value) : String} diff --git a/laws/Utf8DecodesWithReplacement.bend b/laws/Utf8DecodesWithReplacement.bend new file mode 100644 index 0000000..2436af1 --- /dev/null +++ b/laws/Utf8DecodesWithReplacement.bend @@ -0,0 +1,252 @@ +# Bytes are decoded as UTF-8 one at a time (`Qs.utf.dec`). Between +# characters, a byte `00`-`7F` is that character; `C2`-`DF` opens a sequence +# needing one more byte, `E0` two (the next `A0`-`BF`), `E1`-`EC` and +# `EE`-`EF` two, `ED` two (the next `80`-`9F`), `F0` three (the next +# `90`-`BF`), `F1`-`F3` three, `F4` three (the next `80`-`8F`), each byte +# after the next `80`-`BF` (RFC 3629 §4's well-formed sequences, Unicode +# Table 3-7); `80`-`C1` and `F5`-`FF` are U+FFFD. A byte that fits the +# sequence adds its six low bits to the code point, and the last one ends it +# with that character. A byte that does not fit, a character outside ASCII, +# or the end of the bytes ends the sequence unfinished: what it has read is +# one U+FFFD (Unicode §3.9, substitution of maximal subparts, as Python's +# `errors="replace"` does), and the byte is read again between characters. +import Base +import ../src/qs.bend as Qs + +# LAW: decoding ends where the pieces end +law qs_dec_nil: + for st: Qs.Utf + {Qs.utf.dec([], st) == Qs.utf.end(st) : String} + +# LAW: pieces are decoded one at a time, from where the decoding stands +law qs_dec_cons: + for t: Qs.Tok + for rest: List<&2, Qs.Tok> + for st: Qs.Utf + {Qs.utf.dec(t <> rest, st) + == Qs.utf.step(st, t, st2 => Qs.utf.dec(rest, st2)) : String} + +# LAW: between characters, the end leaves nothing +law qs_end_idle: + for st: Qs.Utf + for e: {Qs.Idle{} == st : Qs.Utf} + {Qs.utf.end(st) == "" : String} + +# LAW (Unicode §3.9): a sequence the end leaves unfinished is one U+FFFD +law qs_end_need: + for cp: U32 + for left: Nat + for lo: U32 + for hi: U32 + {Qs.utf.end(Qs.Need{cp, left, lo, hi}) == SCon{Chr{65533}, ""} : String} + +# LAW: between characters, a character outside ASCII is itself +law qs_idle_wide: + for c: Char + for k: Qs.Utf -> String + {Qs.utf.step(Qs.Idle{}, Qs.W{c}, k) == SCon{c, k(Qs.Idle{})} : String} + +# LAW: between characters, a byte is read by what it leads +law qs_idle_byte: + for +b: U32 + for k: Qs.Utf -> String + {Qs.utf.step(Qs.Idle{}, Qs.B{b}, k) == Qs.utf.lead(b, k, Qs.lead(b)) + : String} + +# LAW: an ASCII byte is its character +law qs_lead_on_ascii: + for +b: U32 + for k: Qs.Utf -> String + {Qs.utf.lead(b, k, Qs.Ascii{}) == SCon{Chr{b}, k(Qs.Idle{})} : String} + +# LAW (Unicode §3.9): a byte no sequence starts with is one U+FFFD +law qs_lead_on_bad: + for +b: U32 + for k: Qs.Utf -> String + {Qs.utf.lead(b, k, Qs.Bad{}) == SCon{Chr{65533}, k(Qs.Idle{})} : String} + +# LAW: a lead byte opens its sequence +law qs_lead_on_opens: + for +b: U32 + for k: Qs.Utf -> String + for cp: U32 + for left: Nat + for lo: U32 + for hi: U32 + {Qs.utf.lead(b, k, Qs.Opens{cp, left, lo, hi}) + == k(Qs.Need{cp, left, lo, hi}) : String} + +# LAW (RFC 3629 §4): 00 to 7F is ASCII +law qs_lead_ascii: + for +b: U32 + for e: {True{} == U32.is_lt(b, 128) : Bool} + {Qs.lead(b) == Qs.Ascii{} : Qs.Lead} + +# LAW (RFC 3629 §4): 80 to C1 start no sequence +law qs_lead_low: + for +b: U32 + for e1: {False{} == U32.is_lt(b, 128) : Bool} + for e2: {True{} == U32.is_lt(b, 194) : Bool} + {Qs.lead(b) == Qs.Bad{} : Qs.Lead} + +# LAW (RFC 3629 §4): C2 to DF need one byte 80 to BF +law qs_lead_two: + for +b: U32 + for e1: {False{} == U32.is_lt(b, 128) : Bool} + for e2: {False{} == U32.is_lt(b, 194) : Bool} + for e3: {True{} == U32.is_lt(b, 224) : Bool} + {Qs.lead(b) == Qs.Opens{U32.and(b, 31), 0n, 128, 191} : Qs.Lead} + +# LAW (RFC 3629 §4): E0 needs A0 to BF, then one byte +law qs_lead_e0: + for +b: U32 + for e1: {False{} == U32.is_lt(b, 128) : Bool} + for e2: {False{} == U32.is_lt(b, 194) : Bool} + for e3: {False{} == U32.is_lt(b, 224) : Bool} + for e4: {True{} == U32.is_eq(b, 224) : Bool} + {Qs.lead(b) == Qs.Opens{0, 1n, 160, 191} : Qs.Lead} + +# LAW (RFC 3629 §4): E1 to EC need 80 to BF, then one byte +law qs_lead_e1: + for +b: U32 + for e1: {False{} == U32.is_lt(b, 128) : Bool} + for e2: {False{} == U32.is_lt(b, 194) : Bool} + for e3: {False{} == U32.is_lt(b, 224) : Bool} + for e4: {False{} == U32.is_eq(b, 224) : Bool} + for e5: {True{} == U32.is_lt(b, 237) : Bool} + {Qs.lead(b) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} : Qs.Lead} + +# LAW (RFC 3629 §4): ED needs 80 to 9F (no surrogates), then one byte +law qs_lead_ed: + for +b: U32 + for e1: {False{} == U32.is_lt(b, 128) : Bool} + for e2: {False{} == U32.is_lt(b, 194) : Bool} + for e3: {False{} == U32.is_lt(b, 224) : Bool} + for e4: {False{} == U32.is_eq(b, 224) : Bool} + for e5: {False{} == U32.is_lt(b, 237) : Bool} + for e6: {True{} == U32.is_eq(b, 237) : Bool} + {Qs.lead(b) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} + +# LAW (RFC 3629 §4): EE and EF need 80 to BF, then one byte +law qs_lead_ee: + for +b: U32 + for e1: {False{} == U32.is_lt(b, 128) : Bool} + for e2: {False{} == U32.is_lt(b, 194) : Bool} + for e3: {False{} == U32.is_lt(b, 224) : Bool} + for e4: {False{} == U32.is_eq(b, 224) : Bool} + for e5: {False{} == U32.is_lt(b, 237) : Bool} + for e6: {False{} == U32.is_eq(b, 237) : Bool} + for e7: {True{} == U32.is_lt(b, 240) : Bool} + {Qs.lead(b) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} : Qs.Lead} + +# LAW (RFC 3629 §4): F0 needs 90 to BF, then two bytes +law qs_lead_f0: + for +b: U32 + for e1: {False{} == U32.is_lt(b, 128) : Bool} + for e2: {False{} == U32.is_lt(b, 194) : Bool} + for e3: {False{} == U32.is_lt(b, 224) : Bool} + for e4: {False{} == U32.is_eq(b, 224) : Bool} + for e5: {False{} == U32.is_lt(b, 237) : Bool} + for e6: {False{} == U32.is_eq(b, 237) : Bool} + for e7: {False{} == U32.is_lt(b, 240) : Bool} + for e8: {True{} == U32.is_eq(b, 240) : Bool} + {Qs.lead(b) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} + +# LAW (RFC 3629 §4): F1 to F3 need 80 to BF, then two bytes +law qs_lead_f1: + for +b: U32 + for e1: {False{} == U32.is_lt(b, 128) : Bool} + for e2: {False{} == U32.is_lt(b, 194) : Bool} + for e3: {False{} == U32.is_lt(b, 224) : Bool} + for e4: {False{} == U32.is_eq(b, 224) : Bool} + for e5: {False{} == U32.is_lt(b, 237) : Bool} + for e6: {False{} == U32.is_eq(b, 237) : Bool} + for e7: {False{} == U32.is_lt(b, 240) : Bool} + for e8: {False{} == U32.is_eq(b, 240) : Bool} + for e9: {True{} == U32.is_lt(b, 244) : Bool} + {Qs.lead(b) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} : Qs.Lead} + +# LAW (RFC 3629 §4): F4 needs 80 to 8F (up to U+10FFFF), then two bytes +law qs_lead_f4: + for +b: U32 + for e1: {False{} == U32.is_lt(b, 128) : Bool} + for e2: {False{} == U32.is_lt(b, 194) : Bool} + for e3: {False{} == U32.is_lt(b, 224) : Bool} + for e4: {False{} == U32.is_eq(b, 224) : Bool} + for e5: {False{} == U32.is_lt(b, 237) : Bool} + for e6: {False{} == U32.is_eq(b, 237) : Bool} + for e7: {False{} == U32.is_lt(b, 240) : Bool} + for e8: {False{} == U32.is_eq(b, 240) : Bool} + for e9: {False{} == U32.is_lt(b, 244) : Bool} + for e10: {True{} == U32.is_eq(b, 244) : Bool} + {Qs.lead(b) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} + +# LAW (RFC 3629 §4): F5 and up start no sequence +law qs_lead_high: + for +b: U32 + for e1: {False{} == U32.is_lt(b, 128) : Bool} + for e2: {False{} == U32.is_lt(b, 194) : Bool} + for e3: {False{} == U32.is_lt(b, 224) : Bool} + for e4: {False{} == U32.is_eq(b, 224) : Bool} + for e5: {False{} == U32.is_lt(b, 237) : Bool} + for e6: {False{} == U32.is_eq(b, 237) : Bool} + for e7: {False{} == U32.is_lt(b, 240) : Bool} + for e8: {False{} == U32.is_eq(b, 240) : Bool} + for e9: {False{} == U32.is_lt(b, 244) : Bool} + for e10: {False{} == U32.is_eq(b, 244) : Bool} + {Qs.lead(b) == Qs.Bad{} : Qs.Lead} + +# LAW (Unicode §3.9): a character outside ASCII inside a sequence ends it +# unfinished, one U+FFFD, and is itself +law qs_need_wide: + for cp: U32 + for left: Nat + for lo: U32 + for hi: U32 + for c: Char + for k: Qs.Utf -> String + {Qs.utf.step(Qs.Need{cp, left, lo, hi}, Qs.W{c}, k) + == SCon{Chr{65533}, SCon{c, k(Qs.Idle{})}} : String} + +# LAW (RFC 3629 §3): the last byte that fits ends the character +law qs_need_last: + for +cp: U32 + for +lo: U32 + for +hi: U32 + for +b: U32 + for k: Qs.Utf -> String + for e: {True{} == Bool.and(U32.is_le(lo, b), U32.is_le(b, hi)) : Bool} + {Qs.utf.step(Qs.Need{cp, 0n, lo, hi}, Qs.B{b}, k) + == SCon{Chr{Qs.utf.add(cp, b)}, k(Qs.Idle{})} : String} + +# LAW (RFC 3629 §3): a byte that fits with more to come adds its bits and +# needs the next in 80 to BF +law qs_need_more: + for +cp: U32 + for l: Nat + for +lo: U32 + for +hi: U32 + for +b: U32 + for k: Qs.Utf -> String + for e: {True{} == Bool.and(U32.is_le(lo, b), U32.is_le(b, hi)) : Bool} + {Qs.utf.step(Qs.Need{cp, 1n+l, lo, hi}, Qs.B{b}, k) + == k(Qs.Need{Qs.utf.add(cp, b), l, 128, 191}) : String} + +# LAW (Unicode §3.9): a byte that does not fit ends the sequence unfinished, +# one U+FFFD, and is read again between characters +law qs_need_miss: + for +cp: U32 + for left: Nat + for +lo: U32 + for +hi: U32 + for +b: U32 + for k: Qs.Utf -> String + for e: {False{} == Bool.and(U32.is_le(lo, b), U32.is_le(b, hi)) : Bool} + {Qs.utf.step(Qs.Need{cp, left, lo, hi}, Qs.B{b}, k) + == SCon{Chr{65533}, Qs.utf.lead(b, k, Qs.lead(b))} : String} + +# LAW (RFC 3629 §3): a continuation byte adds its six low bits +law qs_add: + for +cp: U32 + for +b: U32 + {Qs.utf.add(cp, b) == U32.or(U32.shln(cp, 6n), U32.and(b, 63)) : U32} diff --git a/laws/WithServesGetOctetsAndContext.bend b/laws/WithServesGetOctetsAndContext.bend new file mode 100644 index 0000000..d259143 --- /dev/null +++ b/laws/WithServesGetOctetsAndContext.bend @@ -0,0 +1,17 @@ +# For every request that parses, the handler of a `_with` serve gets it as +# text (`step.text`, laws/HandlerGetsRequestText.bend) together with its +# body's octets as they arrived (`step.octets`), and the context value the +# serve was given. +# +# also rests on: laws/HandlerGetsRequestText.bend +import Base +import ../src/http.bend as Http +import ../src/step.bend as Step + +# LAW: the handler gets the body octets of the request as they arrived +law step_octets: + for method: String + for target: String + for hs: List<&2, Http.Header> + for body: String + {Step.step.octets(Http.Request{method, target, hs, body}) == body : String} diff --git a/laws/defs/CharacterAbsent.bend b/laws/defs/CharacterAbsent.bend new file mode 100644 index 0000000..e7eadeb --- /dev/null +++ b/laws/defs/CharacterAbsent.bend @@ -0,0 +1,10 @@ +# whether a string holds a character nowhere +import Base + +# whether a string holds a character nowhere +def avoids(s: String, +c: Char) -> Bool: + match s: + case SNil{}: + True{} + case SCon{+h, t}: + Bool.and(Bool.not(Char.is_eq(h, c)), avoids(t, c)) diff --git a/laws/defs/PercentFrame.bend b/laws/defs/PercentFrame.bend new file mode 100644 index 0000000..ef9ebe0 --- /dev/null +++ b/laws/defs/PercentFrame.bend @@ -0,0 +1,46 @@ +# the frame of percent-encoded text: kept characters and `%HH` triples +import Base +import ../../src/pct.bend as Pct + +# an upper-case hex digit: 0-9 or A-F +def hexdig(ch: Char) -> Bool: + match ch: + case Chr{+code}: + Bool.or(Bool.and(U32.is_le(48, code), U32.is_le(code, 57)), + Bool.and(U32.is_le(65, code), U32.is_le(code, 70))) + +# whether character cp may come next, `need` hex digits still owed to a "%": +# with none owed, a kept character or a "%"; otherwise a hex digit +def framed.ok(slash: Bool, +cp: U32, need: Nat) -> Bool: + match need: + case 0n: + Bool.or(Pct.safe(slash, cp), U32.is_eq(cp, 37)) + case 1n+p: + hexdig(Chr{cp}) + +# hex digits owed after character cp: two after a "%", one fewer after a digit +def framed.next(slash: Bool, +cp: U32, need: Nat) -> Nat: + match need: + case 0n: + Bool.pick(Nat, Pct.safe(slash, cp), 0n, + Bool.pick(Nat, U32.is_eq(cp, 37), 2n, 0n)) + case 1n+p: + p + +# no hex digit is still owed at the end +def framed.end(need: Nat) -> Bool: + match need: + case 0n: + True{} + case 1n+p: + False{} + +# the laws/QuoteOutputIsFramed.bend frame read left to right: kept +# characters, and "%" followed by exactly two hex digits +def framed(+slash: Bool, str: String, +need: Nat) -> Bool: + match str: + case SNil{}: + framed.end(need) + case SCon{Chr{+cp}, tail}: + Bool.and(framed.ok(slash, cp, need), + framed(slash, tail, framed.next(slash, cp, need))) diff --git a/main.bend b/main.bend index aaa73ff..7ae92fd 100644 --- a/main.bend +++ b/main.bend @@ -25,7 +25,7 @@ def parse_url(url: String) -> Url.Loc: # noqa: L001 re-export of Url.parse # percent-encode a path (RFC 3986 §2.1), as Python's urllib.parse.quote(text): # unreserved characters and "/" stay, every other character becomes %XX per -# UTF-8 byte, upper-case hex (SPEC.md HTTP-URL) +# UTF-8 byte, upper-case hex (laws/QuoteKeepsUnreserved.bend) def quote_path(text: String) -> String: # noqa: L001 re-export of Pct.quote (the path form) Pct.quote(True{}, text) @@ -37,7 +37,7 @@ def quote_component(text: String) -> String: # noqa: L001 re-export of Pct.quot # percent-decode a string as urllib.parse.unquote: %XX escapes (either case) # become bytes, decoded as UTF-8 with U+FFFD for bytes that are not; an escape # that is not two hex digits is kept as written, and characters outside ASCII -# are kept (SPEC.md HTTP-QS) +# are kept (laws/UnquoteReadsEscapes.bend) def unquote(text: String) -> String: # noqa: L001 re-export of Qs.unquote Qs.unquote(text) @@ -72,20 +72,20 @@ def parse_query(text: String) -> List<&2, Qs.Kv()>: # noqa: L001 re-export of Q def query_get(pairs: List<&2, Qs.Kv()>, name: String) -> Maybe<&2, String>: # noqa: L001 re-export of Qs.get Qs.get(pairs, name) -# response parse re-export; `raw` is octets, one character each (SPEC.md -# HTTP-BODY) +# response parse re-export; `raw` is octets, one character each +# (laws/OneOctetPerChar.bend) def parse_response(raw: String) -> Http.Reply: # noqa: L001 re-export of Http.parse Http.parse(raw) # a response as the client reads it for a request with this method: interim # 1xx heads skipped, and no body for HEAD or a 1xx, 204 or 304 status, -# whatever Content-Length says (SPEC.md HTTP-CLI-9) +# whatever Content-Length says (laws/InterimResponsesSkipped.bend) def parse_response_to(method: String, raw: String) -> Http.Reply: # noqa: L001 re-export of Http.parse.to Http.parse.to(method, raw) # the reason phrase of a response's status line (RFC 9112 §4), read from the -# head parse_response_to stops at; empty when there is none (SPEC.md -# HTTP-CLI-11) +# head parse_response_to stops at; empty when there is none +# (laws/ReasonPhraseFromFinalHead.bend) def reason_phrase(raw: String) -> String: # noqa: L001 re-export of Http.phrase.to Http.phrase.to(raw) @@ -126,7 +126,7 @@ def form_file(name: String, filename: String, ctype: String, # noqa: L001 re-ex # a multipart/form-data body (RFC 7578) with this boundary: the Content-Type # field value to send and an octet body, or why the boundary cannot be used -# (SPEC.md HTTP-FORM) +# (laws/FormRefusesBadRequests.bend) def form_body(boundary: String, parts: List<&1, Form.Upload>) -> Form.Form: # noqa: L001 re-export of Form.form.body Form.form.body(boundary, parts) @@ -185,15 +185,16 @@ def html_escape(text: String) -> String: # noqa: L001 re-export of Html.escape # HTML character references back to characters, as Python's html.unescape: # decimal and hex numeric references (with the HTML Standard's replacements) -# and the HTML Standard's 2231 named references, the legacy ones also without -# `;`; anything that is not a reference is kept as written (SPEC.md HTTP-REF) +# and the HTML Standard's 2231 named references, the legacy ones also +# without `;`; anything that is not a reference is kept as written +# (laws/UnescapeFindsReferences.bend) def html_unescape(text: String) -> String: # noqa: L001 re-export of Unescape.unescape Unescape.unescape(text) # the Cookie header value to send back after these Set-Cookie field values # (RFC 6265 §5.2, §5.4): each one's name=value, attributes ignored; a later # cookie with the same name replaces the earlier one's value in its place; -# `name=value` pairs joined by `; ` (SPEC.md HTTP-CK) +# `name=value` pairs joined by `; ` (laws/CookieHeaderEchoesStore.bend) def cookie_from_set_cookies(values: List<&2, String>) -> String: # noqa: L001 re-export of Echo.echo Echo.echo(values) @@ -270,7 +271,7 @@ def http.timed_out(r: Client.Response) -> Bool: Client.client.timed_out(r) # a response body's octets (a Response's `bytes`) decoded as UTF-8, done only -# when called (SPEC.md HTTP-BODY-4) +# when called (laws/OctetsDecodeAsUtf8.bend) def body_text(bytes: Bytes.Bytes) -> String: # noqa: L001 re-export of Client.client.text Client.client.text(bytes) @@ -355,7 +356,8 @@ def http.serve_io( # listen on host:port and serve up to `limit` connections, each on its own # fork and at most `max` at once (0: no limit), calling a pure handler with # `ctx` and the request (Server.Req: text, and its body's octets); returns -# once every connection is closed (SPEC.md HTTP-SRV, HTTP-TRUST-2) +# once every connection is closed (laws/BadRequestAnswers400.bend, trusted +# in src/server.bend) def http.serve_with( ~C: Data, ~handle: C -> Server.Req -> Server.Answer, diff --git a/proofs/AbsoluteFormTargetRejected.bend b/proofs/AbsoluteFormTargetRejected.bend new file mode 100644 index 0000000..099609d --- /dev/null +++ b/proofs/AbsoluteFormTargetRejected.bend @@ -0,0 +1,6 @@ +# proofs of laws/AbsoluteFormTargetRejected.bend +import Base +import ../laws/AbsoluteFormTargetRejected.bend as Laws + +def Laws.ask_rejects_absolute_form(): + {==} diff --git a/proofs/AbsoluteLocationsResolve.bend b/proofs/AbsoluteLocationsResolve.bend new file mode 100644 index 0000000..34212f8 --- /dev/null +++ b/proofs/AbsoluteLocationsResolve.bend @@ -0,0 +1,79 @@ +# proofs of laws/AbsoluteLocationsResolve.bend +import Base +import ../src/redirect.bend as Redirect +import ../laws/AbsoluteLocationsResolve.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.target_absolute(url, loc, e): + %e : {Redirect.redirect.target.of(url, loc, _) == Some{loc} + : Maybe<&2, String>} + {==} + +# --- redirect targets --- + +def Laws.target_bad(url, loc, e1, _why, e2): + %e1 : {Redirect.redirect.target.of(url, loc, _) == None{} + : Maybe<&2, String>} + %e2 : {Redirect.redirect.at(_, loc) == None{} : Maybe<&2, String>} + {==} + +def Laws.against_net(_scheme, _host, _port, _path, _loc): + {==} + +def Laws.against_abs(_scheme, _host, _port, _path, _loc): + {==} + +def Laws.against_stay(_scheme, _host, _port, _path, _loc): + {==} + +def Laws.sort_net(loc, e): + %e : {Redirect.redirect.sort.of(_, String.starts_with(loc, "/"), + Redirect.redirect.stays(loc), String.starts_with(loc, "?")) == Redirect.Net{} : Redirect.Sort} + {==} + +def Laws.sort_abs(loc, e1, e2): + %e1 : {Redirect.redirect.sort.of(_, String.starts_with(loc, "/"), + Redirect.redirect.stays(loc), String.starts_with(loc, "?")) == Redirect.Abs{} : Redirect.Sort} + %e2 : {Redirect.redirect.sort.abs(_, Redirect.redirect.stays(loc), + String.starts_with(loc, "?")) == Redirect.Abs{} : Redirect.Sort} + {==} + +def Laws.sort_stay(loc, e1, e2, e3): + %e1 : {Redirect.redirect.sort.of(_, String.starts_with(loc, "/"), + Redirect.redirect.stays(loc), String.starts_with(loc, "?")) == Redirect.Stay{} : Redirect.Sort} + %e2 : {Redirect.redirect.sort.abs(_, Redirect.redirect.stays(loc), + String.starts_with(loc, "?")) == Redirect.Stay{} : Redirect.Sort} + %e3 : {Redirect.redirect.sort.stay(_, String.starts_with(loc, "?")) == Redirect.Stay{} : Redirect.Sort} + {==} + +def Laws.stays_on(_loc): + {==} + +def Laws.schemed_nil(s, e): + match s: + case SNil{}: + {==} + case SCon{h, t}: + M1.eq.bit.absurd(&2, {Redirect.redirect.schemed(SCon{h, t}) == False{} : Bool}, + Equal.sym(Bool, True{}, False{}, e)) + +def Laws.schemed_colon(_t): + {==} + +def Laws.schemed_stop(c, t, e): + %e : {Redirect.redirect.schemed.at(_, Char.is_eq(c, ':'), + _u => Redirect.redirect.schemed(t)) == False{} : Bool} + {==} + +def Laws.schemed_other(c, t, e1, e2): + %e2 : {Redirect.redirect.schemed.at(_, Char.is_eq(c, ':'), + _u => Redirect.redirect.schemed(t)) == Redirect.redirect.schemed(t) : Bool} + %e1 : {Redirect.redirect.schemed.colon(_, _u => Redirect.redirect.schemed(t)) + == Redirect.redirect.schemed(t) : Bool} + {==} + +def Laws.schemed_end(_c): + {==} + +def Laws.abs_of(_path, _query): + {==} diff --git a/proofs/BadRequestAnswers400.bend b/proofs/BadRequestAnswers400.bend new file mode 100644 index 0000000..6bab78e --- /dev/null +++ b/proofs/BadRequestAnswers400.bend @@ -0,0 +1,11 @@ +# proofs of laws/BadRequestAnswers400.bend +import Base +import ../laws/BadRequestAnswers400.bend as Laws + +# --- the server's decisions --- + +def Laws.step_bad(_why): + {==} + +def Laws.step_text_bad(_why): + {==} diff --git a/proofs/BadRequestShowIsMarked.bend b/proofs/BadRequestShowIsMarked.bend new file mode 100644 index 0000000..2977444 --- /dev/null +++ b/proofs/BadRequestShowIsMarked.bend @@ -0,0 +1,7 @@ +# proofs of laws/BadRequestShowIsMarked.bend +import Base +import ../laws/BadRequestShowIsMarked.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.ask_show_bad_is_marked(why): + M1.eq.starts_nil(why) diff --git a/proofs/BareRepliesWriteHeadOnly.bend b/proofs/BareRepliesWriteHeadOnly.bend new file mode 100644 index 0000000..842bbbf --- /dev/null +++ b/proofs/BareRepliesWriteHeadOnly.bend @@ -0,0 +1,15 @@ +# proofs of laws/BareRepliesWriteHeadOnly.bend +import Base +import ../src/http.bend as Http +import ../laws/BareRepliesWriteHeadOnly.bend as Laws + +def Laws.bare_head(_status): + {==} + +def Laws.bare_other(_method, e, status): + %e : {Bool.or(_, Http.status.nocontent(status)) == Http.status.nocontent(status) + : Bool} + {==} + +def Laws.nocontent_is(_code): + {==} diff --git a/proofs/Base64EmptyIsEmpty.bend b/proofs/Base64EmptyIsEmpty.bend new file mode 100644 index 0000000..4f4af82 --- /dev/null +++ b/proofs/Base64EmptyIsEmpty.bend @@ -0,0 +1,6 @@ +# proofs of laws/Base64EmptyIsEmpty.bend +import Base +import ../laws/Base64EmptyIsEmpty.bend as Laws + +def Laws.b64_empty(): + {==} diff --git a/proofs/Base64EncodesF.bend b/proofs/Base64EncodesF.bend new file mode 100644 index 0000000..ce77420 --- /dev/null +++ b/proofs/Base64EncodesF.bend @@ -0,0 +1,6 @@ +# proofs of laws/Base64EncodesF.bend +import Base +import ../laws/Base64EncodesF.bend as Laws + +def Laws.b64_f(): + {==} diff --git a/proofs/Base64EncodesFo.bend b/proofs/Base64EncodesFo.bend new file mode 100644 index 0000000..e47afd2 --- /dev/null +++ b/proofs/Base64EncodesFo.bend @@ -0,0 +1,6 @@ +# proofs of laws/Base64EncodesFo.bend +import Base +import ../laws/Base64EncodesFo.bend as Laws + +def Laws.b64_fo(): + {==} diff --git a/proofs/Base64EncodesFoo.bend b/proofs/Base64EncodesFoo.bend new file mode 100644 index 0000000..7861a11 --- /dev/null +++ b/proofs/Base64EncodesFoo.bend @@ -0,0 +1,6 @@ +# proofs of laws/Base64EncodesFoo.bend +import Base +import ../laws/Base64EncodesFoo.bend as Laws + +def Laws.b64_foo(): + {==} diff --git a/proofs/Base64EncodesUserPass.bend b/proofs/Base64EncodesUserPass.bend new file mode 100644 index 0000000..4945cc9 --- /dev/null +++ b/proofs/Base64EncodesUserPass.bend @@ -0,0 +1,6 @@ +# proofs of laws/Base64EncodesUserPass.bend +import Base +import ../laws/Base64EncodesUserPass.bend as Laws + +def Laws.b64_user_pass(): + {==} diff --git a/proofs/Base64PadsFinalQuantum.bend b/proofs/Base64PadsFinalQuantum.bend new file mode 100644 index 0000000..4f20979 --- /dev/null +++ b/proofs/Base64PadsFinalQuantum.bend @@ -0,0 +1,7 @@ +# proofs of laws/Base64PadsFinalQuantum.bend +import Base +import ../laws/Base64PadsFinalQuantum.bend as Laws + +# b64_go_end: go matches the input first +def Laws.b64_go_end(_phase): + {==} diff --git a/proofs/BasicIsBase64OfUserPassword.bend b/proofs/BasicIsBase64OfUserPassword.bend new file mode 100644 index 0000000..92df0b4 --- /dev/null +++ b/proofs/BasicIsBase64OfUserPassword.bend @@ -0,0 +1,6 @@ +# proofs of laws/BasicIsBase64OfUserPassword.bend +import Base +import ../laws/BasicIsBase64OfUserPassword.bend as Laws + +def Laws.basic_user_pass(): + {==} diff --git a/proofs/BearerIsAuthorization.bend b/proofs/BearerIsAuthorization.bend new file mode 100644 index 0000000..07ce91c --- /dev/null +++ b/proofs/BearerIsAuthorization.bend @@ -0,0 +1,6 @@ +# proofs of laws/BearerIsAuthorization.bend +import Base +import ../laws/BearerIsAuthorization.bend as Laws + +def Laws.bearer_is_authorization(_token): + {==} diff --git a/proofs/BearerTokenAcceptsPlain.bend b/proofs/BearerTokenAcceptsPlain.bend new file mode 100644 index 0000000..5e576bd --- /dev/null +++ b/proofs/BearerTokenAcceptsPlain.bend @@ -0,0 +1,6 @@ +# proofs of laws/BearerTokenAcceptsPlain.bend +import Base +import ../laws/BearerTokenAcceptsPlain.bend as Laws + +def Laws.token_ok_plain(): + {==} diff --git a/proofs/BearerTokenRejectsNewline.bend b/proofs/BearerTokenRejectsNewline.bend new file mode 100644 index 0000000..74b7efa --- /dev/null +++ b/proofs/BearerTokenRejectsNewline.bend @@ -0,0 +1,6 @@ +# proofs of laws/BearerTokenRejectsNewline.bend +import Base +import ../laws/BearerTokenRejectsNewline.bend as Laws + +def Laws.token_ok_newline(): + {==} diff --git a/proofs/BearerTokenRejectsSpace.bend b/proofs/BearerTokenRejectsSpace.bend new file mode 100644 index 0000000..f5e0d78 --- /dev/null +++ b/proofs/BearerTokenRejectsSpace.bend @@ -0,0 +1,6 @@ +# proofs of laws/BearerTokenRejectsSpace.bend +import Base +import ../laws/BearerTokenRejectsSpace.bend as Laws + +def Laws.token_ok_space(): + {==} diff --git a/proofs/BearerTokenRejectsTab.bend b/proofs/BearerTokenRejectsTab.bend new file mode 100644 index 0000000..fbccdb9 --- /dev/null +++ b/proofs/BearerTokenRejectsTab.bend @@ -0,0 +1,6 @@ +# proofs of laws/BearerTokenRejectsTab.bend +import Base +import ../laws/BearerTokenRejectsTab.bend as Laws + +def Laws.token_ok_tab(): + {==} diff --git a/proofs/CacheDirectiveNamesIgnoreCase.bend b/proofs/CacheDirectiveNamesIgnoreCase.bend new file mode 100644 index 0000000..2b7bb0e --- /dev/null +++ b/proofs/CacheDirectiveNamesIgnoreCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheDirectiveNamesIgnoreCase.bend +import Base +import ../laws/CacheDirectiveNamesIgnoreCase.bend as Laws + +def Laws.cache_directive_case(): + {==} diff --git a/proofs/CacheIgnoresUnknownDirective.bend b/proofs/CacheIgnoresUnknownDirective.bend new file mode 100644 index 0000000..a2e5a33 --- /dev/null +++ b/proofs/CacheIgnoresUnknownDirective.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheIgnoresUnknownDirective.bend +import Base +import ../laws/CacheIgnoresUnknownDirective.bend as Laws + +def Laws.cache_ignores_s_maxage(): + {==} diff --git a/proofs/CacheMaxAgeIgnoresJunk.bend b/proofs/CacheMaxAgeIgnoresJunk.bend new file mode 100644 index 0000000..7872dc5 --- /dev/null +++ b/proofs/CacheMaxAgeIgnoresJunk.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheMaxAgeIgnoresJunk.bend +import Base +import ../laws/CacheMaxAgeIgnoresJunk.bend as Laws + +def Laws.cache_max_age_ignores_junk(): + {==} diff --git a/proofs/CacheReadsKnownDirectives.bend b/proofs/CacheReadsKnownDirectives.bend new file mode 100644 index 0000000..b00cc74 --- /dev/null +++ b/proofs/CacheReadsKnownDirectives.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheReadsKnownDirectives.bend +import Base +import ../laws/CacheReadsKnownDirectives.bend as Laws + +def Laws.cache_directives(): + {==} diff --git a/proofs/CacheRecordsExpires.bend b/proofs/CacheRecordsExpires.bend new file mode 100644 index 0000000..7c8c191 --- /dev/null +++ b/proofs/CacheRecordsExpires.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheRecordsExpires.bend +import Base +import ../laws/CacheRecordsExpires.bend as Laws + +def Laws.cache_note_expires(): + {==} diff --git a/proofs/CacheRecordsMaxAge.bend b/proofs/CacheRecordsMaxAge.bend new file mode 100644 index 0000000..bad0e9f --- /dev/null +++ b/proofs/CacheRecordsMaxAge.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheRecordsMaxAge.bend +import Base +import ../laws/CacheRecordsMaxAge.bend as Laws + +def Laws.cache_max_age_present(): + {==} diff --git a/proofs/CacheRecordsNoCache.bend b/proofs/CacheRecordsNoCache.bend new file mode 100644 index 0000000..3fea205 --- /dev/null +++ b/proofs/CacheRecordsNoCache.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheRecordsNoCache.bend +import Base +import ../laws/CacheRecordsNoCache.bend as Laws + +def Laws.cache_no_cache_alone(): + {==} diff --git a/proofs/CacheRecordsPrivate.bend b/proofs/CacheRecordsPrivate.bend new file mode 100644 index 0000000..e3e577b --- /dev/null +++ b/proofs/CacheRecordsPrivate.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheRecordsPrivate.bend +import Base +import ../laws/CacheRecordsPrivate.bend as Laws + +def Laws.cache_private_alone(): + {==} diff --git a/proofs/ChunkDataUnchanged.bend b/proofs/ChunkDataUnchanged.bend new file mode 100644 index 0000000..aa5c7b8 --- /dev/null +++ b/proofs/ChunkDataUnchanged.bend @@ -0,0 +1,51 @@ +# proofs of laws/ChunkDataUnchanged.bend +import Base +import ../src/http.bend as Http +import ../laws/ChunkDataUnchanged.bend as Laws +import ../laws/OneOctetPerChar.bend as L1 +import ./lemmas/DropNothing.bend as M1 +import ./lemmas/OctetTakeDrop.bend as M2 + +# what follows |b| + k characters of b ++ r is what follows k of r +def drop_prefix(b: String, r: String, k: Nat) + -> {String.drop(String.append(b, r), Nat.add(String.length(b), k)) + == String.drop(r, k) : String}: + match b: + case SNil{}: + {==} + case SCon{h, t}: + drop_prefix(t, r, k) + +def Laws.chunk_data(f, h, d, r, e, el): + +a = String.append(d, "\r\n" ++ r) + +rhs = String.append(d, Http.chunks(f, Http.divide(r, "\r\n"))) + %e : {Http.chunks.more(Http.hexlen(h), a, + _u => Http.chunks(f, Http.divide(Http.octs.drop(a, + Nat.add(Http.hexlen(h), 2n)), "\r\n")), _) == rhs : String} + %el : {String.append(Http.octs.take(a, _), + Http.chunks(f, Http.divide(Http.octs.drop(a, Nat.add(_, 2n)), "\r\n"))) + == rhs : String} + +n = String.length(d) + %Equal.sym(String, Http.octs.take(a, n), String.take(a, n), + L1.octs_take(a, n)) + : {String.append(_, Http.chunks(f, Http.divide(Http.octs.drop(a, + Nat.add(n, 2n)), "\r\n"))) == rhs : String} + %Equal.sym(String, String.take(a, n), d, M2.take_prefix(d, "\r\n" ++ r)) + : {String.append(_, Http.chunks(f, Http.divide(Http.octs.drop(a, + Nat.add(n, 2n)), "\r\n"))) == rhs : String} + %Equal.sym(String, Http.octs.drop(a, Nat.add(n, 2n)), + String.drop(a, Nat.add(n, 2n)), L1.octs_drop(a, Nat.add(n, 2n))) + : {String.append(d, Http.chunks(f, Http.divide(_, "\r\n"))) == rhs + : String} + %Equal.sym(String, String.drop(a, Nat.add(n, 2n)), r, + Equal.trans(String, String.drop(a, Nat.add(n, 2n)), String.drop(r, 0n), r, + drop_prefix(d, "\r\n" ++ r, 2n), M1.eq.drop_zero(r))) + : {String.append(d, Http.chunks(f, Http.divide(_, "\r\n"))) == rhs + : String} + {==} + +def Laws.chunk_last(_f, h, after, e): + %e : {Http.chunks.more(Http.hexlen(h), after, + _u => Http.chunks(_f, Http.divide(Http.octs.drop(after, + Nat.add(Http.hexlen(h), 2n)), "\r\n")), _) == "" : String} + {==} diff --git a/proofs/ChunkSizeHexIgnoresCase.bend b/proofs/ChunkSizeHexIgnoresCase.bend new file mode 100644 index 0000000..9481136 --- /dev/null +++ b/proofs/ChunkSizeHexIgnoresCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/ChunkSizeHexIgnoresCase.bend +import Base +import ../laws/ChunkSizeHexIgnoresCase.bend as Laws + +def Laws.hexlen_alpha(): + {==} diff --git a/proofs/ChunkSizeIgnoresExtension.bend b/proofs/ChunkSizeIgnoresExtension.bend new file mode 100644 index 0000000..e0b2025 --- /dev/null +++ b/proofs/ChunkSizeIgnoresExtension.bend @@ -0,0 +1,21 @@ +# proofs of laws/ChunkSizeIgnoresExtension.bend +import Base +import ../src/http.bend as Http +import ../laws/ChunkSizeIgnoresExtension.bend as Laws +import ../laws/SplitAtCharacter.bend as L1 +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.hexlen_ignores_extension(s, ext, e): + Equal.trans(Nat, + Http.hexlen(String.append(s, SCon{';', ext})), + Http.hex.go(String.trim(s), 0n), + Http.hexlen(s), + Equal.cong(List<&2, String>, Nat, + ps => Http.hex.go(String.trim(Http.first(ps)), 0n), + String.split(String.append(s, SCon{';', ext}), ';'), + s <> String.split(ext, ';'), + L1.split_cut(s, ';', ext, e)), + Equal.sym(Nat, Http.hexlen(s), Http.hex.go(String.trim(s), 0n), + Equal.cong(List<&2, String>, Nat, + ps => Http.hex.go(String.trim(Http.first(ps)), 0n), + String.split(s, ';'), [s], L1.split_whole(s, ';', e)))) diff --git a/proofs/ChunkSizeReadsHexSkipsExtension.bend b/proofs/ChunkSizeReadsHexSkipsExtension.bend new file mode 100644 index 0000000..fc66697 --- /dev/null +++ b/proofs/ChunkSizeReadsHexSkipsExtension.bend @@ -0,0 +1,6 @@ +# proofs of laws/ChunkSizeReadsHexSkipsExtension.bend +import Base +import ../laws/ChunkSizeReadsHexSkipsExtension.bend as Laws + +def Laws.hexlen_extension_and_case(): + {==} diff --git a/proofs/ChunkedBodyJoinsChunks.bend b/proofs/ChunkedBodyJoinsChunks.bend new file mode 100644 index 0000000..9625d9a --- /dev/null +++ b/proofs/ChunkedBodyJoinsChunks.bend @@ -0,0 +1,6 @@ +# proofs of laws/ChunkedBodyJoinsChunks.bend +import Base +import ../laws/ChunkedBodyJoinsChunks.bend as Laws + +def Laws.parse_chunked_body(): + {==} diff --git a/proofs/ChunkedNameIgnoresCase.bend b/proofs/ChunkedNameIgnoresCase.bend new file mode 100644 index 0000000..e32543c --- /dev/null +++ b/proofs/ChunkedNameIgnoresCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/ChunkedNameIgnoresCase.bend +import Base +import ../laws/ChunkedNameIgnoresCase.bend as Laws + +def Laws.parse_chunked_ignores_case(): + {==} diff --git a/proofs/ChunksStopWithoutFuel.bend b/proofs/ChunksStopWithoutFuel.bend new file mode 100644 index 0000000..ccaa672 --- /dev/null +++ b/proofs/ChunksStopWithoutFuel.bend @@ -0,0 +1,6 @@ +# proofs of laws/ChunksStopWithoutFuel.bend +import Base +import ../laws/ChunksStopWithoutFuel.bend as Laws + +def Laws.chunks_empty_fuel(): + {==} diff --git a/proofs/ContentLengthFramesBody.bend b/proofs/ContentLengthFramesBody.bend new file mode 100644 index 0000000..11c8287 --- /dev/null +++ b/proofs/ContentLengthFramesBody.bend @@ -0,0 +1,43 @@ +# proofs of laws/ContentLengthFramesBody.bend +import Base +import ../src/http.bend as Http +import ../laws/ContentLengthFramesBody.bend as Laws +import ../laws/OneOctetPerChar.bend as L1 +import ./lemmas/OctetLength.bend as M1 +import ./lemmas/OctetTakeDrop.bend as M2 + +def Laws.clen_short(st, hs, n, body, e): + %e : {Http.parse.short(st, hs, n, body, _) + == Http.Torn{"the response body stopped before its Content-Length"} + : Http.Reply} + {==} + +# b ++ r is never shorter than b +def lt_prefix(b: String, r: String) + -> {Nat.is_lt(String.length(String.append(b, r)), String.length(b)) + == False{} : Bool}: + match b: + case SNil{}: + match r: + case SNil{}: + {==} + case SCon{h, t}: + {==} + case SCon{h, t}: + lt_prefix(t, r) + +def Laws.clen_prefix(st, hs, b, r): + +br = String.append(b, r) + +n = String.length(b) + %Equal.sym(Nat, Http.octs.len(br), String.length(br), L1.octs_len(br)) + : {Http.parse.short(st, hs, n, br, Nat.is_lt(_, n)) + == Http.Reply{st, hs, b} : Http.Reply} + %Equal.sym(Bool, Nat.is_lt(String.length(br), n), False{}, lt_prefix(b, r)) + : {Http.parse.short(st, hs, n, br, _) == Http.Reply{st, hs, b} + : Http.Reply} + %Equal.sym(String, Http.octs.take(br, n), String.take(br, n), + L1.octs_take(br, n)) + : {Http.Reply{st, hs, _} == Http.Reply{st, hs, b} : Http.Reply} + %Equal.sym(String, String.take(br, n), b, M2.take_prefix(b, r)) + : {Http.Reply{st, hs, _} == Http.Reply{st, hs, b} : Http.Reply} + {==} diff --git a/proofs/ContentLengthReadsDecimal.bend b/proofs/ContentLengthReadsDecimal.bend new file mode 100644 index 0000000..35933b3 --- /dev/null +++ b/proofs/ContentLengthReadsDecimal.bend @@ -0,0 +1,6 @@ +# proofs of laws/ContentLengthReadsDecimal.bend +import Base +import ../laws/ContentLengthReadsDecimal.bend as Laws + +def Laws.digits_read_decimal(): + {==} diff --git a/proofs/ContentLengthRejectsJunk.bend b/proofs/ContentLengthRejectsJunk.bend new file mode 100644 index 0000000..b08f062 --- /dev/null +++ b/proofs/ContentLengthRejectsJunk.bend @@ -0,0 +1,6 @@ +# proofs of laws/ContentLengthRejectsJunk.bend +import Base +import ../laws/ContentLengthRejectsJunk.bend as Laws + +def Laws.digits_read_rejects_junk(): + {==} diff --git a/proofs/ContentLengthTakesPrefix.bend b/proofs/ContentLengthTakesPrefix.bend new file mode 100644 index 0000000..92bffd2 --- /dev/null +++ b/proofs/ContentLengthTakesPrefix.bend @@ -0,0 +1,6 @@ +# proofs of laws/ContentLengthTakesPrefix.bend +import Base +import ../laws/ContentLengthTakesPrefix.bend as Laws + +def Laws.parse_clen_takes_prefix(): + {==} diff --git a/proofs/ContinuationLineRejected.bend b/proofs/ContinuationLineRejected.bend new file mode 100644 index 0000000..b8bef6a --- /dev/null +++ b/proofs/ContinuationLineRejected.bend @@ -0,0 +1,6 @@ +# proofs of laws/ContinuationLineRejected.bend +import Base +import ../laws/ContinuationLineRejected.bend as Laws + +def Laws.head_line_rejects_continuation(): + {==} diff --git a/proofs/CookieAliveUnderMaxAge.bend b/proofs/CookieAliveUnderMaxAge.bend new file mode 100644 index 0000000..30a0a00 --- /dev/null +++ b/proofs/CookieAliveUnderMaxAge.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieAliveUnderMaxAge.bend +import Base +import ../laws/CookieAliveUnderMaxAge.bend as Laws + +def Laws.cookie_max_age_open(): + {==} diff --git a/proofs/CookieAttributesJoinWithSemicolons.bend b/proofs/CookieAttributesJoinWithSemicolons.bend new file mode 100644 index 0000000..97d2d35 --- /dev/null +++ b/proofs/CookieAttributesJoinWithSemicolons.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieAttributesJoinWithSemicolons.bend +import Base +import ../laws/CookieAttributesJoinWithSemicolons.bend as Laws + +def Laws.cookie_line_attributes(): + {==} diff --git a/proofs/CookieDefaultPathFallsBackToRoot.bend b/proofs/CookieDefaultPathFallsBackToRoot.bend new file mode 100644 index 0000000..61299b3 --- /dev/null +++ b/proofs/CookieDefaultPathFallsBackToRoot.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDefaultPathFallsBackToRoot.bend +import Base +import ../laws/CookieDefaultPathFallsBackToRoot.bend as Laws + +def Laws.cookie_default_path_root(): + {==} diff --git a/proofs/CookieDefaultPathIsDirectory.bend b/proofs/CookieDefaultPathIsDirectory.bend new file mode 100644 index 0000000..7cc3daf --- /dev/null +++ b/proofs/CookieDefaultPathIsDirectory.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDefaultPathIsDirectory.bend +import Base +import ../laws/CookieDefaultPathIsDirectory.bend as Laws + +def Laws.cookie_default_path_dir(): + {==} diff --git a/proofs/CookieDefaultPathNeedsTwoSlashes.bend b/proofs/CookieDefaultPathNeedsTwoSlashes.bend new file mode 100644 index 0000000..b123155 --- /dev/null +++ b/proofs/CookieDefaultPathNeedsTwoSlashes.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDefaultPathNeedsTwoSlashes.bend +import Base +import ../laws/CookieDefaultPathNeedsTwoSlashes.bend as Laws + +def Laws.cookie_default_path_one_slash(): + {==} diff --git a/proofs/CookieDefaultPathOmitsTrailingSlash.bend b/proofs/CookieDefaultPathOmitsTrailingSlash.bend new file mode 100644 index 0000000..50c45bf --- /dev/null +++ b/proofs/CookieDefaultPathOmitsTrailingSlash.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDefaultPathOmitsTrailingSlash.bend +import Base +import ../laws/CookieDefaultPathOmitsTrailingSlash.bend as Laws + +def Laws.cookie_default_path_trailing(): + {==} diff --git a/proofs/CookieDomainLowercased.bend b/proofs/CookieDomainLowercased.bend new file mode 100644 index 0000000..94feed2 --- /dev/null +++ b/proofs/CookieDomainLowercased.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDomainLowercased.bend +import Base +import ../laws/CookieDomainLowercased.bend as Laws + +def Laws.cookie_domain_lowercased(): + {==} diff --git a/proofs/CookieDomainMatchesDotSuffix.bend b/proofs/CookieDomainMatchesDotSuffix.bend new file mode 100644 index 0000000..7ee049d --- /dev/null +++ b/proofs/CookieDomainMatchesDotSuffix.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDomainMatchesDotSuffix.bend +import Base +import ../laws/CookieDomainMatchesDotSuffix.bend as Laws + +def Laws.cookie_domain_suffix(): + {==} diff --git a/proofs/CookieDomainNeedsDotBeforeSuffix.bend b/proofs/CookieDomainNeedsDotBeforeSuffix.bend new file mode 100644 index 0000000..e0e10df --- /dev/null +++ b/proofs/CookieDomainNeedsDotBeforeSuffix.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDomainNeedsDotBeforeSuffix.bend +import Base +import ../laws/CookieDomainNeedsDotBeforeSuffix.bend as Laws + +def Laws.cookie_domain_not_suffix(): + {==} diff --git a/proofs/CookieHeaderEchoesStore.bend b/proofs/CookieHeaderEchoesStore.bend new file mode 100644 index 0000000..2aebf9c --- /dev/null +++ b/proofs/CookieHeaderEchoesStore.bend @@ -0,0 +1,50 @@ +# proofs of laws/CookieHeaderEchoesStore.bend +import Base +import ../src/qs.bend as Qs +import ../src/echo.bend as Echo +import ../laws/CookieHeaderEchoesStore.bend as Laws + +def Laws.ck_echo(_lines): + {==} + +def Laws.ck_store_nil(_jar): + {==} + +def Laws.ck_store_cons(_l, _t, _jar): + {==} + +def Laws.ck_add_none(_jar): + {==} + +def Laws.ck_add_some(_jar, _name, _value): + {==} + +def Laws.ck_put_nil(_name, _value): + {==} + +def Laws.ck_put_hit(key, old, t, name, value, e): + %e : {Echo.put.at(key, old, value, t, _, _u => Echo.put(t, name, value)) + == (key, value) <> t : List<&2, Qs.Kv()>} + {==} + +def Laws.ck_put_miss(key, old, t, name, value, e): + %e : {Echo.put.at(key, old, value, t, _, _u => Echo.put(t, name, value)) + == (key, old) <> Echo.put(t, name, value) : List<&2, Qs.Kv()>} + {==} + +def Laws.ck_line_nil(_jar, e): + %e : {Echo.line(_) == "" : String} + {==} + +def Laws.ck_line_cons(_name, _value, _t): + {==} + +def Laws.ck_more_nil(_jar, e): + %e : {Echo.more(_) == "" : String} + {==} + +def Laws.ck_more_cons(_name, _value, _t): + {==} + +def Laws.ck_one(_name, _value): + {==} diff --git a/proofs/CookieHostOnlyNotSentElsewhere.bend b/proofs/CookieHostOnlyNotSentElsewhere.bend new file mode 100644 index 0000000..7d8be16 --- /dev/null +++ b/proofs/CookieHostOnlyNotSentElsewhere.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieHostOnlyNotSentElsewhere.bend +import Base +import ../laws/CookieHostOnlyNotSentElsewhere.bend as Laws + +def Laws.cookie_send_other_host(): + {==} diff --git a/proofs/CookieHostOnlySentToHost.bend b/proofs/CookieHostOnlySentToHost.bend new file mode 100644 index 0000000..46d35cd --- /dev/null +++ b/proofs/CookieHostOnlySentToHost.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieHostOnlySentToHost.bend +import Base +import ../laws/CookieHostOnlySentToHost.bend as Laws + +def Laws.cookie_send_host_only(): + {==} diff --git a/proofs/CookieHostOnlyStoresHost.bend b/proofs/CookieHostOnlyStoresHost.bend new file mode 100644 index 0000000..373ac1a --- /dev/null +++ b/proofs/CookieHostOnlyStoresHost.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieHostOnlyStoresHost.bend +import Base +import ../laws/CookieHostOnlyStoresHost.bend as Laws + +def Laws.cookie_receive_host_only(): + {==} diff --git a/proofs/CookieIdenticalPathsMatch.bend b/proofs/CookieIdenticalPathsMatch.bend new file mode 100644 index 0000000..f02889a --- /dev/null +++ b/proofs/CookieIdenticalPathsMatch.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieIdenticalPathsMatch.bend +import Base +import ../laws/CookieIdenticalPathsMatch.bend as Laws + +def Laws.cookie_path_identical(): + {==} diff --git a/proofs/CookieInsecureSentOnCleartext.bend b/proofs/CookieInsecureSentOnCleartext.bend new file mode 100644 index 0000000..e8165ed --- /dev/null +++ b/proofs/CookieInsecureSentOnCleartext.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieInsecureSentOnCleartext.bend +import Base +import ../laws/CookieInsecureSentOnCleartext.bend as Laws + +def Laws.cookie_insecure_on_tcp(): + {==} diff --git a/proofs/CookieIpHostMatchesExactly.bend b/proofs/CookieIpHostMatchesExactly.bend new file mode 100644 index 0000000..4a6b21f --- /dev/null +++ b/proofs/CookieIpHostMatchesExactly.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieIpHostMatchesExactly.bend +import Base +import ../laws/CookieIpHostMatchesExactly.bend as Laws + +def Laws.cookie_domain_ip_exact(): + {==} diff --git a/proofs/CookieIpHostNoSuffixMatch.bend b/proofs/CookieIpHostNoSuffixMatch.bend new file mode 100644 index 0000000..58026b8 --- /dev/null +++ b/proofs/CookieIpHostNoSuffixMatch.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieIpHostNoSuffixMatch.bend +import Base +import ../laws/CookieIpHostNoSuffixMatch.bend as Laws + +def Laws.cookie_domain_ip_suffix(): + {==} diff --git a/proofs/CookieLongerPathFails.bend b/proofs/CookieLongerPathFails.bend new file mode 100644 index 0000000..8cab393 --- /dev/null +++ b/proofs/CookieLongerPathFails.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieLongerPathFails.bend +import Base +import ../laws/CookieLongerPathFails.bend as Laws + +def Laws.cookie_path_longer_cookie(): + {==} diff --git a/proofs/CookieMaxAgeIgnoresJunk.bend b/proofs/CookieMaxAgeIgnoresJunk.bend new file mode 100644 index 0000000..19584b6 --- /dev/null +++ b/proofs/CookieMaxAgeIgnoresJunk.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieMaxAgeIgnoresJunk.bend +import Base +import ../laws/CookieMaxAgeIgnoresJunk.bend as Laws + +def Laws.cookie_max_age_ignores_junk(): + {==} diff --git a/proofs/CookieMaxAgeOneStaleAtOne.bend b/proofs/CookieMaxAgeOneStaleAtOne.bend new file mode 100644 index 0000000..6a46d11 --- /dev/null +++ b/proofs/CookieMaxAgeOneStaleAtOne.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieMaxAgeOneStaleAtOne.bend +import Base +import ../laws/CookieMaxAgeOneStaleAtOne.bend as Laws + +def Laws.cookie_max_age_one(): + {==} diff --git a/proofs/CookieMaxAgeSetsAge.bend b/proofs/CookieMaxAgeSetsAge.bend new file mode 100644 index 0000000..619cd5b --- /dev/null +++ b/proofs/CookieMaxAgeSetsAge.bend @@ -0,0 +1,19 @@ +# proofs of laws/CookieMaxAgeSetsAge.bend +import Base +import ../laws/CookieMaxAgeSetsAge.bend as Laws + +# cookie_age_sets holds by unfolding cookie.age +def Laws.cookie_age_sets( + _name, + _value, + _expires, + _max_age, + _domain, + _path, + _secure, + _http_only, + _same_site, + _host_only, + _age +): + {==} diff --git a/proofs/CookieMaxAgeZeroNotAlive.bend b/proofs/CookieMaxAgeZeroNotAlive.bend new file mode 100644 index 0000000..d61f131 --- /dev/null +++ b/proofs/CookieMaxAgeZeroNotAlive.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieMaxAgeZeroNotAlive.bend +import Base +import ../laws/CookieMaxAgeZeroNotAlive.bend as Laws + +def Laws.cookie_max_age_zero(): + {==} diff --git a/proofs/CookieNameIsNotIpv4.bend b/proofs/CookieNameIsNotIpv4.bend new file mode 100644 index 0000000..cec2688 --- /dev/null +++ b/proofs/CookieNameIsNotIpv4.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieNameIsNotIpv4.bend +import Base +import ../laws/CookieNameIsNotIpv4.bend as Laws + +def Laws.cookie_ipv4_name(): + {==} diff --git a/proofs/CookieNoDomainNotSentElsewhere.bend b/proofs/CookieNoDomainNotSentElsewhere.bend new file mode 100644 index 0000000..65be8d4 --- /dev/null +++ b/proofs/CookieNoDomainNotSentElsewhere.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieNoDomainNotSentElsewhere.bend +import Base +import ../laws/CookieNoDomainNotSentElsewhere.bend as Laws + +def Laws.cookie_send_unset_domain(): + {==} diff --git a/proofs/CookiePathMatchesSlashPrefix.bend b/proofs/CookiePathMatchesSlashPrefix.bend new file mode 100644 index 0000000..4b1dfc6 --- /dev/null +++ b/proofs/CookiePathMatchesSlashPrefix.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookiePathMatchesSlashPrefix.bend +import Base +import ../laws/CookiePathMatchesSlashPrefix.bend as Laws + +def Laws.cookie_path_prefix(): + {==} diff --git a/proofs/CookiePathNeedsSegmentBoundary.bend b/proofs/CookiePathNeedsSegmentBoundary.bend new file mode 100644 index 0000000..6831334 --- /dev/null +++ b/proofs/CookiePathNeedsSegmentBoundary.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookiePathNeedsSegmentBoundary.bend +import Base +import ../laws/CookiePathNeedsSegmentBoundary.bend as Laws + +def Laws.cookie_path_not_prefix(): + {==} diff --git a/proofs/CookieRecognizesIpv4Literal.bend b/proofs/CookieRecognizesIpv4Literal.bend new file mode 100644 index 0000000..d49acfc --- /dev/null +++ b/proofs/CookieRecognizesIpv4Literal.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieRecognizesIpv4Literal.bend +import Base +import ../laws/CookieRecognizesIpv4Literal.bend as Laws + +def Laws.cookie_ipv4_literal(): + {==} diff --git a/proofs/CookieRejectsEmptyName.bend b/proofs/CookieRejectsEmptyName.bend new file mode 100644 index 0000000..9081dbe --- /dev/null +++ b/proofs/CookieRejectsEmptyName.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieRejectsEmptyName.bend +import Base +import ../laws/CookieRejectsEmptyName.bend as Laws + +def Laws.cookie_rejects_empty_name(): + {==} diff --git a/proofs/CookieRequestJoinsPairs.bend b/proofs/CookieRequestJoinsPairs.bend new file mode 100644 index 0000000..4cb6429 --- /dev/null +++ b/proofs/CookieRequestJoinsPairs.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieRequestJoinsPairs.bend +import Base +import ../laws/CookieRequestJoinsPairs.bend as Laws + +def Laws.cookie_request_joins(): + {==} diff --git a/proofs/CookieRootPathMatchesLonger.bend b/proofs/CookieRootPathMatchesLonger.bend new file mode 100644 index 0000000..930535e --- /dev/null +++ b/proofs/CookieRootPathMatchesLonger.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieRootPathMatchesLonger.bend +import Base +import ../laws/CookieRootPathMatchesLonger.bend as Laws + +def Laws.cookie_path_root(): + {==} diff --git a/proofs/CookieSameSiteFoldsCase.bend b/proofs/CookieSameSiteFoldsCase.bend new file mode 100644 index 0000000..073c130 --- /dev/null +++ b/proofs/CookieSameSiteFoldsCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSameSiteFoldsCase.bend +import Base +import ../laws/CookieSameSiteFoldsCase.bend as Laws + +def Laws.cookie_samesite_strict(): + {==} diff --git a/proofs/CookieSameSiteNoneCanonical.bend b/proofs/CookieSameSiteNoneCanonical.bend new file mode 100644 index 0000000..dd2cb96 --- /dev/null +++ b/proofs/CookieSameSiteNoneCanonical.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSameSiteNoneCanonical.bend +import Base +import ../laws/CookieSameSiteNoneCanonical.bend as Laws + +def Laws.cookie_samesite_none(): + {==} diff --git a/proofs/CookieSecureNeedsTls.bend b/proofs/CookieSecureNeedsTls.bend new file mode 100644 index 0000000..14560cc --- /dev/null +++ b/proofs/CookieSecureNeedsTls.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSecureNeedsTls.bend +import Base +import ../laws/CookieSecureNeedsTls.bend as Laws + +def Laws.cookie_secure_needs_tls(): + {==} diff --git a/proofs/CookieSecureSentOnTls.bend b/proofs/CookieSecureSentOnTls.bend new file mode 100644 index 0000000..6f275d5 --- /dev/null +++ b/proofs/CookieSecureSentOnTls.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSecureSentOnTls.bend +import Base +import ../laws/CookieSecureSentOnTls.bend as Laws + +def Laws.cookie_secure_on_tls(): + {==} diff --git a/proofs/CookieSessionStaysAlive.bend b/proofs/CookieSessionStaysAlive.bend new file mode 100644 index 0000000..216e09a --- /dev/null +++ b/proofs/CookieSessionStaysAlive.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSessionStaysAlive.bend +import Base +import ../laws/CookieSessionStaysAlive.bend as Laws + +def Laws.cookie_session_alive(): + {==} diff --git a/proofs/CookieSlashEndedPathIsPrefix.bend b/proofs/CookieSlashEndedPathIsPrefix.bend new file mode 100644 index 0000000..dc483cb --- /dev/null +++ b/proofs/CookieSlashEndedPathIsPrefix.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSlashEndedPathIsPrefix.bend +import Base +import ../laws/CookieSlashEndedPathIsPrefix.bend as Laws + +def Laws.cookie_path_slash_prefix(): + {==} diff --git a/proofs/CookieUnknownSameSiteDropped.bend b/proofs/CookieUnknownSameSiteDropped.bend new file mode 100644 index 0000000..8e5896e --- /dev/null +++ b/proofs/CookieUnknownSameSiteDropped.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieUnknownSameSiteDropped.bend +import Base +import ../laws/CookieUnknownSameSiteDropped.bend as Laws + +def Laws.cookie_samesite_unknown(): + {==} diff --git a/proofs/CorsAllowListReflectsOrigin.bend b/proofs/CorsAllowListReflectsOrigin.bend new file mode 100644 index 0000000..e347a09 --- /dev/null +++ b/proofs/CorsAllowListReflectsOrigin.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsAllowListReflectsOrigin.bend +import Base +import ../laws/CorsAllowListReflectsOrigin.bend as Laws + +def Laws.cors_explicit_origin(): + {==} diff --git a/proofs/CorsAllowedPreflightIs200.bend b/proofs/CorsAllowedPreflightIs200.bend new file mode 100644 index 0000000..5af59e9 --- /dev/null +++ b/proofs/CorsAllowedPreflightIs200.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsAllowedPreflightIs200.bend +import Base +import ../laws/CorsAllowedPreflightIs200.bend as Laws + +def Laws.cors_preflight_allowed(): + {==} diff --git a/proofs/CorsBadPreflightIs400.bend b/proofs/CorsBadPreflightIs400.bend new file mode 100644 index 0000000..faa172b --- /dev/null +++ b/proofs/CorsBadPreflightIs400.bend @@ -0,0 +1,7 @@ +# proofs of laws/CorsBadPreflightIs400.bend +import Base +import ../laws/CorsBadPreflightIs400.bend as Laws + +# cors_preflight_bad: preflight_req matches the request first +def Laws.cors_preflight_bad(_cfg, _why): + {==} diff --git a/proofs/CorsCredentialsKeepExplicitOrigin.bend b/proofs/CorsCredentialsKeepExplicitOrigin.bend new file mode 100644 index 0000000..7574024 --- /dev/null +++ b/proofs/CorsCredentialsKeepExplicitOrigin.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsCredentialsKeepExplicitOrigin.bend +import Base +import ../laws/CorsCredentialsKeepExplicitOrigin.bend as Laws + +def Laws.cors_credentials_explicit_origin(): + {==} diff --git a/proofs/CorsCredentialsReflectOrigin.bend b/proofs/CorsCredentialsReflectOrigin.bend new file mode 100644 index 0000000..03f64dc --- /dev/null +++ b/proofs/CorsCredentialsReflectOrigin.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsCredentialsReflectOrigin.bend +import Base +import ../laws/CorsCredentialsReflectOrigin.bend as Laws + +def Laws.cors_credentials_reflect_origin(): + {==} diff --git a/proofs/CorsGetIsNotPreflight.bend b/proofs/CorsGetIsNotPreflight.bend new file mode 100644 index 0000000..0a0f821 --- /dev/null +++ b/proofs/CorsGetIsNotPreflight.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsGetIsNotPreflight.bend +import Base +import ../laws/CorsGetIsNotPreflight.bend as Laws + +def Laws.cors_get_is_not_preflight(): + {==} diff --git a/proofs/CorsNoVaryForStar.bend b/proofs/CorsNoVaryForStar.bend new file mode 100644 index 0000000..3589de8 --- /dev/null +++ b/proofs/CorsNoVaryForStar.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsNoVaryForStar.bend +import Base +import ../laws/CorsNoVaryForStar.bend as Laws + +def Laws.cors_vary_star(): + {==} diff --git a/proofs/CorsNoVaryWhenRefused.bend b/proofs/CorsNoVaryWhenRefused.bend new file mode 100644 index 0000000..3187313 --- /dev/null +++ b/proofs/CorsNoVaryWhenRefused.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsNoVaryWhenRefused.bend +import Base +import ../laws/CorsNoVaryWhenRefused.bend as Laws + +def Laws.cors_vary_empty(): + {==} diff --git a/proofs/CorsOptionsWithMethodIsPreflight.bend b/proofs/CorsOptionsWithMethodIsPreflight.bend new file mode 100644 index 0000000..462a349 --- /dev/null +++ b/proofs/CorsOptionsWithMethodIsPreflight.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsOptionsWithMethodIsPreflight.bend +import Base +import ../laws/CorsOptionsWithMethodIsPreflight.bend as Laws + +def Laws.cors_on_preflight(): + {==} diff --git a/proofs/CorsPreflightAnswersStar.bend b/proofs/CorsPreflightAnswersStar.bend new file mode 100644 index 0000000..575f20e --- /dev/null +++ b/proofs/CorsPreflightAnswersStar.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsPreflightAnswersStar.bend +import Base +import ../laws/CorsPreflightAnswersStar.bend as Laws + +def Laws.cors_preflight_star(): + {==} diff --git a/proofs/CorsPreflightHeaderDenied.bend b/proofs/CorsPreflightHeaderDenied.bend new file mode 100644 index 0000000..89c8cd2 --- /dev/null +++ b/proofs/CorsPreflightHeaderDenied.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsPreflightHeaderDenied.bend +import Base +import ../laws/CorsPreflightHeaderDenied.bend as Laws + +def Laws.cors_preflight_header_denied(): + {==} diff --git a/proofs/CorsPreflightHeadersIgnoreCase.bend b/proofs/CorsPreflightHeadersIgnoreCase.bend new file mode 100644 index 0000000..bfa7aad --- /dev/null +++ b/proofs/CorsPreflightHeadersIgnoreCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsPreflightHeadersIgnoreCase.bend +import Base +import ../laws/CorsPreflightHeadersIgnoreCase.bend as Laws + +def Laws.cors_preflight_header_case(): + {==} diff --git a/proofs/CorsPreflightMethodDenied.bend b/proofs/CorsPreflightMethodDenied.bend new file mode 100644 index 0000000..0ec11fe --- /dev/null +++ b/proofs/CorsPreflightMethodDenied.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsPreflightMethodDenied.bend +import Base +import ../laws/CorsPreflightMethodDenied.bend as Laws + +def Laws.cors_preflight_method_denied(): + {==} diff --git a/proofs/CorsPreflightOriginDenied.bend b/proofs/CorsPreflightOriginDenied.bend new file mode 100644 index 0000000..0fbb6ec --- /dev/null +++ b/proofs/CorsPreflightOriginDenied.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsPreflightOriginDenied.bend +import Base +import ../laws/CorsPreflightOriginDenied.bend as Laws + +def Laws.cors_preflight_origin_denied(): + {==} diff --git a/proofs/CorsReadsOriginHeader.bend b/proofs/CorsReadsOriginHeader.bend new file mode 100644 index 0000000..4a5b2eb --- /dev/null +++ b/proofs/CorsReadsOriginHeader.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsReadsOriginHeader.bend +import Base +import ../laws/CorsReadsOriginHeader.bend as Laws + +def Laws.cors_origin_header(): + {==} diff --git a/proofs/CorsReadsRequestHeaders.bend b/proofs/CorsReadsRequestHeaders.bend new file mode 100644 index 0000000..eb4d5be --- /dev/null +++ b/proofs/CorsReadsRequestHeaders.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsReadsRequestHeaders.bend +import Base +import ../laws/CorsReadsRequestHeaders.bend as Laws + +def Laws.cors_request_headers_header(): + {==} diff --git a/proofs/CorsReadsRequestMethod.bend b/proofs/CorsReadsRequestMethod.bend new file mode 100644 index 0000000..c46d6a1 --- /dev/null +++ b/proofs/CorsReadsRequestMethod.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsReadsRequestMethod.bend +import Base +import ../laws/CorsReadsRequestMethod.bend as Laws + +def Laws.cors_request_method_header(): + {==} diff --git a/proofs/CorsRefusedLeavesReply.bend b/proofs/CorsRefusedLeavesReply.bend new file mode 100644 index 0000000..bd03cd7 --- /dev/null +++ b/proofs/CorsRefusedLeavesReply.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsRefusedLeavesReply.bend +import Base +import ../laws/CorsRefusedLeavesReply.bend as Laws + +def Laws.cors_simple_refused(): + {==} diff --git a/proofs/CorsRefusesMissingOrigin.bend b/proofs/CorsRefusesMissingOrigin.bend new file mode 100644 index 0000000..68ce7e1 --- /dev/null +++ b/proofs/CorsRefusesMissingOrigin.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsRefusesMissingOrigin.bend +import Base +import ../laws/CorsRefusesMissingOrigin.bend as Laws + +def Laws.cors_refused_origin(): + {==} diff --git a/proofs/CorsSimpleAnswersStar.bend b/proofs/CorsSimpleAnswersStar.bend new file mode 100644 index 0000000..7af27f3 --- /dev/null +++ b/proofs/CorsSimpleAnswersStar.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsSimpleAnswersStar.bend +import Base +import ../laws/CorsSimpleAnswersStar.bend as Laws + +def Laws.cors_simple_star(): + {==} diff --git a/proofs/CorsSimpleKeepsBody.bend b/proofs/CorsSimpleKeepsBody.bend new file mode 100644 index 0000000..9cab59c --- /dev/null +++ b/proofs/CorsSimpleKeepsBody.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsSimpleKeepsBody.bend +import Base +import ../laws/CorsSimpleKeepsBody.bend as Laws + +def Laws.cors_simple_reflects(): + {==} diff --git a/proofs/CorsStarStaysStar.bend b/proofs/CorsStarStaysStar.bend new file mode 100644 index 0000000..9e438f2 --- /dev/null +++ b/proofs/CorsStarStaysStar.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsStarStaysStar.bend +import Base +import ../laws/CorsStarStaysStar.bend as Laws + +def Laws.cors_star_without_credentials(): + {==} diff --git a/proofs/CorsVaryOnSpecificOrigin.bend b/proofs/CorsVaryOnSpecificOrigin.bend new file mode 100644 index 0000000..a795aa5 --- /dev/null +++ b/proofs/CorsVaryOnSpecificOrigin.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsVaryOnSpecificOrigin.bend +import Base +import ../laws/CorsVaryOnSpecificOrigin.bend as Laws + +def Laws.cors_vary_specific(): + {==} diff --git a/proofs/CutAtFirstEquals.bend b/proofs/CutAtFirstEquals.bend new file mode 100644 index 0000000..85cc6ab --- /dev/null +++ b/proofs/CutAtFirstEquals.bend @@ -0,0 +1,41 @@ +# proofs of laws/CutAtFirstEquals.bend +import Base +import ../src/qs.bend as Qs +import ../laws/CutAtFirstEquals.bend as Laws +import ../laws/defs/CharacterAbsent.bend as D1 +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.qs_cut_found(s, r, e): + match s: + case SNil{}: + {==} + case SCon{+h, +t}: + ne = Equal.sym(Bool, Char.is_eq(h, '='), False{}, + M1.eq.not.true(Char.is_eq(h, '='), + M1.eq.and.left(Bool.not(Char.is_eq(h, '=')), D1.avoids(t, '='), e))) + ih = Equal.sym(Maybe<&2, Qs.Kv()>, + Qs.cut(String.append(t, SCon{'=', r})), Some{(t, r)}, + Laws.qs_cut_found(t, r, + M1.eq.and.right(Bool.not(Char.is_eq(h, '=')), D1.avoids(t, '='), e))) + %ne : {Qs.cut.at(h, String.append(t, SCon{'=', r}), _, + _u => Qs.cut(String.append(t, SCon{'=', r}))) + == Some{(SCon{h, t}, r)} : Maybe<&2, Qs.Kv()>} + %ih : {Qs.cut.push(h, _) == Some{(SCon{h, t}, r)} + : Maybe<&2, Qs.Kv()>} + {==} + +def Laws.qs_cut_none(s, e): + match s: + case SNil{}: + {==} + case SCon{+h, +t}: + ne = Equal.sym(Bool, Char.is_eq(h, '='), False{}, + M1.eq.not.true(Char.is_eq(h, '='), + M1.eq.and.left(Bool.not(Char.is_eq(h, '=')), D1.avoids(t, '='), e))) + ih = Equal.sym(Maybe<&2, Qs.Kv()>, Qs.cut(t), None{}, + Laws.qs_cut_none(t, + M1.eq.and.right(Bool.not(Char.is_eq(h, '=')), D1.avoids(t, '='), e))) + %ne : {Qs.cut.at(h, t, _, _u => Qs.cut(t)) == None{} + : Maybe<&2, Qs.Kv()>} + %ih : {Qs.cut.push(h, _) == None{} : Maybe<&2, Qs.Kv()>} + {==} diff --git a/proofs/DeadlineIsTimeout.bend b/proofs/DeadlineIsTimeout.bend new file mode 100644 index 0000000..19fc719 --- /dev/null +++ b/proofs/DeadlineIsTimeout.bend @@ -0,0 +1,10 @@ +# proofs of laws/DeadlineIsTimeout.bend +import Base +import ../src/wirespec.bend as Spec +import ../laws/DeadlineIsTimeout.bend as Laws + +# --- the client's deadline and failures --- + +def Laws.ms_none(_m, e): + %e : {Spec.wire.ms.of(_) == 60000 : U32} + {==} diff --git a/proofs/DechunkEmptyIsEmpty.bend b/proofs/DechunkEmptyIsEmpty.bend new file mode 100644 index 0000000..c00da85 --- /dev/null +++ b/proofs/DechunkEmptyIsEmpty.bend @@ -0,0 +1,6 @@ +# proofs of laws/DechunkEmptyIsEmpty.bend +import Base +import ../laws/DechunkEmptyIsEmpty.bend as Laws + +def Laws.dechunk_empty(): + {==} diff --git a/proofs/DechunkIgnoresExtension.bend b/proofs/DechunkIgnoresExtension.bend new file mode 100644 index 0000000..75238cd --- /dev/null +++ b/proofs/DechunkIgnoresExtension.bend @@ -0,0 +1,6 @@ +# proofs of laws/DechunkIgnoresExtension.bend +import Base +import ../laws/DechunkIgnoresExtension.bend as Laws + +def Laws.dechunk_ignores_extension(): + {==} diff --git a/proofs/DechunkJoinsTwoChunks.bend b/proofs/DechunkJoinsTwoChunks.bend new file mode 100644 index 0000000..f315d4c --- /dev/null +++ b/proofs/DechunkJoinsTwoChunks.bend @@ -0,0 +1,6 @@ +# proofs of laws/DechunkJoinsTwoChunks.bend +import Base +import ../laws/DechunkJoinsTwoChunks.bend as Laws + +def Laws.dechunk_two_chunks(): + {==} diff --git a/proofs/DechunkLastChunkAloneIsEmpty.bend b/proofs/DechunkLastChunkAloneIsEmpty.bend new file mode 100644 index 0000000..ee64d6f --- /dev/null +++ b/proofs/DechunkLastChunkAloneIsEmpty.bend @@ -0,0 +1,6 @@ +# proofs of laws/DechunkLastChunkAloneIsEmpty.bend +import Base +import ../laws/DechunkLastChunkAloneIsEmpty.bend as Laws + +def Laws.dechunk_only_last(): + {==} diff --git a/proofs/DechunkRfcExample.bend b/proofs/DechunkRfcExample.bend new file mode 100644 index 0000000..ef7f2e0 --- /dev/null +++ b/proofs/DechunkRfcExample.bend @@ -0,0 +1,6 @@ +# proofs of laws/DechunkRfcExample.bend +import Base +import ../laws/DechunkRfcExample.bend as Laws + +def Laws.dechunk_wiki(): + {==} diff --git a/proofs/DechunkStopsAtZero.bend b/proofs/DechunkStopsAtZero.bend new file mode 100644 index 0000000..5023108 --- /dev/null +++ b/proofs/DechunkStopsAtZero.bend @@ -0,0 +1,6 @@ +# proofs of laws/DechunkStopsAtZero.bend +import Base +import ../laws/DechunkStopsAtZero.bend as Laws + +def Laws.dechunk_stops_at_zero(): + {==} diff --git a/proofs/DeleteIsIdempotent.bend b/proofs/DeleteIsIdempotent.bend new file mode 100644 index 0000000..0244dcf --- /dev/null +++ b/proofs/DeleteIsIdempotent.bend @@ -0,0 +1,6 @@ +# proofs of laws/DeleteIsIdempotent.bend +import Base +import ../laws/DeleteIsIdempotent.bend as Laws + +def Laws.method_idempotent_delete(): + {==} diff --git a/proofs/DeleteNotSafe.bend b/proofs/DeleteNotSafe.bend new file mode 100644 index 0000000..ccd7401 --- /dev/null +++ b/proofs/DeleteNotSafe.bend @@ -0,0 +1,6 @@ +# proofs of laws/DeleteNotSafe.bend +import Base +import ../laws/DeleteNotSafe.bend as Laws + +def Laws.method_safe_delete(): + {==} diff --git a/proofs/DeleteRequestLine.bend b/proofs/DeleteRequestLine.bend new file mode 100644 index 0000000..47fecc6 --- /dev/null +++ b/proofs/DeleteRequestLine.bend @@ -0,0 +1,12 @@ +# proofs of laws/DeleteRequestLine.bend +import Base +import ../src/http.bend as Http +import ../laws/DeleteRequestLine.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.delete_req_names_path(host, path, hs, body): + +payload = body + M1.eq.starts_append(path, + " HTTP/1.1\r\n" + ++ Http.headers.lines(Http.request.fields(host, hs, payload)) + ++ "\r\n" ++ payload) diff --git a/proofs/DotSegmentsRemoved.bend b/proofs/DotSegmentsRemoved.bend new file mode 100644 index 0000000..8d6c76d --- /dev/null +++ b/proofs/DotSegmentsRemoved.bend @@ -0,0 +1,58 @@ +# proofs of laws/DotSegmentsRemoved.bend +import Base +import ../src/redirect.bend as Redirect +import ../laws/DotSegmentsRemoved.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.clean_path(_ss): + {==} + +def Laws.dots_nil(_out): + {==} + +def Laws.dots_dot(s, t, out, e): + %e : {Redirect.redirect.dots.one(_, String.eq(s, ".."), + List.is_empty(&2, String, t), s, out, o => Redirect.redirect.dots(t, o)) + == Redirect.redirect.dots(t, + Redirect.redirect.dots.end(List.is_empty(&2, String, t), out)) + : List<&2, String>} + {==} + +def Laws.dots_up(s, t, out, e1, e2): + %e1 : {Redirect.redirect.dots.one(_, String.eq(s, ".."), + List.is_empty(&2, String, t), s, out, o => Redirect.redirect.dots(t, o)) + == Redirect.redirect.dots(t, + Redirect.redirect.dots.end(List.is_empty(&2, String, t), + Redirect.redirect.pop(out))) : List<&2, String>} + %e2 : {Redirect.redirect.dots.up(_, List.is_empty(&2, String, t), s, out, + o => Redirect.redirect.dots(t, o)) + == Redirect.redirect.dots(t, + Redirect.redirect.dots.end(List.is_empty(&2, String, t), + Redirect.redirect.pop(out))) : List<&2, String>} + {==} + +def Laws.dots_seg(s, t, out, e1, e2): + %e1 : {Redirect.redirect.dots.one(_, String.eq(s, ".."), + List.is_empty(&2, String, t), s, out, o => Redirect.redirect.dots(t, o)) + == Redirect.redirect.dots(t, s <> out) : List<&2, String>} + %e2 : {Redirect.redirect.dots.up(_, List.is_empty(&2, String, t), s, out, + o => Redirect.redirect.dots(t, o)) + == Redirect.redirect.dots(t, s <> out) : List<&2, String>} + {==} + +def Laws.dots_last(_out): + {==} + +def Laws.dots_inner(_out): + {==} + +def Laws.pop_nil(out, e): + match out: + case []: + {==} + case h <> t: + M1.eq.bit.absurd(&2, {Redirect.redirect.pop(h <> t) == [] : List<&2, String>}, + Equal.sym(Bool, True{}, False{}, e)) + +def Laws.pop_cons(_s, _t): + {==} diff --git a/proofs/EqualContentLengthsAreOne.bend b/proofs/EqualContentLengthsAreOne.bend new file mode 100644 index 0000000..458ca08 --- /dev/null +++ b/proofs/EqualContentLengthsAreOne.bend @@ -0,0 +1,6 @@ +# proofs of laws/EqualContentLengthsAreOne.bend +import Base +import ../laws/EqualContentLengthsAreOne.bend as Laws + +def Laws.parse_equal_content_lengths(): + {==} diff --git a/proofs/ErrnoNamesAreLinux.bend b/proofs/ErrnoNamesAreLinux.bend new file mode 100644 index 0000000..e950246 --- /dev/null +++ b/proofs/ErrnoNamesAreLinux.bend @@ -0,0 +1,23 @@ +# proofs of laws/ErrnoNamesAreLinux.bend +import Base +import ../src/wirespec.bend as Spec +import ../laws/ErrnoNamesAreLinux.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.errno_names(_n): + {==} + +def Laws.errno_or_none(s, e): + match s: + case SNil{}: + {==} + case SCon{h, t}: + M1.eq.bit.absurd(&2, {Spec.wire.errno.or(SCon{h, t}) == "errno" : String}, + Equal.sym(Bool, True{}, False{}, e)) + +def Laws.errno_or_named(s, e): + match s: + case SNil{}: + M1.eq.bit.absurd(&2, {Spec.wire.errno.or(SNil{}) == SNil{} : String}, e) + case SCon{h, t}: + {==} diff --git a/proofs/ExactContentLengthBodyKept.bend b/proofs/ExactContentLengthBodyKept.bend new file mode 100644 index 0000000..7bf5e36 --- /dev/null +++ b/proofs/ExactContentLengthBodyKept.bend @@ -0,0 +1,17 @@ +# proofs of laws/ExactContentLengthBodyKept.bend +import Base +import ../src/http.bend as Http +import ../laws/ExactContentLengthBodyKept.bend as Laws +import ./lemmas/NatOrder.bend as M1 + +def Laws.clen_exact(st, hs, body): + Equal.trans(Http.Reply, + Http.parse.clen(st, hs, Http.octs.len(body), body), + Http.parse.short(st, hs, Http.octs.len(body), body, False{}), + Http.Reply{st, hs, body}, + Equal.cong(Bool, Http.Reply, + b => Http.parse.short(st, hs, Http.octs.len(body), body, b), + Nat.is_lt(Http.octs.len(body), Http.octs.len(body)), False{}, + M1.lt_self(Http.octs.len(body))), + Equal.cong(String, Http.Reply, x => Http.Reply{st, hs, x}, + Http.octs.take(body, Http.octs.len(body)), body, M1.take_all(body))) diff --git a/proofs/ExchangeCarriesMethod.bend b/proofs/ExchangeCarriesMethod.bend new file mode 100644 index 0000000..2dcf460 --- /dev/null +++ b/proofs/ExchangeCarriesMethod.bend @@ -0,0 +1,37 @@ +# proofs of laws/ExchangeCarriesMethod.bend +import Base +import ../src/http.bend as Http +import ../laws/ExchangeCarriesMethod.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +# --- the request's header fields --- + +# a string prefix both sides share does not change starts_with +def eq.starts_cancel(+a: String, +x: String, +y: String) + -> {String.starts_with.go(String.append(a, y), String.append(a, x)) + == String.starts_with.go(y, x) : Bool}: + match a: + case SNil{}: + {==} + case SCon{h, t}: + %Equal.sym(Bool, Char.is_eq(h, h), True{}, M1.eq.char_eq_self(h)) + : {String.starts_with.go.if(String.append(t, y), String.append(t, x), _) + == String.starts_with.go(y, x) : Bool} + eq.starts_cancel(t, x, y) + +def Laws.exchange_method(_method, _scheme, _host, _port, _path, _hs, _body): + {==} + +def Laws.request_line(method, secure, host, port, path, hs, body): + +rest = Http.headers.lines( + Http.request.fields(Http.authority(secure, host, port), hs, body)) + ++ "\r\n" ++ body + Equal.trans(Bool, + String.starts_with.go(method ++ " " ++ path ++ " HTTP/1.1\r\n", + method ++ " " ++ path ++ " HTTP/1.1\r\n" ++ rest), + String.starts_with.go(" " ++ path ++ " HTTP/1.1\r\n", + " " ++ path ++ " HTTP/1.1\r\n" ++ rest), + True{}, + eq.starts_cancel(method, " " ++ path ++ " HTTP/1.1\r\n" ++ rest, + " " ++ path ++ " HTTP/1.1\r\n"), + eq.starts_cancel(path, " HTTP/1.1\r\n" ++ rest, " HTTP/1.1\r\n")) diff --git a/proofs/FailedEffectsLoseConnection.bend b/proofs/FailedEffectsLoseConnection.bend new file mode 100644 index 0000000..917b222 --- /dev/null +++ b/proofs/FailedEffectsLoseConnection.bend @@ -0,0 +1,28 @@ +# proofs of laws/FailedEffectsLoseConnection.bend +import Base +import ../src/step.bend as Step +import ../laws/FailedEffectsLoseConnection.bend as Laws + +def Laws.lost_failed(_a, _phase, _code, _why): + {==} + +def Laws.lost_kept(_a, _phase, _value): + {==} + +def Laws.lost_send(_code, _why, _left): + {==} + +def Laws.lost_sent(_done): + {==} + +def Laws.accept_idle(_a, _code, e, why): + %e : {Step.step.missed(_a, why, _) == Step.Idle{} : Step.Accept<_a>} + {==} + +def Laws.accept_missed(_a, _code, e, why): + %e : {Step.step.missed(_a, why, _) == Step.Missed{"ezhttp: accept: " ++ why} + : Step.Accept<_a>} + {==} + +def Laws.accept_took(_a, _value): + {==} diff --git a/proofs/FailuresReportedByPhase.bend b/proofs/FailuresReportedByPhase.bend new file mode 100644 index 0000000..c048dc1 --- /dev/null +++ b/proofs/FailuresReportedByPhase.bend @@ -0,0 +1,96 @@ +# proofs of laws/FailuresReportedByPhase.bend +import Base +import ../src/wirespec.bend as Spec +import ../laws/FailuresReportedByPhase.bend as Laws + +# a resolve's failure is fail.resolve, ran out of time or not +def fail.resolve.any(host: String, +port: U32, +ms: U32, +n: U32, why: String, + late: Bool) + -> {Spec.wire.fail.of(Spec.Resolve{}, host, port, ms, n, why, late) + == Spec.wire.fail.resolve(host, n, U32.is_eq(n, 2)) : String}: + match late: + case True{}: + {==} + case False{}: + {==} + +# a resolve that failed with another number names the cause +def fail.resolve.some(host: String, +n: U32, + e: {False{} == U32.is_eq(n, 2) : Bool}) + -> {Spec.wire.fail.resolve(host, n, U32.is_eq(n, 2)) + == "6\ncould not resolve " ++ host ++ ": " ++ Spec.wire.cause(n) + : String}: + %e : {Spec.wire.fail.resolve(host, n, _) + == "6\ncould not resolve " ++ host ++ ": " ++ Spec.wire.cause(n) + : String} + {==} + +# a name with no address names the host alone +def fail.resolve.none(host: String, +n: U32, + e: {True{} == U32.is_eq(n, 2) : Bool}) + -> {Spec.wire.fail.resolve(host, n, U32.is_eq(n, 2)) + == "6\ncould not resolve " ++ host : String}: + %e : {Spec.wire.fail.resolve(host, n, _) == "6\ncould not resolve " ++ host + : String} + {==} + +def Laws.fail_resolve(host, port, ms, n, e, why): + Equal.trans(String, + Spec.wire.fail(Spec.Resolve{}, host, port, ms, n, why), + Spec.wire.fail.resolve(host, n, U32.is_eq(n, 2)), + "6\ncould not resolve " ++ host ++ ": " ++ Spec.wire.cause(n), + fail.resolve.any(host, port, ms, n, why, U32.is_eq(n, 110)), + fail.resolve.some(host, n, e)) + +def Laws.fail_resolve_none(host, port, ms, n, e, why): + Equal.trans(String, + Spec.wire.fail(Spec.Resolve{}, host, port, ms, n, why), + Spec.wire.fail.resolve(host, n, U32.is_eq(n, 2)), + "6\ncould not resolve " ++ host, + fail.resolve.any(host, port, ms, n, why, U32.is_eq(n, 110)), + fail.resolve.none(host, n, e)) + +# a call that ran out of time is reported by fail.late +def fail.at.late(p: Spec.Phase, host: String, +port: U32, +ms: U32, +n: U32, + why: String, e: {True{} == U32.is_eq(n, 110) : Bool}) + -> {Spec.wire.fail(p, host, port, ms, n, why) + == Spec.wire.fail.late(p, host, port, ms, n) : String}: + %e : {Spec.wire.fail.of(p, host, port, ms, n, why, _) + == Spec.wire.fail.late(p, host, port, ms, n) : String} + {==} + +# a call that failed otherwise is reported by fail.other +def fail.at.other(p: Spec.Phase, host: String, +port: U32, +ms: U32, +n: U32, + why: String, e: {False{} == U32.is_eq(n, 110) : Bool}) + -> {Spec.wire.fail(p, host, port, ms, n, why) + == Spec.wire.fail.other(p, host, port, n, why) : String}: + %e : {Spec.wire.fail.of(p, host, port, ms, n, why, _) + == Spec.wire.fail.other(p, host, port, n, why) : String} + {==} + +def Laws.fail_connect_late(host, port, ms, n, e, why): + fail.at.late(Spec.Connect{}, host, port, ms, n, why, e) + +def Laws.fail_handshake_late(host, port, ms, n, e, why): + fail.at.late(Spec.Handshake{}, host, port, ms, n, why, e) + +def Laws.fail_send_late(host, port, ms, n, e, why): + fail.at.late(Spec.Send{}, host, port, ms, n, why, e) + +def Laws.fail_receive_late(host, port, ms, n, e, why): + fail.at.late(Spec.Receive{}, host, port, ms, n, why, e) + +def Laws.fail_connect(host, port, ms, n, e, why): + fail.at.other(Spec.Connect{}, host, port, ms, n, why, e) + +def Laws.fail_handshake(host, port, ms, n, e, why): + fail.at.other(Spec.Handshake{}, host, port, ms, n, why, e) + +def Laws.fail_send(host, port, ms, n, e, why): + fail.at.other(Spec.Send{}, host, port, ms, n, why, e) + +def Laws.fail_receive(host, port, ms, n, e, why): + fail.at.other(Spec.Receive{}, host, port, ms, n, why, e) + +def Laws.cause_is(_n): + {==} diff --git a/proofs/FieldLookupFindsFirst.bend b/proofs/FieldLookupFindsFirst.bend new file mode 100644 index 0000000..91aaacc --- /dev/null +++ b/proofs/FieldLookupFindsFirst.bend @@ -0,0 +1,6 @@ +# proofs of laws/FieldLookupFindsFirst.bend +import Base +import ../laws/FieldLookupFindsFirst.bend as Laws + +def Laws.headers_find_first(): + {==} diff --git a/proofs/FieldLookupIgnoresCase.bend b/proofs/FieldLookupIgnoresCase.bend new file mode 100644 index 0000000..0452a3c --- /dev/null +++ b/proofs/FieldLookupIgnoresCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/FieldLookupIgnoresCase.bend +import Base +import ../laws/FieldLookupIgnoresCase.bend as Laws + +def Laws.headers_find_ignores_case(): + {==} diff --git a/proofs/FieldNamesIgnoreCase.bend b/proofs/FieldNamesIgnoreCase.bend new file mode 100644 index 0000000..14dff6d --- /dev/null +++ b/proofs/FieldNamesIgnoreCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/FieldNamesIgnoreCase.bend +import Base +import ../laws/FieldNamesIgnoreCase.bend as Laws + +def Laws.head_hit_ignores_case(_h, _name): + {==} diff --git a/proofs/FieldValueTrimsWhitespace.bend b/proofs/FieldValueTrimsWhitespace.bend new file mode 100644 index 0000000..9d3915a --- /dev/null +++ b/proofs/FieldValueTrimsWhitespace.bend @@ -0,0 +1,6 @@ +# proofs of laws/FieldValueTrimsWhitespace.bend +import Base +import ../laws/FieldValueTrimsWhitespace.bend as Laws + +def Laws.head_line_trims(): + {==} diff --git a/proofs/FirstIsFirstWord.bend b/proofs/FirstIsFirstWord.bend new file mode 100644 index 0000000..b19e435 --- /dev/null +++ b/proofs/FirstIsFirstWord.bend @@ -0,0 +1,11 @@ +# proofs of laws/FirstIsFirstWord.bend +import Base +import ../src/http.bend as Http +import ../laws/FirstIsFirstWord.bend as Laws +import ../laws/SplitAtCharacter.bend as L1 +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.first_is_first_word(w, r, e): + Equal.cong(List<&2, String>, String, ps => Http.first(ps), + String.split(String.append(w, SCon{' ', r}), ' '), + w <> String.split(r, ' '), L1.split_cut(w, ' ', r, e)) diff --git a/proofs/FormBodyLayout.bend b/proofs/FormBodyLayout.bend new file mode 100644 index 0000000..dc5f4ef --- /dev/null +++ b/proofs/FormBodyLayout.bend @@ -0,0 +1,24 @@ +# proofs of laws/FormBodyLayout.bend +import Base +import ../laws/FormBodyLayout.bend as Laws + +def Laws.content_field(_name, _value, _rest): + {==} + +def Laws.content_file(_name, _filename, _ctype, _clash, _rest): + {==} + +def Laws.parts_nil(_b): + {==} + +def Laws.parts_cons(_b, _p, _t): + {==} + +def Laws.part(_b, _p, _rest): + {==} + +def Laws.onto_nil(_rest): + {==} + +def Laws.onto_cons(_c, _t, _rest): + {==} diff --git a/proofs/FormBoundaryRules.bend b/proofs/FormBoundaryRules.bend new file mode 100644 index 0000000..c467c1c --- /dev/null +++ b/proofs/FormBoundaryRules.bend @@ -0,0 +1,78 @@ +# proofs of laws/FormBoundaryRules.bend +import Base +import ../src/form.bend as Form +import ../laws/FormBoundaryRules.bend as Laws + +def Laws.all_step(ok, _rest): + match ok: + case False{}: + {==} + case True{}: + {==} + +def Laws.any_step(hit, _rest): + match hit: + case True{}: + {==} + case False{}: + {==} + +def Laws.boundary_ok(_b): + {==} + +def Laws.bchar(_cp): + {==} + +def Laws.bchars_nil(_s, e): + %e : {Form.form.bchars(_) == True{} : Bool} + {==} + +def Laws.bchars_cons(c, t): + Laws.all_step(Form.form.bchar(c), _u => Form.form.bchars(t)) + +def Laws.delim(_b): + {==} + +def Laws.clash_nil(_b): + {==} + +def Laws.clash_cons(b, p, t): + Laws.any_step(Form.form.hit(b, p), _u => Form.form.clash(b, t)) + +def Laws.has_nil(_pat): + {==} + +def Laws.has_cons(x, t, pat): + Laws.any_step(Form.octets.starts(x <> t, pat), _u => Form.octets.has(t, pat)) + +def Laws.starts_done(xs): + match xs: + case []: + {==} + case _x <> _t: + {==} + +def Laws.starts_short(_p, _q): + {==} + +def Laws.starts_cons(x, t, p, q): + Laws.all_step(U32.is_eq(x, p), _u => Form.octets.starts(t, q)) + +def Laws.badtype_nil(_ps, e): + %e : {Form.form.badtype(_) == False{} : Bool} + {==} + +def Laws.badtype_cons(p, t): + Laws.any_step(Form.form.badtype.one(p), _u => Form.form.badtype(t)) + +def Laws.badtype_field(_name, _value): + {==} + +def Laws.badtype_file(_name, _filename, _ctype, _clash): + {==} + +def Laws.hit_field(_b, _name, _value): + {==} + +def Laws.hit_file(_b, _name, _filename, _ctype, _clash): + {==} diff --git a/proofs/FormNamesEscaped.bend b/proofs/FormNamesEscaped.bend new file mode 100644 index 0000000..61c131f --- /dev/null +++ b/proofs/FormNamesEscaped.bend @@ -0,0 +1,26 @@ +# proofs of laws/FormNamesEscaped.bend +import Base +import ../src/form.bend as Form +import ../laws/FormNamesEscaped.bend as Laws + +def Laws.fq_nil(_s, e): + %e : {Form.form.quote(_) == "" : String} + {==} + +def Laws.fq_cons(_c, _t): + {==} + +def Laws.fq_dq(_rest): + {==} + +def Laws.fq_cr(_rest): + {==} + +def Laws.fq_lf(_rest): + {==} + +def Laws.fq_other(c, e1, e2, e3, rest): + %e1 : {Form.form.one.dq(c, rest, _) == SCon{Chr{c}, rest} : String} + %e2 : {Form.form.one.cr(c, rest, _) == SCon{Chr{c}, rest} : String} + %e3 : {Form.form.one.lf(c, rest, _) == SCon{Chr{c}, rest} : String} + {==} diff --git a/proofs/FormPartHeaders.bend b/proofs/FormPartHeaders.bend new file mode 100644 index 0000000..6775f3e --- /dev/null +++ b/proofs/FormPartHeaders.bend @@ -0,0 +1,18 @@ +# proofs of laws/FormPartHeaders.bend +import Base +import ../laws/FormPartHeaders.bend as Laws + +def Laws.field_is(_d, _name, _value): + {==} + +def Laws.file_is(_name, _filename, _ctype, _n, _buf, _hit): + {==} + +def Laws.head_field(_name, _value): + {==} + +def Laws.head_file(_name, _filename, _ctype, _clash): + {==} + +def Laws.disposition(_name): + {==} diff --git a/proofs/FormRefusesBadRequests.bend b/proofs/FormRefusesBadRequests.bend new file mode 100644 index 0000000..357e506 --- /dev/null +++ b/proofs/FormRefusesBadRequests.bend @@ -0,0 +1,41 @@ +# proofs of laws/FormRefusesBadRequests.bend +import Base +import ../src/form.bend as Form +import ../laws/FormRefusesBadRequests.bend as Laws + +def Laws.form_bad_boundary(b, ps, e): + %e : {Form.form.check(b, ps, _) + == Form.NoPlan{"the boundary must be 1 to 70 of A-Z a-z 0-9 - _ ."} + : Form.Plan} + {==} + +def Laws.form_clash(b, ps, e1, e2): + %e1 : {Form.form.check(b, ps, _) + == Form.NoPlan{"the boundary occurs in a part's content"} : Form.Plan} + %e2 : {Form.form.check.clash(b, ps, _) + == Form.NoPlan{"the boundary occurs in a part's content"} : Form.Plan} + {==} + +def Laws.form_bad_type(b, ps, e1, e2, e3): + %e1 : {Form.form.check(b, ps, _) + == Form.NoPlan{"a file part's content type holds a CR or LF"} : Form.Plan} + %e2 : {Form.form.check.clash(b, ps, _) + == Form.NoPlan{"a file part's content type holds a CR or LF"} : Form.Plan} + %e3 : {Form.form.check.type(b, ps, _) + == Form.NoPlan{"a file part's content type holds a CR or LF"} : Form.Plan} + {==} + +def Laws.form_ok(b, ps, e1, e2, e3): + %e1 : {Form.form.check(b, ps, _) + == Form.Plan{Form.form.type(b), Form.form.parts(b, ps)} + : Form.Plan} + %e2 : {Form.form.check.clash(b, ps, _) + == Form.Plan{Form.form.type(b), Form.form.parts(b, ps)} + : Form.Plan} + %e3 : {Form.form.check.type(b, ps, _) + == Form.Plan{Form.form.type(b), Form.form.parts(b, ps)} + : Form.Plan} + {==} + +def Laws.form_type(_b): + {==} diff --git a/proofs/FreshUnderMaxAge.bend b/proofs/FreshUnderMaxAge.bend new file mode 100644 index 0000000..19ca5c6 --- /dev/null +++ b/proofs/FreshUnderMaxAge.bend @@ -0,0 +1,6 @@ +# proofs of laws/FreshUnderMaxAge.bend +import Base +import ../laws/FreshUnderMaxAge.bend as Laws + +def Laws.fresh_max_age_open(): + {==} diff --git a/proofs/GetAsteriskTargetRejected.bend b/proofs/GetAsteriskTargetRejected.bend new file mode 100644 index 0000000..2e20387 --- /dev/null +++ b/proofs/GetAsteriskTargetRejected.bend @@ -0,0 +1,6 @@ +# proofs of laws/GetAsteriskTargetRejected.bend +import Base +import ../laws/GetAsteriskTargetRejected.bend as Laws + +def Laws.ask_get_asterisk_rejected(): + {==} diff --git a/proofs/GetIsIdempotent.bend b/proofs/GetIsIdempotent.bend new file mode 100644 index 0000000..4d34365 --- /dev/null +++ b/proofs/GetIsIdempotent.bend @@ -0,0 +1,6 @@ +# proofs of laws/GetIsIdempotent.bend +import Base +import ../laws/GetIsIdempotent.bend as Laws + +def Laws.method_idempotent_get(): + {==} diff --git a/proofs/GetIsSafe.bend b/proofs/GetIsSafe.bend new file mode 100644 index 0000000..ac03ed3 --- /dev/null +++ b/proofs/GetIsSafe.bend @@ -0,0 +1,6 @@ +# proofs of laws/GetIsSafe.bend +import Base +import ../laws/GetIsSafe.bend as Laws + +def Laws.method_safe_get(): + {==} diff --git a/proofs/GetOmitsContentLength.bend b/proofs/GetOmitsContentLength.bend new file mode 100644 index 0000000..1f3c0bc --- /dev/null +++ b/proofs/GetOmitsContentLength.bend @@ -0,0 +1,6 @@ +# proofs of laws/GetOmitsContentLength.bend +import Base +import ../laws/GetOmitsContentLength.bend as Laws + +def Laws.get_omits_content_length(): + {==} diff --git a/proofs/GetRequestLine.bend b/proofs/GetRequestLine.bend new file mode 100644 index 0000000..bb5959d --- /dev/null +++ b/proofs/GetRequestLine.bend @@ -0,0 +1,10 @@ +# proofs of laws/GetRequestLine.bend +import Base +import ../src/http.bend as Http +import ../laws/GetRequestLine.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.get_req_names_path(host, path, hs): + M1.eq.starts_append(path, + " HTTP/1.1\r\n" ++ Http.headers.lines(Http.request.fields(host, hs, "")) + ++ "\r\n") diff --git a/proofs/HandlerGetsRequestText.bend b/proofs/HandlerGetsRequestText.bend new file mode 100644 index 0000000..2f3d420 --- /dev/null +++ b/proofs/HandlerGetsRequestText.bend @@ -0,0 +1,9 @@ +# proofs of laws/HandlerGetsRequestText.bend +import Base +import ../laws/HandlerGetsRequestText.bend as Laws + +def Laws.step_call(_method, _target, _hs, _body): + {==} + +def Laws.step_text(_method, _target, _hs, _body): + {==} diff --git a/proofs/HeadAndNoContentHaveNoBody.bend b/proofs/HeadAndNoContentHaveNoBody.bend new file mode 100644 index 0000000..66a2256 --- /dev/null +++ b/proofs/HeadAndNoContentHaveNoBody.bend @@ -0,0 +1,29 @@ +# proofs of laws/HeadAndNoContentHaveNoBody.bend +import Base +import ../src/http.bend as Http +import ../laws/HeadAndNoContentHaveNoBody.bend as Laws + +def Laws.shape_head(_status, _hs, _body): + {==} + +def Laws.shape_other(_method, e, reply): + match reply: + case Http.Torn{why}: + %e : {Http.reply.for.pick(Http.Torn{why}, _) == Http.Torn{why} + : Http.Reply} + {==} + case Http.Reply{status, hs, body}: + %e : {Http.reply.for.pick(Http.Reply{status, hs, body}, _) + == Http.Reply{status, hs, body} : Http.Reply} + {==} + +def Laws.respond_204(_hs, _body): + {==} + +def Laws.respond_304(_hs, _body): + {==} + +def Laws.respond_other(status, e, hs, body): + %e : {Http.respond.of.body(status, hs, body, _) + == Http.respond(status, hs, body) : String} + {==} diff --git a/proofs/HeadIsIdempotent.bend b/proofs/HeadIsIdempotent.bend new file mode 100644 index 0000000..905912e --- /dev/null +++ b/proofs/HeadIsIdempotent.bend @@ -0,0 +1,6 @@ +# proofs of laws/HeadIsIdempotent.bend +import Base +import ../laws/HeadIsIdempotent.bend as Laws + +def Laws.method_idempotent_head(): + {==} diff --git a/proofs/HeadIsSafe.bend b/proofs/HeadIsSafe.bend new file mode 100644 index 0000000..207743d --- /dev/null +++ b/proofs/HeadIsSafe.bend @@ -0,0 +1,6 @@ +# proofs of laws/HeadIsSafe.bend +import Base +import ../laws/HeadIsSafe.bend as Laws + +def Laws.method_safe_head(): + {==} diff --git a/proofs/HeadPlannedBeforeBody.bend b/proofs/HeadPlannedBeforeBody.bend new file mode 100644 index 0000000..2cbda1a --- /dev/null +++ b/proofs/HeadPlannedBeforeBody.bend @@ -0,0 +1,123 @@ +# proofs of laws/HeadPlannedBeforeBody.bend +import Base +import ../src/http.bend as Http +import ../laws/HeadPlannedBeforeBody.bend as Laws + +# --- a response planned from its head --- + +def Laws.plan_none(_ls): + {==} + +def Laws.plan_disagree(n, ls, e): + %e : {Http.plan.agree(n, Http.head.list(ls), Http.head.go(ls, "transfer-encoding"), + Http.head.values(ls, "content-length"), _) + == Http.Unplanned{"Content-Length values disagree"} : Http.Plan} + {==} + +def Laws.plan_conflict(n, ls, e1, e2, e3): + +V = Http.head.values(ls, "content-length") + %e1 : {Http.plan.agree(n, Http.head.list(ls), Http.head.go(ls, "transfer-encoding"), + V, _) == Http.Unplanned{"Content-Length and Transfer-Encoding conflict"} : Http.Plan} + %e2 : {Http.plan.te(n, Http.head.list(ls), V, + _) == Http.Unplanned{"Content-Length and Transfer-Encoding conflict"} : Http.Plan} + %e3 : {Http.plan.clash(n, Http.head.list(ls), + _) == Http.Unplanned{"Content-Length and Transfer-Encoding conflict"} : Http.Plan} + {==} + +def Laws.plan_chunked(n, ls, e1, e2, e3): + +V = Http.head.values(ls, "content-length") + %e1 : {Http.plan.agree(n, Http.head.list(ls), Http.head.go(ls, "transfer-encoding"), + V, _) == Http.Planned{n, Http.head.list(ls), Http.Chunked{}} : Http.Plan} + %e2 : {Http.plan.te(n, Http.head.list(ls), V, _) == Http.Planned{n, Http.head.list(ls), Http.Chunked{}} : Http.Plan} + %e3 : {Http.plan.clash(n, Http.head.list(ls), _) == Http.Planned{n, Http.head.list(ls), Http.Chunked{}} : Http.Plan} + {==} + +def Laws.plan_sized(n, ls, e1, e2, k, e3): + +V = Http.head.values(ls, "content-length") + %e1 : {Http.plan.agree(n, Http.head.list(ls), Http.head.go(ls, "transfer-encoding"), + V, _) == Http.Planned{n, Http.head.list(ls), Http.Sized{k}} : Http.Plan} + %e2 : {Http.plan.te(n, Http.head.list(ls), V, _) == Http.Planned{n, Http.head.list(ls), Http.Sized{k}} : Http.Plan} + %e3 : {Http.plan.len(n, Http.head.list(ls), _) == Http.Planned{n, Http.head.list(ls), Http.Sized{k}} : Http.Plan} + {==} + +def Laws.plan_closed(n, ls, e1, e2, e3): + +V = Http.head.values(ls, "content-length") + %e1 : {Http.plan.agree(n, Http.head.list(ls), Http.head.go(ls, "transfer-encoding"), + V, _) == Http.Planned{n, Http.head.list(ls), Http.Closed{}} : Http.Plan} + %e2 : {Http.plan.te(n, Http.head.list(ls), V, _) == Http.Planned{n, Http.head.list(ls), Http.Closed{}} : Http.Plan} + %e3 : {Http.plan.len(n, Http.head.list(ls), _) == Http.Planned{n, Http.head.list(ls), Http.Closed{}} : Http.Plan} + {==} + +def Laws.plan_status(_m, _ls, _body): + {==} + +law final_of: + for b: Bool + for +m: Maybe<&2, U32> + for +ls: List<&2, String> + for +after: String + {Http.parse.to.final(m, ls, after, b) + == Http.plan.text(Http.head.final.of(b, m, ls), after) : Http.Reply} + +def final_of(b, m, _ls, _after): + match b: + case True{}: + match m: + case None{}: + {==} + case Some{n}: + {==} + case False{}: + {==} + +def Laws.plan_final(f, head, before, after, e): + +L = String.lines(before) + +M = Http.status.of(L) + %e : {Http.parse.to.step(head, L, M, after, _, cut => Http.parse.to.go(f, head, cut)) + == Http.plan.text(Http.head.final(head, L), after) : Http.Reply} + final_of(Bool.or(head, Http.status.empty(M)), M, L, after) + +law size_ready: + for +after: String + for +n: Nat + for r: Bool + {Http.chunks.next.of(False{}, r, after, n) == Http.chunk.next.data(after, n, r) + : Http.Next} + +def size_ready(_after, _n, r): + match r: + case True{}: + {==} + case False{}: + {==} + +law size_zero: + for +n: Nat + for +after: String + for z: Bool + {Http.chunks.next.of(z, Http.chunks.ready(after, n), after, n) + == Http.chunk.next(Http.chunk.size.n(n, z), after) : Http.Next} + +def size_zero(n, after, z): + match z: + case True{}: + {==} + case False{}: + size_ready(after, n, Http.chunks.ready(after, n)) + +law size_hex: + for +before: String + for +after: String + for ok: Bool + {Http.chunks.next.hex(before, after, ok) + == Http.chunk.next(Http.chunk.size.of(before, ok), after) : Http.Next} + +def size_hex(before, after, ok): + match ok: + case False{}: + {==} + case True{}: + size_zero(Http.hexlen(before), after, Nat.is_eq(Http.hexlen(before), 0n)) + +def Laws.size_next(before, after): + size_hex(before, after, Http.hex.well(Http.chunks.token(before))) diff --git a/proofs/HeadReplyDropsBody.bend b/proofs/HeadReplyDropsBody.bend new file mode 100644 index 0000000..4444c3c --- /dev/null +++ b/proofs/HeadReplyDropsBody.bend @@ -0,0 +1,6 @@ +# proofs of laws/HeadReplyDropsBody.bend +import Base +import ../laws/HeadReplyDropsBody.bend as Laws + +def Laws.head_reply_drops_body(): + {==} diff --git a/proofs/HeadRequestLine.bend b/proofs/HeadRequestLine.bend new file mode 100644 index 0000000..100c8c9 --- /dev/null +++ b/proofs/HeadRequestLine.bend @@ -0,0 +1,10 @@ +# proofs of laws/HeadRequestLine.bend +import Base +import ../src/http.bend as Http +import ../laws/HeadRequestLine.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.head_req_names_path(host, path, hs): + M1.eq.starts_append(path, + " HTTP/1.1\r\n" ++ Http.headers.lines(Http.request.fields(host, hs, "")) + ++ "\r\n") diff --git a/proofs/HtmlEscapeCharByChar.bend b/proofs/HtmlEscapeCharByChar.bend new file mode 100644 index 0000000..9a7af09 --- /dev/null +++ b/proofs/HtmlEscapeCharByChar.bend @@ -0,0 +1,14 @@ +# proofs of laws/HtmlEscapeCharByChar.bend +import Base +import ../laws/HtmlEscapeCharByChar.bend as Laws + +# --- HTML escaping --- + +def Laws.html_escape_onto(_s): + {==} + +def Laws.html_nil(_rest): + {==} + +def Laws.html_cons(_c, _t, _rest): + {==} diff --git a/proofs/HtmlEscapeIsSafe.bend b/proofs/HtmlEscapeIsSafe.bend new file mode 100644 index 0000000..1f9f893 --- /dev/null +++ b/proofs/HtmlEscapeIsSafe.bend @@ -0,0 +1,135 @@ +# proofs of laws/HtmlEscapeIsSafe.bend +import Base +import ../src/html.bend as Html +import ../laws/HtmlEscapeIsSafe.bend as Laws + +# LAW: a character none of the five stays in the frame +law frame.sq: + for +c: Char + for rest: String + for k: Bool + for ek: {k == Char.is_eq(c, '\'') : Bool} + for ea: {False{} == Char.is_eq(c, '&') : Bool} + for el: {False{} == Char.is_eq(c, '<') : Bool} + for eg: {False{} == Char.is_eq(c, '>') : Bool} + for ed: {False{} == Char.is_eq(c, '"') : Bool} + {Laws.html.framed(Html.one.sq(c, rest, k), Laws.Out{}) + == Laws.html.framed(rest, Laws.Out{}) : Bool} + +def frame.sq(c, rest, k, ek, ea, el, eg, ed): + match k: + case True{}: + {==} + case False{}: + %ea : {Bool.and(Laws.ent.out.ok(Char.is_eq(c, '<'), Char.is_eq(c, '>'), + Char.is_eq(c, '"'), Char.is_eq(c, '\'')), + Laws.html.framed(rest, Laws.ent.out.next(_))) + == Laws.html.framed(rest, Laws.Out{}) : Bool} + %el : {Bool.and(Laws.ent.out.ok(_, Char.is_eq(c, '>'), + Char.is_eq(c, '"'), Char.is_eq(c, '\'')), + Laws.html.framed(rest, Laws.Out{})) + == Laws.html.framed(rest, Laws.Out{}) : Bool} + %eg : {Bool.and(Laws.ent.out.ok(False{}, _, + Char.is_eq(c, '"'), Char.is_eq(c, '\'')), + Laws.html.framed(rest, Laws.Out{})) + == Laws.html.framed(rest, Laws.Out{}) : Bool} + %ed : {Bool.and(Laws.ent.out.ok(False{}, False{}, _, + Char.is_eq(c, '\'')), + Laws.html.framed(rest, Laws.Out{})) + == Laws.html.framed(rest, Laws.Out{}) : Bool} + %ek : {Bool.and(Laws.ent.out.ok(False{}, False{}, False{}, _), + Laws.html.framed(rest, Laws.Out{})) + == Laws.html.framed(rest, Laws.Out{}) : Bool} + {==} + +# LAW: a character that is not `&`, `<` or `>` stays in the frame +law frame.dq: + for +c: Char + for rest: String + for k: Bool + for ek: {k == Char.is_eq(c, '"') : Bool} + for ea: {False{} == Char.is_eq(c, '&') : Bool} + for el: {False{} == Char.is_eq(c, '<') : Bool} + for eg: {False{} == Char.is_eq(c, '>') : Bool} + {Laws.html.framed(Html.one.dq(c, rest, k), Laws.Out{}) + == Laws.html.framed(rest, Laws.Out{}) : Bool} + +def frame.dq(c, rest, k, ek, ea, el, eg): + match k: + case True{}: + {==} + case False{}: + frame.sq(c, rest, Char.is_eq(c, '\''), {==}, ea, el, eg, ek) + +# LAW: a character that is not `&` or `<` stays in the frame +law frame.gt: + for +c: Char + for rest: String + for k: Bool + for ek: {k == Char.is_eq(c, '>') : Bool} + for ea: {False{} == Char.is_eq(c, '&') : Bool} + for el: {False{} == Char.is_eq(c, '<') : Bool} + {Laws.html.framed(Html.one.gt(c, rest, k), Laws.Out{}) + == Laws.html.framed(rest, Laws.Out{}) : Bool} + +def frame.gt(c, rest, k, ek, ea, el): + match k: + case True{}: + {==} + case False{}: + frame.dq(c, rest, Char.is_eq(c, '"'), {==}, ea, el, ek) + +# LAW: a character that is not `&` stays in the frame +law frame.lt: + for +c: Char + for rest: String + for k: Bool + for ek: {k == Char.is_eq(c, '<') : Bool} + for ea: {False{} == Char.is_eq(c, '&') : Bool} + {Laws.html.framed(Html.one.lt(c, rest, k), Laws.Out{}) + == Laws.html.framed(rest, Laws.Out{}) : Bool} + +def frame.lt(c, rest, k, ek, ea): + match k: + case True{}: + {==} + case False{}: + frame.gt(c, rest, Char.is_eq(c, '>'), {==}, ea, ek) + +# LAW: one escaped character stays in the frame +law frame.amp: + for +c: Char + for rest: String + for k: Bool + for ek: {k == Char.is_eq(c, '&') : Bool} + {Laws.html.framed(Html.one.amp(c, rest, k), Laws.Out{}) + == Laws.html.framed(rest, Laws.Out{}) : Bool} + +def frame.amp(c, rest, k, ek): + match k: + case True{}: + {==} + case False{}: + frame.lt(c, rest, Char.is_eq(c, '<'), {==}, ek) + +# LAW: an escaped string stays in the frame, whatever follows it +law frame.onto: + for s: String + for +rest: String + {Laws.html.framed(Html.escape.onto(s, rest), Laws.Out{}) + == Laws.html.framed(rest, Laws.Out{}) : Bool} + +def frame.onto(s, rest): + match s: + case SNil{}: + {==} + case SCon{+c, +t}: + Equal.trans(Bool, + Laws.html.framed(Html.one(c, Html.escape.onto(t, rest)), Laws.Out{}), + Laws.html.framed(Html.escape.onto(t, rest), Laws.Out{}), + Laws.html.framed(rest, Laws.Out{}), + frame.amp(c, Html.escape.onto(t, rest), Char.is_eq(c, '&'), {==}), + frame.onto(t, rest)) + +def Laws.html_frame(s): + frame.onto(s, "") diff --git a/proofs/HtmlEscapesFiveChars.bend b/proofs/HtmlEscapesFiveChars.bend new file mode 100644 index 0000000..cdd2cc3 --- /dev/null +++ b/proofs/HtmlEscapesFiveChars.bend @@ -0,0 +1,18 @@ +# proofs of laws/HtmlEscapesFiveChars.bend +import Base +import ../laws/HtmlEscapesFiveChars.bend as Laws + +def Laws.html_amp(_rest): + {==} + +def Laws.html_lt(_rest): + {==} + +def Laws.html_gt(_rest): + {==} + +def Laws.html_dq(_rest): + {==} + +def Laws.html_sq(_rest): + {==} diff --git a/proofs/HtmlKeepsOtherChars.bend b/proofs/HtmlKeepsOtherChars.bend new file mode 100644 index 0000000..698973f --- /dev/null +++ b/proofs/HtmlKeepsOtherChars.bend @@ -0,0 +1,12 @@ +# proofs of laws/HtmlKeepsOtherChars.bend +import Base +import ../src/html.bend as Html +import ../laws/HtmlKeepsOtherChars.bend as Laws + +def Laws.html_other(c, e1, e2, e3, e4, e5, rest): + %e1 : {Html.one.amp(c, rest, _) == SCon{c, rest} : String} + %e2 : {Html.one.lt(c, rest, _) == SCon{c, rest} : String} + %e3 : {Html.one.gt(c, rest, _) == SCon{c, rest} : String} + %e4 : {Html.one.dq(c, rest, _) == SCon{c, rest} : String} + %e5 : {Html.one.sq(c, rest, _) == SCon{c, rest} : String} + {==} diff --git a/proofs/HttpsDefaultsTo443.bend b/proofs/HttpsDefaultsTo443.bend new file mode 100644 index 0000000..b3eb220 --- /dev/null +++ b/proofs/HttpsDefaultsTo443.bend @@ -0,0 +1,6 @@ +# proofs of laws/HttpsDefaultsTo443.bend +import Base +import ../laws/HttpsDefaultsTo443.bend as Laws + +def Laws.dflt_https(): + {==} diff --git a/proofs/InterimResponsesSkipped.bend b/proofs/InterimResponsesSkipped.bend new file mode 100644 index 0000000..c4690f0 --- /dev/null +++ b/proofs/InterimResponsesSkipped.bend @@ -0,0 +1,72 @@ +# proofs of laws/InterimResponsesSkipped.bend +import Base +import ../src/http.bend as Http +import ../src/wirespec.bend as Spec +import ../laws/InterimResponsesSkipped.bend as Laws +import ./lemmas/DropNothing.bend as M1 + +def Laws.parse_splits_at_blank_line(_raw, before, after, e): + %e : {Http.parse.cut(_) == Http.parse.head(String.lines(before), after) + : Http.Reply} + {==} + +def Laws.reply_to(_method, _raw): + {==} + +def Laws.reply_fuel(_head, _c): + {==} + +def Laws.reply_open(_f, _head): + {==} + +def Laws.reply_interim(f, head, before, after, e): + %e : {Http.parse.to.step(head, String.lines(before), Http.status.of(String.lines(before)), after, _, + cut => Http.parse.to.go(f, head, cut)) + == Http.parse.to.go(f, head, Http.divide(after, "\r\n\r\n")) + : Http.Reply} + {==} + +def Laws.reply_bare(f, head, before, after, e1, e2, n, e3): + %e1 : {Http.parse.to.step(head, String.lines(before), Http.status.of(String.lines(before)), after, _, + cut => Http.parse.to.go(f, head, cut)) + == Http.Reply{n, Http.head.list(String.lines(before)), ""} : Http.Reply} + %e2 : {Http.parse.to.final(Http.status.of(String.lines(before)), String.lines(before), after, _) + == Http.Reply{n, Http.head.list(String.lines(before)), ""} : Http.Reply} + %e3 : {Http.parse.to.bare(_, String.lines(before)) + == Http.Reply{n, Http.head.list(String.lines(before)), ""} : Http.Reply} + {==} + +def Laws.reply_bare_none(f, head, before, after, e1, e2, e3): + %e1 : {Http.parse.to.step(head, String.lines(before), Http.status.of(String.lines(before)), after, _, + cut => Http.parse.to.go(f, head, cut)) + == Http.Torn{"the response has no status line"} : Http.Reply} + %e2 : {Http.parse.to.final(Http.status.of(String.lines(before)), String.lines(before), after, _) + == Http.Torn{"the response has no status line"} : Http.Reply} + %e3 : {Http.parse.to.bare(_, String.lines(before)) + == Http.Torn{"the response has no status line"} : Http.Reply} + {==} + +def Laws.reply_framed(f, head, before, after, e1, e2): + %e1 : {Http.parse.to.step(head, String.lines(before), Http.status.of(String.lines(before)), after, _, + cut => Http.parse.to.go(f, head, cut)) + == Http.parse.head(String.lines(before), after) : Http.Reply} + %e2 : {Http.parse.to.final(Http.status.of(String.lines(before)), String.lines(before), after, _) + == Http.parse.head(String.lines(before), after) : Http.Reply} + {==} + +def Laws.interim_skip(_m, _after): + {==} + +def Laws.status_empty(_m): + {==} + +def Laws.heard_ok(method, text): + dz = Equal.sym(String, String.drop(text, 0n), text, M1.eq.drop_zero(text)) + %dz : {Http.parse.to(method, _) == Http.parse.to(method, text) : Http.Reply} + {==} + +def Laws.heard_fail(method, raw, e): + %e : {Spec.wire.heard.of(method, raw, _) + == Http.Torn{Spec.wire.why(Spec.wire.code(raw), Spec.wire.text(raw))} + : Http.Reply} + {==} diff --git a/proofs/InterimStatusIs1xx.bend b/proofs/InterimStatusIs1xx.bend new file mode 100644 index 0000000..ad927e0 --- /dev/null +++ b/proofs/InterimStatusIs1xx.bend @@ -0,0 +1,11 @@ +# proofs of laws/InterimStatusIs1xx.bend +import Base +import ../src/http.bend as Http +import ../laws/InterimStatusIs1xx.bend as Laws + +def Laws.status_interim_some(_n): + {==} + +def Laws.status_interim_none(_m, e): + %e : {Http.status.interim(_) == False{} : Bool} + {==} diff --git a/proofs/JoinedPiecesAreText.bend b/proofs/JoinedPiecesAreText.bend new file mode 100644 index 0000000..4bed388 --- /dev/null +++ b/proofs/JoinedPiecesAreText.bend @@ -0,0 +1,51 @@ +# proofs of laws/JoinedPiecesAreText.bend +import Base +import ../src/wirespec.bend as Spec +import ../laws/JoinedPiecesAreText.bend as Laws + +law app_nil: + for x: String + {x ++ "" == x : String} + +def app_nil(x): + match x: + case SNil{}: + {==} + case SCon{+h, +t}: + Equal.cong(String, String, r => SCon{h, r}, t ++ "", t, app_nil(t)) + +law app_assoc: + for x: String + for +y: String + for +z: String + {(x ++ y) ++ z == x ++ (y ++ z) : String} + +def app_assoc(x, y, z): + match x: + case SNil{}: + {==} + case SCon{+h, +t}: + Equal.cong(String, String, r => SCon{h, r}, (t ++ y) ++ z, t ++ (y ++ z), + app_assoc(t, y, z)) + +law join_go: + for ps: List<&2, String> + for +acc: String + {Spec.wire.join.go(ps, acc) == Spec.wire.joined(ps) ++ acc : String} + +def join_go(ps, acc): + match ps: + case []: + {==} + case +p <> +rest: + Equal.trans(String, Spec.wire.join.go(rest, p ++ acc), + Spec.wire.joined(rest) ++ (p ++ acc), + (Spec.wire.joined(rest) ++ p) ++ acc, + join_go(rest, p ++ acc), + Equal.sym(String, (Spec.wire.joined(rest) ++ p) ++ acc, + Spec.wire.joined(rest) ++ (p ++ acc), + app_assoc(Spec.wire.joined(rest), p, acc))) + +def Laws.join_joined(ps): + Equal.trans(String, Spec.wire.join.go(ps, ""), Spec.wire.joined(ps) ++ "", + Spec.wire.joined(ps), join_go(ps, ""), app_nil(Spec.wire.joined(ps))) diff --git a/proofs/JsonRejectsNonJson.bend b/proofs/JsonRejectsNonJson.bend new file mode 100644 index 0000000..c501bb1 --- /dev/null +++ b/proofs/JsonRejectsNonJson.bend @@ -0,0 +1,6 @@ +# proofs of laws/JsonRejectsNonJson.bend +import Base +import ../laws/JsonRejectsNonJson.bend as Laws + +def Laws.json_parse_rejects(): + {==} diff --git a/proofs/JsonStringIsTextBody.bend b/proofs/JsonStringIsTextBody.bend new file mode 100644 index 0000000..0de1558 --- /dev/null +++ b/proofs/JsonStringIsTextBody.bend @@ -0,0 +1,6 @@ +# proofs of laws/JsonStringIsTextBody.bend +import Base +import ../laws/JsonStringIsTextBody.bend as Laws + +def Laws.json_string_is_text_body(): + {==} diff --git a/proofs/JsonStringParsesBack.bend b/proofs/JsonStringParsesBack.bend new file mode 100644 index 0000000..302c8ba --- /dev/null +++ b/proofs/JsonStringParsesBack.bend @@ -0,0 +1,6 @@ +# proofs of laws/JsonStringParsesBack.bend +import Base +import ../laws/JsonStringParsesBack.bend as Laws + +def Laws.json_parse_string(): + {==} diff --git a/proofs/LocationResolvedBySort.bend b/proofs/LocationResolvedBySort.bend new file mode 100644 index 0000000..f0900fc --- /dev/null +++ b/proofs/LocationResolvedBySort.bend @@ -0,0 +1,13 @@ +# proofs of laws/LocationResolvedBySort.bend +import Base +import ../src/redirect.bend as Redirect +import ../laws/LocationResolvedBySort.bend as Laws + +def Laws.target_against(url, loc, e1, scheme, host, port, path, e2): + %e1 : {Redirect.redirect.target.of(url, loc, _) + == Redirect.redirect.against(scheme, host, port, path, loc, + Redirect.redirect.sort(loc)) : Maybe<&2, String>} + %e2 : {Redirect.redirect.at(_, loc) + == Redirect.redirect.against(scheme, host, port, path, loc, + Redirect.redirect.sort(loc)) : Maybe<&2, String>} + {==} diff --git a/proofs/LocationSplitsAtQuery.bend b/proofs/LocationSplitsAtQuery.bend new file mode 100644 index 0000000..e312fce --- /dev/null +++ b/proofs/LocationSplitsAtQuery.bend @@ -0,0 +1,15 @@ +# proofs of laws/LocationSplitsAtQuery.bend +import Base +import ../src/url.bend as Url +import ../src/redirect.bend as Redirect +import ../laws/LocationSplitsAtQuery.bend as Laws + +def Laws.split_query(loc, p, q, e): + %e : {Redirect.redirect.split.of(_, Url.frag.drop(loc)) + == Redirect.Ref{p, "?" ++ q} : Redirect.Ref} + {==} + +def Laws.split_plain(loc, e): + %e : {Redirect.redirect.split.of(_, Url.frag.drop(loc)) + == Redirect.Ref{Url.frag.drop(loc), ""} : Redirect.Ref} + {==} diff --git a/proofs/LowercaseGetNotSafe.bend b/proofs/LowercaseGetNotSafe.bend new file mode 100644 index 0000000..02f74ae --- /dev/null +++ b/proofs/LowercaseGetNotSafe.bend @@ -0,0 +1,6 @@ +# proofs of laws/LowercaseGetNotSafe.bend +import Base +import ../laws/LowercaseGetNotSafe.bend as Laws + +def Laws.method_safe_get_lower(): + {==} diff --git a/proofs/MaxAgeOneFreshThenStale.bend b/proofs/MaxAgeOneFreshThenStale.bend new file mode 100644 index 0000000..a54cd2e --- /dev/null +++ b/proofs/MaxAgeOneFreshThenStale.bend @@ -0,0 +1,6 @@ +# proofs of laws/MaxAgeOneFreshThenStale.bend +import Base +import ../laws/MaxAgeOneFreshThenStale.bend as Laws + +def Laws.fresh_max_age_one(): + {==} diff --git a/proofs/MaxAgeOverridesExpires.bend b/proofs/MaxAgeOverridesExpires.bend new file mode 100644 index 0000000..64c2215 --- /dev/null +++ b/proofs/MaxAgeOverridesExpires.bend @@ -0,0 +1,6 @@ +# proofs of laws/MaxAgeOverridesExpires.bend +import Base +import ../laws/MaxAgeOverridesExpires.bend as Laws + +def Laws.fresh_max_age_overrides_expires(): + {==} diff --git a/proofs/MaxAgeZeroIsStale.bend b/proofs/MaxAgeZeroIsStale.bend new file mode 100644 index 0000000..230bdf3 --- /dev/null +++ b/proofs/MaxAgeZeroIsStale.bend @@ -0,0 +1,6 @@ +# proofs of laws/MaxAgeZeroIsStale.bend +import Base +import ../laws/MaxAgeZeroIsStale.bend as Laws + +def Laws.fresh_max_age_zero(): + {==} diff --git a/proofs/MethodTokensAreCaseSensitive.bend b/proofs/MethodTokensAreCaseSensitive.bend new file mode 100644 index 0000000..ee944f1 --- /dev/null +++ b/proofs/MethodTokensAreCaseSensitive.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodTokensAreCaseSensitive.bend +import Base +import ../laws/MethodTokensAreCaseSensitive.bend as Laws + +def Laws.method_tokens_are_case_sensitive(): + {==} diff --git a/proofs/MissingFieldIsEmpty.bend b/proofs/MissingFieldIsEmpty.bend new file mode 100644 index 0000000..b2489b9 --- /dev/null +++ b/proofs/MissingFieldIsEmpty.bend @@ -0,0 +1,6 @@ +# proofs of laws/MissingFieldIsEmpty.bend +import Base +import ../laws/MissingFieldIsEmpty.bend as Laws + +def Laws.headers_find_missing(): + {==} diff --git a/proofs/NamedReferencesLongestMatch.bend b/proofs/NamedReferencesLongestMatch.bend new file mode 100644 index 0000000..a1f9e0f --- /dev/null +++ b/proofs/NamedReferencesLongestMatch.bend @@ -0,0 +1,51 @@ +# proofs of laws/NamedReferencesLongestMatch.bend +import Base +import ../src/unescape.bend as Ref +import ../laws/NamedReferencesLongestMatch.bend as Laws + +def Laws.ref_named(_s): + {==} + +def Laws.ref_named_none(_s): + {==} + +def Laws.ref_named_some(_name, _first, _second, _s): + {==} + +def Laws.ref_best_nil(_s, _sofar): + {==} + +def Laws.ref_best_cons(_r, _t, _s, _sofar): + {==} + +def Laws.ref_step(_sofar, _name, _first, _second, _s): + {==} + +def Laws.ref_hit_miss(_sofar, _r, _name): + {==} + +def Laws.ref_hit_take(_sofar, _r, _name): + {==} + +def Laws.ref_take_longer(_sofar, _r): + {==} + +def Laws.ref_take_keep(_sofar, _r): + {==} + +def Laws.ref_len_none(_m, e): + %e : {Ref.best.len(_) == 0n : Nat} + {==} + +def Laws.ref_len_some(_name, _first, _second): + {==} + +def Laws.ref_chars_one(first, second, e): + %e : {SCon{Chr{first}, Bool.pick(String, _, "", SCon{Chr{second}, ""})} + == SCon{Chr{first}, ""} : String} + {==} + +def Laws.ref_chars_two(first, second, e): + %e : {SCon{Chr{first}, Bool.pick(String, _, "", SCon{Chr{second}, ""})} + == SCon{Chr{first}, SCon{Chr{second}, ""}} : String} + {==} diff --git a/proofs/NoCacheNeverFresh.bend b/proofs/NoCacheNeverFresh.bend new file mode 100644 index 0000000..f9a38df --- /dev/null +++ b/proofs/NoCacheNeverFresh.bend @@ -0,0 +1,18 @@ +# proofs of laws/NoCacheNeverFresh.bend +import Base +import ../laws/NoCacheNeverFresh.bend as Laws + +# fresh_no_cache_any: no-store either way, then no-cache +def Laws.fresh_no_cache_any( + no_store, + _public, + _private, + _max_age, + _expires, + _age +): + match no_store: + case False{}: + {==} + case True{}: + {==} diff --git a/proofs/NoCacheNotFresh.bend b/proofs/NoCacheNotFresh.bend new file mode 100644 index 0000000..8dfccb8 --- /dev/null +++ b/proofs/NoCacheNotFresh.bend @@ -0,0 +1,6 @@ +# proofs of laws/NoCacheNotFresh.bend +import Base +import ../laws/NoCacheNotFresh.bend as Laws + +def Laws.fresh_no_cache(): + {==} diff --git a/proofs/NoMaxAgeNotFresh.bend b/proofs/NoMaxAgeNotFresh.bend new file mode 100644 index 0000000..f9f12ae --- /dev/null +++ b/proofs/NoMaxAgeNotFresh.bend @@ -0,0 +1,6 @@ +# proofs of laws/NoMaxAgeNotFresh.bend +import Base +import ../laws/NoMaxAgeNotFresh.bend as Laws + +def Laws.fresh_public_only(): + {==} diff --git a/proofs/NoStoreNeverFresh.bend b/proofs/NoStoreNeverFresh.bend new file mode 100644 index 0000000..624a85e --- /dev/null +++ b/proofs/NoStoreNeverFresh.bend @@ -0,0 +1,14 @@ +# proofs of laws/NoStoreNeverFresh.bend +import Base +import ../laws/NoStoreNeverFresh.bend as Laws + +# fresh_no_store_any: fresh matches no-store first +def Laws.fresh_no_store_any( + _no_cache, + _public, + _private, + _max_age, + _expires, + _age +): + {==} diff --git a/proofs/NoStoreNotFresh.bend b/proofs/NoStoreNotFresh.bend new file mode 100644 index 0000000..cf1947a --- /dev/null +++ b/proofs/NoStoreNotFresh.bend @@ -0,0 +1,6 @@ +# proofs of laws/NoStoreNotFresh.bend +import Base +import ../laws/NoStoreNotFresh.bend as Laws + +def Laws.fresh_no_store(): + {==} diff --git a/proofs/NonHeadReplyKeepsBody.bend b/proofs/NonHeadReplyKeepsBody.bend new file mode 100644 index 0000000..593b7db --- /dev/null +++ b/proofs/NonHeadReplyKeepsBody.bend @@ -0,0 +1,6 @@ +# proofs of laws/NonHeadReplyKeepsBody.bend +import Base +import ../laws/NonHeadReplyKeepsBody.bend as Laws + +def Laws.get_reply_keeps_body(): + {==} diff --git a/proofs/NonHexChunkSizeTorn.bend b/proofs/NonHexChunkSizeTorn.bend new file mode 100644 index 0000000..c93e6e5 --- /dev/null +++ b/proofs/NonHexChunkSizeTorn.bend @@ -0,0 +1,6 @@ +# proofs of laws/NonHexChunkSizeTorn.bend +import Base +import ../laws/NonHexChunkSizeTorn.bend as Laws + +def Laws.parse_rejects_non_hex_chunk(): + {==} diff --git a/proofs/NumericReferencesDecode.bend b/proofs/NumericReferencesDecode.bend new file mode 100644 index 0000000..d18c9ca --- /dev/null +++ b/proofs/NumericReferencesDecode.bend @@ -0,0 +1,54 @@ +# proofs of laws/NumericReferencesDecode.bend +import Base +import ../src/unescape.bend as Ref +import ../laws/NumericReferencesDecode.bend as Laws + +def Laws.ref_code(_n): + {==} + +def Laws.ref_code_zero(n, e): + %e : {Ref.code.zero(n, _) == SCon{Chr{65533}, ""} : String} + {==} + +def Laws.ref_code_cr(n, e1, e2): + %e1 : {Ref.code.zero(n, _) == "\r" : String} + %e2 : {Ref.code.cr(n, _) == "\r" : String} + {==} + +def Laws.ref_code_c1(n, e1, e2, e3): + %e1 : {Ref.code.zero(n, _) + == SCon{Chr{Ref.nth(Ref.w1252(), U32.to_nat(U32.sub(n, 128)))}, ""} + : String} + %e2 : {Ref.code.cr(n, _) + == SCon{Chr{Ref.nth(Ref.w1252(), U32.to_nat(U32.sub(n, 128)))}, ""} + : String} + %e3 : {Ref.code.c1(n, _) + == SCon{Chr{Ref.nth(Ref.w1252(), U32.to_nat(U32.sub(n, 128)))}, ""} + : String} + {==} + +def Laws.ref_code_bad(n, e1, e2, e3, e4): + %e1 : {Ref.code.zero(n, _) == SCon{Chr{65533}, ""} : String} + %e2 : {Ref.code.cr(n, _) == SCon{Chr{65533}, ""} : String} + %e3 : {Ref.code.c1(n, _) == SCon{Chr{65533}, ""} : String} + %e4 : {Ref.code.range(n, _) == SCon{Chr{65533}, ""} : String} + {==} + +def Laws.ref_code_dropped(n, e1, e2, e3, e4, e5): + %e1 : {Ref.code.zero(n, _) == "" : String} + %e2 : {Ref.code.cr(n, _) == "" : String} + %e3 : {Ref.code.c1(n, _) == "" : String} + %e4 : {Ref.code.range(n, _) == "" : String} + %e5 : {Ref.code.kept(n, _) == "" : String} + {==} + +def Laws.ref_code_kept(n, e1, e2, e3, e4, e5): + %e1 : {Ref.code.zero(n, _) == SCon{Chr{n}, ""} : String} + %e2 : {Ref.code.cr(n, _) == SCon{Chr{n}, ""} : String} + %e3 : {Ref.code.c1(n, _) == SCon{Chr{n}, ""} : String} + %e4 : {Ref.code.range(n, _) == SCon{Chr{n}, ""} : String} + %e5 : {Ref.code.kept(n, _) == SCon{Chr{n}, ""} : String} + {==} + +def Laws.ref_dropped(_n): + {==} diff --git a/proofs/ObsFoldRejected.bend b/proofs/ObsFoldRejected.bend new file mode 100644 index 0000000..eb27a5b --- /dev/null +++ b/proofs/ObsFoldRejected.bend @@ -0,0 +1,6 @@ +# proofs of laws/ObsFoldRejected.bend +import Base +import ../laws/ObsFoldRejected.bend as Laws + +def Laws.head_line_rejects_obs_fold(): + {==} diff --git a/proofs/OctetBodiesSentUnchanged.bend b/proofs/OctetBodiesSentUnchanged.bend new file mode 100644 index 0000000..78ceef8 --- /dev/null +++ b/proofs/OctetBodiesSentUnchanged.bend @@ -0,0 +1,63 @@ +# proofs of laws/OctetBodiesSentUnchanged.bend +import Base +import ../src/http.bend as Http +import ../laws/OctetBodiesSentUnchanged.bend as Laws +import ./lemmas/HeaderFieldSlots.bend as M1 + +def Laws.exchange_body_text(_method, _l, _hs, _text): + {==} + +def Laws.exchange_body_empty(_method, _l, _hs): + {==} + +def Laws.exchange_body_octets(_method, _l, _hs, _n, _buf): + {==} + +def Laws.exchange_octets_bad(_method, _why, _hs, _n): + {==} + +def Laws.exchange_octets_loc(_method, _scheme, _host, _port, _path, _hs, _n): + {==} + +def Laws.request_head_at(_method, _secure, _host, _port, _path, _hs, _n): + {==} + +def Laws.fields_n(_host, _hs, _body): + {==} + +def Laws.fields_clen_n(host, hs, n): + M1.hd.of_fields(Http.FClen{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FOther{}, hs), + Http.clen.field(n, Nat.is_eq(n, 0n)), + [], [], [], [], + Http.clen.field(n, + Nat.is_eq(n, 0n)), + M1.hd.or_gone(Http.FClen{}, Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}], + M1.hd.disj(Http.FClen{}, Http.FHost{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FClen{}, Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}], + M1.hd.disj(Http.FClen{}, Http.FAgent{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FClen{}, Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}], + M1.hd.disj(Http.FClen{}, Http.FAccept{}, {==}, hs), {==}), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FClen{}, Http.fields.of(Http.FOther{}, hs)), [], + M1.hd.disj(Http.FClen{}, Http.FOther{}, {==}, hs)), + M1.hd.clen_self(n, Nat.is_eq(n, 0n))) + +# --- what is sent --- + +def Laws.payload_nil(_rest): + {==} + +def Laws.payload_cons(_cp, _t, _rest): + {==} + +def Laws.payload_is(_req): + {==} diff --git a/proofs/OctetsDecodeAsUtf8.bend b/proofs/OctetsDecodeAsUtf8.bend new file mode 100644 index 0000000..f7ce0d4 --- /dev/null +++ b/proofs/OctetsDecodeAsUtf8.bend @@ -0,0 +1,22 @@ +# proofs of laws/OctetsDecodeAsUtf8.bend +import Base +import ../src/http.bend as Http +import ../src/qs.bend as Qs +import ../laws/OctetsDecodeAsUtf8.bend as Laws + +def Laws.octets_text(_o): + {==} + +def Laws.octets_toks_nil(_s, e): + %e : {Qs.octets.toks(_) == [] : List<&2, Qs.Tok>} + {==} + +def Laws.octets_toks_cons(_c, _t): + {==} + +def Laws.octets_headers_cons(_name, _value, _t): + {==} + +def Laws.octets_headers_nil(_hs, e): + %e : {Qs.octets.headers(_) == [] : List<&2, Http.Header>} + {==} diff --git a/proofs/OneOctetPerChar.bend b/proofs/OneOctetPerChar.bend new file mode 100644 index 0000000..876eea5 --- /dev/null +++ b/proofs/OneOctetPerChar.bend @@ -0,0 +1,7 @@ +# proofs of laws/OneOctetPerChar.bend +import Base +import ../laws/OneOctetPerChar.bend as Laws +import ./lemmas/OctetLength.bend as M1 +import ./lemmas/OctetTakeDrop.bend as M2 + + diff --git a/proofs/OnlyGetFollows.bend b/proofs/OnlyGetFollows.bend new file mode 100644 index 0000000..846dc7b --- /dev/null +++ b/proofs/OnlyGetFollows.bend @@ -0,0 +1,18 @@ +# proofs of laws/OnlyGetFollows.bend +import Base +import ../laws/OnlyGetFollows.bend as Laws + +def Laws.follows_get(follow): + match follow: + case True{}: + {==} + case False{}: + {==} + +def Laws.follows_other(_method, e, follow): + match follow: + case True{}: + %e : {_ == False{} : Bool} + {==} + case False{}: + {==} diff --git a/proofs/OptionsAsteriskNamesServer.bend b/proofs/OptionsAsteriskNamesServer.bend new file mode 100644 index 0000000..2db4387 --- /dev/null +++ b/proofs/OptionsAsteriskNamesServer.bend @@ -0,0 +1,6 @@ +# proofs of laws/OptionsAsteriskNamesServer.bend +import Base +import ../laws/OptionsAsteriskNamesServer.bend as Laws + +def Laws.ask_options_asterisk(): + {==} diff --git a/proofs/OptionsIsIdempotent.bend b/proofs/OptionsIsIdempotent.bend new file mode 100644 index 0000000..94594c4 --- /dev/null +++ b/proofs/OptionsIsIdempotent.bend @@ -0,0 +1,6 @@ +# proofs of laws/OptionsIsIdempotent.bend +import Base +import ../laws/OptionsIsIdempotent.bend as Laws + +def Laws.method_idempotent_options(): + {==} diff --git a/proofs/OptionsIsSafe.bend b/proofs/OptionsIsSafe.bend new file mode 100644 index 0000000..7963ce7 --- /dev/null +++ b/proofs/OptionsIsSafe.bend @@ -0,0 +1,6 @@ +# proofs of laws/OptionsIsSafe.bend +import Base +import ../laws/OptionsIsSafe.bend as Laws + +def Laws.method_safe_options(): + {==} diff --git a/proofs/OptionsRequestLine.bend b/proofs/OptionsRequestLine.bend new file mode 100644 index 0000000..0cda2d4 --- /dev/null +++ b/proofs/OptionsRequestLine.bend @@ -0,0 +1,10 @@ +# proofs of laws/OptionsRequestLine.bend +import Base +import ../src/http.bend as Http +import ../laws/OptionsRequestLine.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.options_req_names_path(host, path, hs): + M1.eq.starts_append(path, + " HTTP/1.1\r\n" ++ Http.headers.lines(Http.request.fields(host, hs, "")) + ++ "\r\n") diff --git a/proofs/OriginFormKeepsQuery.bend b/proofs/OriginFormKeepsQuery.bend new file mode 100644 index 0000000..6506f28 --- /dev/null +++ b/proofs/OriginFormKeepsQuery.bend @@ -0,0 +1,6 @@ +# proofs of laws/OriginFormKeepsQuery.bend +import Base +import ../laws/OriginFormKeepsQuery.bend as Laws + +def Laws.ask_origin_form_keeps_query(): + {==} diff --git a/proofs/OtherSchemesDefaultTo80.bend b/proofs/OtherSchemesDefaultTo80.bend new file mode 100644 index 0000000..61c782f --- /dev/null +++ b/proofs/OtherSchemesDefaultTo80.bend @@ -0,0 +1,7 @@ +# proofs of laws/OtherSchemesDefaultTo80.bend +import Base +import ../laws/OtherSchemesDefaultTo80.bend as Laws + +def Laws.dflt_plain(_scheme, e): + %e : {Bool.pick(U32, _, 443, 80) == 80 : U32} + {==} diff --git a/proofs/OtherStatusesStopChain.bend b/proofs/OtherStatusesStopChain.bend new file mode 100644 index 0000000..c487bbc --- /dev/null +++ b/proofs/OtherStatusesStopChain.bend @@ -0,0 +1,16 @@ +# proofs of laws/OtherStatusesStopChain.bend +import Base +import ../src/redirect.bend as Redirect +import ../laws/OtherStatusesStopChain.bend as Laws + +def Laws.redir_other(_status, e, left, url, loc): + match left: + case 0n: + {==} + case 1n+p: + %e : {Redirect.redirect.on(url, loc, _) == Redirect.Stop{} + : Redirect.Hop} + {==} + +def Laws.redir_zero(_url, _status, _loc): + {==} diff --git a/proofs/ParseQuerySplitsPairs.bend b/proofs/ParseQuerySplitsPairs.bend new file mode 100644 index 0000000..bf969c5 --- /dev/null +++ b/proofs/ParseQuerySplitsPairs.bend @@ -0,0 +1,28 @@ +# proofs of laws/ParseQuerySplitsPairs.bend +import Base +import ../src/qs.bend as Qs +import ../laws/ParseQuerySplitsPairs.bend as Laws + +def Laws.qs_parse(_text): + {==} + +def Laws.qs_fields_nil(_ps, e): + %e : {Qs.fields(_) == [] : List<&2, Qs.Kv()>} + {==} + +def Laws.qs_fields_skip(_t): + {==} + +def Laws.qs_fields_keep(p, t, e): + %e : {Qs.fields.put(p, Qs.fields(t), _) == Qs.field(p) <> Qs.fields(t) + : List<&2, Qs.Kv()>} + {==} + +def Laws.qs_field(_piece): + {==} + +def Laws.qs_field_none(_whole): + {==} + +def Laws.qs_field_some(_name, _value, _whole): + {==} diff --git a/proofs/PatchNotIdempotent.bend b/proofs/PatchNotIdempotent.bend new file mode 100644 index 0000000..1420a65 --- /dev/null +++ b/proofs/PatchNotIdempotent.bend @@ -0,0 +1,6 @@ +# proofs of laws/PatchNotIdempotent.bend +import Base +import ../laws/PatchNotIdempotent.bend as Laws + +def Laws.method_idempotent_patch(): + {==} diff --git a/proofs/PathFormKeepsSlash.bend b/proofs/PathFormKeepsSlash.bend new file mode 100644 index 0000000..ba67556 --- /dev/null +++ b/proofs/PathFormKeepsSlash.bend @@ -0,0 +1,16 @@ +# proofs of laws/PathFormKeepsSlash.bend +import Base +import ../src/pct.bend as Pct +import ../laws/PathFormKeepsSlash.bend as Laws + +def Laws.path_keeps_slash(_rest): + {==} + +def Laws.component_escapes_slash(_rest): + {==} + +def Laws.component_is_path_elsewhere(c, e, rest): + %e : {Pct.one.of(c, rest, Bool.or(Pct.unreserved(c), Bool.and(_, False{}))) + == Pct.one.of(c, rest, Bool.or(Pct.unreserved(c), Bool.and(_, True{}))) + : String} + {==} diff --git a/proofs/PhraseTableLookup.bend b/proofs/PhraseTableLookup.bend new file mode 100644 index 0000000..49dcbad --- /dev/null +++ b/proofs/PhraseTableLookup.bend @@ -0,0 +1,17 @@ +# proofs of laws/PhraseTableLookup.bend +import Base +import ../src/http.bend as Http +import ../laws/PhraseTableLookup.bend as Laws + +def Laws.reason_in_nil(_code): + {==} + +def Laws.reason_in_hit(code, _c, text, t, e): + %e : {Http.reason.pick(text, _, _u => Http.reason.in(t, code)) == text + : String} + {==} + +def Laws.reason_in_miss(code, _c, text, t, e): + %e : {Http.reason.pick(text, _, _u => Http.reason.in(t, code)) + == Http.reason.in(t, code) : String} + {==} diff --git a/proofs/PostNotIdempotent.bend b/proofs/PostNotIdempotent.bend new file mode 100644 index 0000000..1bcd08a --- /dev/null +++ b/proofs/PostNotIdempotent.bend @@ -0,0 +1,6 @@ +# proofs of laws/PostNotIdempotent.bend +import Base +import ../laws/PostNotIdempotent.bend as Laws + +def Laws.method_idempotent_post(): + {==} diff --git a/proofs/PostNotSafe.bend b/proofs/PostNotSafe.bend new file mode 100644 index 0000000..424b73c --- /dev/null +++ b/proofs/PostNotSafe.bend @@ -0,0 +1,6 @@ +# proofs of laws/PostNotSafe.bend +import Base +import ../laws/PostNotSafe.bend as Laws + +def Laws.method_safe_post(): + {==} diff --git a/proofs/PostRequestLine.bend b/proofs/PostRequestLine.bend new file mode 100644 index 0000000..134dab0 --- /dev/null +++ b/proofs/PostRequestLine.bend @@ -0,0 +1,12 @@ +# proofs of laws/PostRequestLine.bend +import Base +import ../src/http.bend as Http +import ../laws/PostRequestLine.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.post_req_names_path(host, path, hs, body): + +payload = body + M1.eq.starts_append(path, + " HTTP/1.1\r\n" + ++ Http.headers.lines(Http.request.fields(host, hs, payload)) + ++ "\r\n" ++ payload) diff --git a/proofs/PostWritesContentLength.bend b/proofs/PostWritesContentLength.bend new file mode 100644 index 0000000..2d64572 --- /dev/null +++ b/proofs/PostWritesContentLength.bend @@ -0,0 +1,6 @@ +# proofs of laws/PostWritesContentLength.bend +import Base +import ../laws/PostWritesContentLength.bend as Laws + +def Laws.post_writes_content_length(): + {==} diff --git a/proofs/PutIsIdempotent.bend b/proofs/PutIsIdempotent.bend new file mode 100644 index 0000000..76dd3ce --- /dev/null +++ b/proofs/PutIsIdempotent.bend @@ -0,0 +1,6 @@ +# proofs of laws/PutIsIdempotent.bend +import Base +import ../laws/PutIsIdempotent.bend as Laws + +def Laws.method_idempotent_put(): + {==} diff --git a/proofs/PutNotSafe.bend b/proofs/PutNotSafe.bend new file mode 100644 index 0000000..20f163c --- /dev/null +++ b/proofs/PutNotSafe.bend @@ -0,0 +1,6 @@ +# proofs of laws/PutNotSafe.bend +import Base +import ../laws/PutNotSafe.bend as Laws + +def Laws.method_safe_put(): + {==} diff --git a/proofs/PutRequestLine.bend b/proofs/PutRequestLine.bend new file mode 100644 index 0000000..900da85 --- /dev/null +++ b/proofs/PutRequestLine.bend @@ -0,0 +1,12 @@ +# proofs of laws/PutRequestLine.bend +import Base +import ../src/http.bend as Http +import ../laws/PutRequestLine.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.put_req_names_path(host, path, hs, body): + +payload = body + M1.eq.starts_append(path, + " HTTP/1.1\r\n" + ++ Http.headers.lines(Http.request.fields(host, hs, payload)) + ++ "\r\n" ++ payload) diff --git a/proofs/QueryGetLastWins.bend b/proofs/QueryGetLastWins.bend new file mode 100644 index 0000000..99df82b --- /dev/null +++ b/proofs/QueryGetLastWins.bend @@ -0,0 +1,24 @@ +# proofs of laws/QueryGetLastWins.bend +import Base +import ../src/qs.bend as Qs +import ../laws/QueryGetLastWins.bend as Laws + +def Laws.qs_get_nil(_name): + {==} + +def Laws.qs_get_later(key, value, _t, name, x, e): + %e : {Qs.get.pick(_, value, String.eq(key, name)) == Some{x} + : Maybe<&2, String>} + {==} + +def Laws.qs_get_here(key, value, _t, name, e1, e2): + %e1 : {Qs.get.pick(_, value, String.eq(key, name)) == Some{value} + : Maybe<&2, String>} + %e2 : {Qs.get.pick(None{}, value, _) == Some{value} : Maybe<&2, String>} + {==} + +def Laws.qs_get_miss(key, value, _t, name, e1, e2): + %e1 : {Qs.get.pick(_, value, String.eq(key, name)) == None{} + : Maybe<&2, String>} + %e2 : {Qs.get.pick(None{}, value, _) == None{} : Maybe<&2, String>} + {==} diff --git a/proofs/QuoteEscapesUtf8Bytes.bend b/proofs/QuoteEscapesUtf8Bytes.bend new file mode 100644 index 0000000..f91b692 --- /dev/null +++ b/proofs/QuoteEscapesUtf8Bytes.bend @@ -0,0 +1,62 @@ +# proofs of laws/QuoteEscapesUtf8Bytes.bend +import Base +import ../src/pct.bend as Pct +import ../laws/QuoteEscapesUtf8Bytes.bend as Laws + +def Laws.one_escaped(_slash, c, e, rest): + %e : {Pct.one.of(c, rest, _) == Pct.pct(Pct.utf8(c), rest) : String} + {==} + +def Laws.pct_nil(_rest): + {==} + +def Laws.pct_cons(_b, _bs, _rest): + {==} + +def Laws.hex_digit(n, e): + %e : {Chr{Bool.pick(U32, _, U32.add(48, n), U32.add(55, n))} + == Chr{U32.add(48, n)} : Char} + {==} + +def Laws.hex_letter(n, e): + %e : {Chr{Bool.pick(U32, _, U32.add(48, n), U32.add(55, n))} + == Chr{U32.add(55, n)} : Char} + {==} + +def Laws.utf8_one(c, e): + %e : {Pct.utf8.one(c, _) == [c] : List<&2, U32>} + {==} + +def Laws.utf8_two(c, e1, e2): + %e1 : {Pct.utf8.one(c, _) == [U32.or(192, U32.shrn(c, 6n)), + U32.or(128, U32.and(c, 63))] : List<&2, U32>} + %e2 : {Pct.utf8.two(c, _) == [U32.or(192, U32.shrn(c, 6n)), + U32.or(128, U32.and(c, 63))] : List<&2, U32>} + {==} + +def Laws.utf8_three(c, e1, e2, e3): + %e1 : {Pct.utf8.one(c, _) == [U32.or(224, U32.shrn(c, 12n)), + U32.or(128, U32.and(U32.shrn(c, 6n), 63)), + U32.or(128, U32.and(c, 63))] : List<&2, U32>} + %e2 : {Pct.utf8.two(c, _) == [U32.or(224, U32.shrn(c, 12n)), + U32.or(128, U32.and(U32.shrn(c, 6n), 63)), + U32.or(128, U32.and(c, 63))] : List<&2, U32>} + %e3 : {Pct.utf8.three(c, _) == [U32.or(224, U32.shrn(c, 12n)), + U32.or(128, U32.and(U32.shrn(c, 6n), 63)), + U32.or(128, U32.and(c, 63))] : List<&2, U32>} + {==} + +def Laws.utf8_four(c, e1, e2, e3): + %e1 : {Pct.utf8.one(c, _) == [U32.or(240, U32.shrn(c, 18n)), + U32.or(128, U32.and(U32.shrn(c, 12n), 63)), + U32.or(128, U32.and(U32.shrn(c, 6n), 63)), + U32.or(128, U32.and(c, 63))] : List<&2, U32>} + %e2 : {Pct.utf8.two(c, _) == [U32.or(240, U32.shrn(c, 18n)), + U32.or(128, U32.and(U32.shrn(c, 12n), 63)), + U32.or(128, U32.and(U32.shrn(c, 6n), 63)), + U32.or(128, U32.and(c, 63))] : List<&2, U32>} + %e3 : {Pct.utf8.three(c, _) == [U32.or(240, U32.shrn(c, 18n)), + U32.or(128, U32.and(U32.shrn(c, 12n), 63)), + U32.or(128, U32.and(U32.shrn(c, 6n), 63)), + U32.or(128, U32.and(c, 63))] : List<&2, U32>} + {==} diff --git a/proofs/QuoteKeepsUnreserved.bend b/proofs/QuoteKeepsUnreserved.bend new file mode 100644 index 0000000..8908977 --- /dev/null +++ b/proofs/QuoteKeepsUnreserved.bend @@ -0,0 +1,17 @@ +# proofs of laws/QuoteKeepsUnreserved.bend +import Base +import ../src/pct.bend as Pct +import ../laws/QuoteKeepsUnreserved.bend as Laws + +# --- percent-encoding --- + +def Laws.quote_nil(_slash): + {==} + +def Laws.quote_cons(_slash, _c, _t): + {==} + +def Laws.one_unreserved(slash, c, e, rest): + %e : {Pct.one.of(c, rest, Bool.or(_, Bool.and(U32.is_eq(c, 47), slash))) + == SCon{Chr{c}, rest} : String} + {==} diff --git a/proofs/QuoteOutputIsFramed.bend b/proofs/QuoteOutputIsFramed.bend new file mode 100644 index 0000000..a7e8b7a --- /dev/null +++ b/proofs/QuoteOutputIsFramed.bend @@ -0,0 +1,94 @@ +# proofs of laws/QuoteOutputIsFramed.bend +import Base +import ../src/pct.bend as Pct +import ../laws/QuoteOutputIsFramed.bend as Laws +import ../laws/defs/PercentFrame.bend as D1 +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.frame_kept(slash, c, e, rest): + %e : {D1.framed(slash, Pct.one.of(c, rest, _), 0n) + == D1.framed(slash, rest, 0n) : Bool} + %e : {Bool.and(Bool.or(_, U32.is_eq(c, 37)), + D1.framed(slash, rest, + Bool.pick(Nat, _, 0n, Bool.pick(Nat, U32.is_eq(c, 37), 2n, 0n)))) + == D1.framed(slash, rest, 0n) : Bool} + {==} + +# LAW: one %XX triple whose nibbles are hex digits stays in the frame +law frame_bytes.step: + for +slash: Bool + for +b: U32 + for t: List<&2, U32> + for +rest: String + for e1: {True{} == D1.hexdig(Pct.hex(U32.shrn(b, 4n))) : Bool} + for e2: {True{} == D1.hexdig(Pct.hex(U32.and(b, 15))) : Bool} + for ih: {D1.framed(slash, Pct.pct(t, rest), 0n) + == D1.framed(slash, rest, 0n) : Bool} + {D1.framed(slash, Pct.pct(b <> t, rest), 0n) + == D1.framed(slash, rest, 0n) : Bool} + +def frame_bytes.step(slash, b, t, rest, e1, e2, ih): + %e1 : {Bool.and(_, Bool.and(D1.hexdig(Pct.hex(U32.and(b, 15))), + D1.framed(slash, Pct.pct(t, rest), 0n))) + == D1.framed(slash, rest, 0n) : Bool} + %e2 : {Bool.and(_, D1.framed(slash, Pct.pct(t, rest), 0n)) + == D1.framed(slash, rest, 0n) : Bool} + ih + +def Laws.frame_bytes(slash, bs, e, rest): + match bs: + case []: + {==} + case +b <> +t: + frame_bytes.step(slash, b, t, rest, + Equal.sym(Bool, D1.hexdig(Pct.hex(U32.shrn(b, 4n))), True{}, + M1.eq.and.left(D1.hexdig(Pct.hex(U32.shrn(b, 4n))), + Bool.and(D1.hexdig(Pct.hex(U32.and(b, 15))), Laws.hexes(t)), e)), + Equal.sym(Bool, D1.hexdig(Pct.hex(U32.and(b, 15))), True{}, + M1.eq.and.left(D1.hexdig(Pct.hex(U32.and(b, 15))), Laws.hexes(t), + M1.eq.and.right(D1.hexdig(Pct.hex(U32.shrn(b, 4n))), + Bool.and(D1.hexdig(Pct.hex(U32.and(b, 15))), Laws.hexes(t)), + e))), + Laws.frame_bytes(slash, t, + M1.eq.and.right(D1.hexdig(Pct.hex(U32.and(b, 15))), Laws.hexes(t), + M1.eq.and.right(D1.hexdig(Pct.hex(U32.shrn(b, 4n))), + Bool.and(D1.hexdig(Pct.hex(U32.and(b, 15))), Laws.hexes(t)), + e)), + rest)) + +# LAW: one character stays in the frame, kept or escaped +law frame_char: + for +slash: Bool + for +c: U32 + for +k: Bool + for +ek: {k == Pct.safe(slash, c) : Bool} + for +h: {Bool.or(k, Laws.hexes(Pct.utf8(c))) == True{} : Bool} + for +rest: String + {D1.framed(slash, Pct.one(slash, c, rest), 0n) + == D1.framed(slash, rest, 0n) : Bool} + +def frame_char(slash, c, k, ek, h, rest): + match k: + case True{}: + Laws.frame_kept(slash, c, ek, rest) + case False{}: + %ek : {D1.framed(slash, Pct.one.of(c, rest, _), 0n) + == D1.framed(slash, rest, 0n) : Bool} + Laws.frame_bytes(slash, Pct.utf8(c), h, rest) + +def Laws.frame_quote(slash, str, e): + match str: + case SNil{}: + {==} + case SCon{Chr{+c}, +t}: + Equal.trans(Bool, + D1.framed(slash, Pct.one(slash, c, Pct.quote(slash, t)), 0n), + D1.framed(slash, Pct.quote(slash, t), 0n), + True{}, + frame_char(slash, c, Pct.safe(slash, c), {==}, + M1.eq.and.left(Bool.or(Pct.safe(slash, c), Laws.hexes(Pct.utf8(c))), + Laws.nibbles(slash, t), e), + Pct.quote(slash, t)), + Laws.frame_quote(slash, t, + M1.eq.and.right(Bool.or(Pct.safe(slash, c), Laws.hexes(Pct.utf8(c))), + Laws.nibbles(slash, t), e))) diff --git a/proofs/QuotePlusWritesSpaces.bend b/proofs/QuotePlusWritesSpaces.bend new file mode 100644 index 0000000..2adbd77 --- /dev/null +++ b/proofs/QuotePlusWritesSpaces.bend @@ -0,0 +1,17 @@ +# proofs of laws/QuotePlusWritesSpaces.bend +import Base +import ../src/pct.bend as Pct +import ../src/qs.bend as Qs +import ../laws/QuotePlusWritesSpaces.bend as Laws + +def Laws.qs_qp_nil(_s, e): + %e : {Qs.quote.plus(_) == "" : String} + {==} + +def Laws.qs_qp_space(_t): + {==} + +def Laws.qs_qp_other(cp, t, e): + %e : {Qs.plus.one(cp, Qs.quote.plus(t), _) + == Pct.one(False{}, cp, Qs.quote.plus(t)) : String} + {==} diff --git a/proofs/ReadStateMatchesWholeText.bend b/proofs/ReadStateMatchesWholeText.bend new file mode 100644 index 0000000..09105c4 --- /dev/null +++ b/proofs/ReadStateMatchesWholeText.bend @@ -0,0 +1,6 @@ +# proofs of laws/ReadStateMatchesWholeText.bend +import Base +import ../laws/ReadStateMatchesWholeText.bend as Laws +import ./lemmas/WalksOverAppendedText.bend as M1 + + diff --git a/proofs/ReadsJudgedByCount.bend b/proofs/ReadsJudgedByCount.bend new file mode 100644 index 0000000..4370225 --- /dev/null +++ b/proofs/ReadsJudgedByCount.bend @@ -0,0 +1,116 @@ +# proofs of laws/ReadsJudgedByCount.bend +import Base +import ../src/http.bend as Http +import ../src/wirespec.bend as Spec +import ../laws/OneOctetPerChar.bend as L1 +import ../laws/ReadStateMatchesWholeText.bend as L2 +import ../laws/ReadsJudgedByCount.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 +import ./lemmas/NatOrder.bend as M2 +import ./lemmas/OctetLength.bend as M3 +import ./lemmas/WalksOverAppendedText.bend as M4 + +# --- reading packed octets, read by read --- + +def Laws.read_closed(_h): + {==} + +def Laws.skip_short(b, n, e): + match b: + case SNil{}: + Equal.cong(Nat, Http.Skip, x => Http.Short{x}, n, Nat.sub(n, 0n), + Equal.sym(Nat, Nat.sub(n, 0n), n, M2.sub_zero(n))) + case SCon{+h, +t}: + match n: + case 0n: + M1.eq.bit.absurd(&2, {Http.octs.skip(SCon{h, t}, 0n) + == Http.Short{Nat.sub(0n, String.length(SCon{h, t}))} : Http.Skip}, e) + case 1n+k: + match k: + case 0n: + Laws.skip_short(t, 0n, e) + case 1n+j: + Laws.skip_short(t, 1n+j, e) + +def Laws.hear_count(h, b, e): + match h: + case Http.Heads{fuel, asked, raw}: + M1.eq.bit.absurd(&2, {Http.hear.more.n(Http.Heads{fuel, asked, raw}, String.length(b)) + == Http.hear.more(Http.Heads{fuel, asked, raw}, b) : Http.Hear}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.Whole{}: + {==} + case Http.Never{}: + {==} + case Http.Count{n, have}: + Equal.cong(Nat, Http.Hear, x => Http.Count{n, Nat.add(have, x)}, + String.length(b), Http.octs.len(b), + Equal.sym(Nat, Http.octs.len(b), String.length(b), L1.octs_len(b))) + case Http.Walking{w}: + match w: + case Http.Ended{}: + M1.eq.bit.absurd(&2, {Http.hear.more.n(Http.Walking{Http.Ended{}}, String.length(b)) + == Http.hear.more(Http.Walking{Http.Ended{}}, b) : Http.Hear}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.Hold{fuel, rest}: + M1.eq.bit.absurd(&2, {Http.hear.more.n(Http.Walking{Http.Hold{fuel, rest}}, String.length(b)) + == Http.hear.more(Http.Walking{Http.Hold{fuel, rest}}, b) : Http.Hear}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.Tail{fuel, left, rest}: + M1.eq.bit.absurd(&2, {Http.hear.more.n(Http.Walking{Http.Tail{fuel, left, rest}}, String.length(b)) + == Http.hear.more(Http.Walking{Http.Tail{fuel, left, rest}}, b) : Http.Hear}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.Owed{+g, +owed}: + +f = Nat.add(g, String.length(b)) + +lt = Equal.sym(Bool, Nat.is_lt(owed, String.length(b)), False{}, + M1.eq.not.true(Nat.is_lt(owed, String.length(b)), Equal.sym(Bool, True{}, + Bool.not(Nat.is_lt(owed, String.length(b))), e))) + Equal.cong(Http.Skip, Http.Hear, + x => Http.Walking{Http.walk.data(f, x, s => Http.walk(f, s))}, + Http.Short{Nat.sub(owed, String.length(b))}, Http.octs.skip(b, owed), + Equal.sym(Http.Skip, Http.octs.skip(b, owed), Http.Short{Nat.sub(owed, String.length(b))}, + Laws.skip_short(b, owed, lt))) + +def Laws.read_count(h, b, e1, e2): + match b: + case SNil{}: + M1.eq.bit.absurd(&2, {Spec.wire.read.count(h, 0n) + == Some{Spec.wire.read.next(h, "")} : Maybe<&2, Spec.Read>}, e1) + case SCon{+c, +t}: + +L = String.length(SCon{c, t}) + +e = e2 + %e : {Spec.wire.read.counted(h, L, _) + == Some{Spec.wire.read.next(h, SCon{c, t})} : Maybe<&2, Spec.Read>} + Equal.cong(Http.Hear, Maybe<&2, Spec.Read>, x => Some{Spec.wire.read.with(x)}, + Http.hear.more.n(h, L), Http.hear.more(h, SCon{c, t}), + Laws.hear_count(h, SCon{c, t}, e)) + +def Laws.read_whole(method, a, text, e1, e2): + +H0 = Http.hear.of(method, a) + +J = a ++ text + %e1 : {Spec.wire.read.got(H0, text, _) == Spec.Heard{} : Spec.Read} + %Equal.sym(Http.Hear, Http.hear.more(H0, text), Http.hear.of(method, J), + L2.hear_more(method, a, text)) + : {Spec.wire.read.done(_, Http.hear.done(_)) == Spec.Heard{} : Spec.Read} + %Equal.sym(Bool, Http.hear.done(Http.hear.of(method, J)), Http.whole(method, J), + L2.hear_done(method, J)) + : {Spec.wire.read.done(Http.hear.of(method, J), _) == Spec.Heard{} : Spec.Read} + %e2 : {Spec.wire.read.done(Http.hear.of(method, J), _) == Spec.Heard{} : Spec.Read} + {==} + +def Laws.read_more(method, a, text, e1, e2): + +H0 = Http.hear.of(method, a) + +J = a ++ text + %e1 : {Spec.wire.read.got(H0, text, _) == Spec.Again{Http.hear.of(method, J)} + : Spec.Read} + %Equal.sym(Http.Hear, Http.hear.more(H0, text), Http.hear.of(method, J), + L2.hear_more(method, a, text)) + : {Spec.wire.read.done(_, Http.hear.done(_)) + == Spec.Again{Http.hear.of(method, J)} : Spec.Read} + %Equal.sym(Bool, Http.hear.done(Http.hear.of(method, J)), Http.whole(method, J), + L2.hear_done(method, J)) + : {Spec.wire.read.done(Http.hear.of(method, J), _) + == Spec.Again{Http.hear.of(method, J)} : Spec.Read} + %e2 : {Spec.wire.read.done(Http.hear.of(method, J), _) + == Spec.Again{Http.hear.of(method, J)} : Spec.Read} + {==} diff --git a/proofs/Reason200IsOk.bend b/proofs/Reason200IsOk.bend new file mode 100644 index 0000000..4d5c906 --- /dev/null +++ b/proofs/Reason200IsOk.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason200IsOk.bend +import Base +import ../laws/Reason200IsOk.bend as Laws + +def Laws.reason_200(): + {==} diff --git a/proofs/Reason201IsCreated.bend b/proofs/Reason201IsCreated.bend new file mode 100644 index 0000000..331276b --- /dev/null +++ b/proofs/Reason201IsCreated.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason201IsCreated.bend +import Base +import ../laws/Reason201IsCreated.bend as Laws + +def Laws.reason_201(): + {==} diff --git a/proofs/Reason204IsNoContent.bend b/proofs/Reason204IsNoContent.bend new file mode 100644 index 0000000..a79ab49 --- /dev/null +++ b/proofs/Reason204IsNoContent.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason204IsNoContent.bend +import Base +import ../laws/Reason204IsNoContent.bend as Laws + +def Laws.reason_204(): + {==} diff --git a/proofs/Reason400IsBadRequest.bend b/proofs/Reason400IsBadRequest.bend new file mode 100644 index 0000000..3fc3ab2 --- /dev/null +++ b/proofs/Reason400IsBadRequest.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason400IsBadRequest.bend +import Base +import ../laws/Reason400IsBadRequest.bend as Laws + +def Laws.reason_400(): + {==} diff --git a/proofs/Reason404IsNotFound.bend b/proofs/Reason404IsNotFound.bend new file mode 100644 index 0000000..83db435 --- /dev/null +++ b/proofs/Reason404IsNotFound.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason404IsNotFound.bend +import Base +import ../laws/Reason404IsNotFound.bend as Laws + +def Laws.reason_404(): + {==} diff --git a/proofs/Reason405IsMethodNotAllowed.bend b/proofs/Reason405IsMethodNotAllowed.bend new file mode 100644 index 0000000..aeaa19a --- /dev/null +++ b/proofs/Reason405IsMethodNotAllowed.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason405IsMethodNotAllowed.bend +import Base +import ../laws/Reason405IsMethodNotAllowed.bend as Laws + +def Laws.reason_405(): + {==} diff --git a/proofs/Reason500IsInternalServerError.bend b/proofs/Reason500IsInternalServerError.bend new file mode 100644 index 0000000..090b060 --- /dev/null +++ b/proofs/Reason500IsInternalServerError.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason500IsInternalServerError.bend +import Base +import ../laws/Reason500IsInternalServerError.bend as Laws + +def Laws.reason_500(): + {==} diff --git a/proofs/ReasonPhraseFromFinalHead.bend b/proofs/ReasonPhraseFromFinalHead.bend new file mode 100644 index 0000000..11406c7 --- /dev/null +++ b/proofs/ReasonPhraseFromFinalHead.bend @@ -0,0 +1,227 @@ +# proofs of laws/ReasonPhraseFromFinalHead.bend +import Base +import ../src/http.bend as Http +import ../laws/ReasonPhraseFromFinalHead.bend as Laws +import ../laws/defs/CharacterAbsent.bend as D1 +import ./lemmas/BaseEqualities.bend as M1 +import ./lemmas/DropNothing.bend as M2 + +# --- a response's reason phrase --- + +def Laws.phrase_to(_raw): + {==} + +def Laws.phrase_fuel(_c): + {==} + +def Laws.phrase_open(_f): + {==} + +def Laws.phrase_interim(f, before, after, e): + %e : {Http.phrase.to.step(String.lines(before), after, _, + cut => Http.phrase.to.go(f, cut)) + == Http.phrase.to.go(f, Http.divide(after, "\r\n\r\n")) : String} + {==} + +def Laws.phrase_final(f, before, after, e): + %e : {Http.phrase.to.step(String.lines(before), after, _, + cut => Http.phrase.to.go(f, cut)) + == Http.phrase.line(Http.first(String.lines(before))) : String} + {==} + +# LAW: a string that avoids a character does not start with it +law avoids.head: + for +h: Char + for +t: String + for +c: Char + for e: {True{} == D1.avoids(SCon{h, t}, c) : Bool} + {False{} == Char.is_eq(h, c) : Bool} + +def avoids.head(h, t, c, e): + Equal.sym(Bool, Char.is_eq(h, c), False{}, + M1.eq.not.true(Char.is_eq(h, c), + M1.eq.and.left(Bool.not(Char.is_eq(h, c)), D1.avoids(t, c), + Equal.sym(Bool, True{}, D1.avoids(SCon{h, t}, c), e)))) + +# LAW: a string that avoids a character has a tail that avoids it +law avoids.tail: + for +h: Char + for +t: String + for +c: Char + for e: {True{} == D1.avoids(SCon{h, t}, c) : Bool} + {True{} == D1.avoids(t, c) : Bool} + +def avoids.tail(h, t, c, e): + Equal.sym(Bool, D1.avoids(t, c), True{}, + M1.eq.and.right(Bool.not(Char.is_eq(h, c)), D1.avoids(t, c), + Equal.sym(Bool, True{}, D1.avoids(SCon{h, t}, c), e))) + +# LAW: one step of dividing at the first SP, past a character that is not one +law sp_cut.step: + for +h: Char + for +t: String + for +b: String + for f: {False{} == Char.is_eq(h, ' ') : Bool} + for ih: {Http.divide.go(String.append(t, SCon{' ', b}), " ", 1n) + == Http.Cut{t, b} : Http.Cut} + {Http.divide.go(String.append(SCon{h, t}, SCon{' ', b}), " ", 1n) + == Http.Cut{SCon{h, t}, b} : Http.Cut} + +def sp_cut.step(h, t, b, f, ih): + %f : {Http.divide.step( + String.starts_with.go.if("", String.append(t, SCon{' ', b}), _), h, + String.append(t, SCon{' ', b}), 1n, + _u => Http.divide.go(String.append(t, SCon{' ', b}), " ", 1n)) + == Http.Cut{SCon{h, t}, b} : Http.Cut} + Equal.cong(Http.Cut, Http.Cut, r => Http.divide.push(h, r), + Http.divide.go(String.append(t, SCon{' ', b}), " ", 1n), Http.Cut{t, b}, + ih) + +# LAW: a text that holds no SP before one divides there +law sp_cut: + for +a: String + for +b: String + for +e: {True{} == D1.avoids(a, ' ') : Bool} + {Http.divide.go(String.append(a, SCon{' ', b}), " ", 1n) + == Http.Cut{a, b} : Http.Cut} + +def sp_cut(a, b, e): + match a: + case SNil{}: + Equal.cong(String, Http.Cut, x => Http.Cut{"", x}, String.drop(b, 0n), + b, M2.eq.drop_zero(b)) + case SCon{+h, +t}: + sp_cut.step(h, t, b, avoids.head(h, t, ' ', e), + sp_cut(t, b, avoids.tail(h, t, ' ', e))) + +# LAW: one step of dividing a text with no SP +law sp_none.step: + for +h: Char + for +t: String + for f: {False{} == Char.is_eq(h, ' ') : Bool} + for ih: {Http.divide.go(t, " ", 1n) == Http.NoCut{} : Http.Cut} + {Http.divide.go(SCon{h, t}, " ", 1n) == Http.NoCut{} : Http.Cut} + +def sp_none.step(h, t, f, ih): + %f : {Http.divide.step(String.starts_with.go.if("", t, _), h, t, 1n, + _u => Http.divide.go(t, " ", 1n)) == Http.NoCut{} : Http.Cut} + Equal.cong(Http.Cut, Http.Cut, r => Http.divide.push(h, r), + Http.divide.go(t, " ", 1n), Http.NoCut{}, ih) + +# LAW: a text with no SP does not divide at one +law sp_none: + for +a: String + for +e: {True{} == D1.avoids(a, ' ') : Bool} + {Http.divide.go(a, " ", 1n) == Http.NoCut{} : Http.Cut} + +def sp_none(a, e): + match a: + case SNil{}: + {==} + case SCon{+h, +t}: + sp_none.step(h, t, avoids.head(h, t, ' ', e), + sp_none(t, avoids.tail(h, t, ' ', e))) + +# LAW: one step of the chop, past a character that is not CR +law chop.step: + for +h: Char + for +t: String + for +x: String + for f: {False{} == Char.is_eq(h, '\r') : Bool} + for ih: {Http.phrase.chop(x) == t : String} + {Http.phrase.chop(SCon{h, x}) == SCon{h, t} : String} + +def chop.step(h, t, x, f, ih): + %f : {Http.phrase.chop.at(_, h, x, _u => Http.phrase.chop(x)) + == SCon{h, t} : String} + Equal.cong(String, String, y => SCon{h, y}, Http.phrase.chop(x), t, ih) + +# LAW: a text with no CR is its own chop +law chop.keep: + for +r: String + for +e: {True{} == D1.avoids(r, '\r') : Bool} + {Http.phrase.chop(r) == r : String} + +def chop.keep(r, e): + match r: + case SNil{}: + {==} + case SCon{+h, +t}: + chop.step(h, t, t, avoids.head(h, t, '\r', e), + chop.keep(t, avoids.tail(h, t, '\r', e))) + +# LAW: the chop of a text with no CR and then a CR is the text +law chop.cr: + for +r: String + for +e: {True{} == D1.avoids(r, '\r') : Bool} + {Http.phrase.chop(String.append(r, "\r")) == r : String} + +def chop.cr(r, e): + match r: + case SNil{}: + {==} + case SCon{+h, +t}: + chop.step(h, t, String.append(t, "\r"), avoids.head(h, t, '\r', e), + chop.cr(t, avoids.tail(h, t, '\r', e))) + +def Laws.phrase_line(v, c, r, e1, e2, e3): + Equal.trans(String, + Http.phrase.line(String.append(v, SCon{' ', String.append(c, SCon{' ', r})})), + Http.phrase.code(Http.Cut{c, r}), + r, + Equal.trans(String, + Http.phrase.line(String.append(v, SCon{' ', String.append(c, SCon{' ', r})})), + Http.phrase.after(Http.Cut{v, String.append(c, SCon{' ', r})}), + Http.phrase.code(Http.Cut{c, r}), + Equal.cong(Http.Cut, String, k => Http.phrase.after(k), + Http.divide.go(String.append(v, SCon{' ', String.append(c, SCon{' ', r})}), " ", 1n), + Http.Cut{v, String.append(c, SCon{' ', r})}, + sp_cut(v, String.append(c, SCon{' ', r}), e1)), + Equal.cong(Http.Cut, String, k => Http.phrase.code(k), + Http.divide.go(String.append(c, SCon{' ', r}), " ", 1n), + Http.Cut{c, r}, sp_cut(c, r, e2))), + chop.keep(r, e3)) + +def Laws.phrase_line_cr(v, c, r, e1, e2, e3): + Equal.trans(String, + Http.phrase.line(String.append(v, SCon{' ', String.append(c, + SCon{' ', String.append(r, "\r")})})), + Http.phrase.code(Http.Cut{c, String.append(r, "\r")}), + r, + Equal.trans(String, + Http.phrase.line(String.append(v, SCon{' ', String.append(c, + SCon{' ', String.append(r, "\r")})})), + Http.phrase.after(Http.Cut{v, String.append(c, + SCon{' ', String.append(r, "\r")})}), + Http.phrase.code(Http.Cut{c, String.append(r, "\r")}), + Equal.cong(Http.Cut, String, k => Http.phrase.after(k), + Http.divide.go(String.append(v, SCon{' ', String.append(c, + SCon{' ', String.append(r, "\r")})}), " ", 1n), + Http.Cut{v, String.append(c, SCon{' ', String.append(r, "\r")})}, + sp_cut(v, String.append(c, SCon{' ', String.append(r, "\r")}), e1)), + Equal.cong(Http.Cut, String, k => Http.phrase.code(k), + Http.divide.go(String.append(c, SCon{' ', String.append(r, "\r")}), + " ", 1n), + Http.Cut{c, String.append(r, "\r")}, + sp_cut(c, String.append(r, "\r"), e2))), + chop.cr(r, e3)) + +def Laws.phrase_line_one(v, c, e1, e2): + Equal.trans(String, + Http.phrase.line(String.append(v, SCon{' ', c})), + Http.phrase.after(Http.Cut{v, c}), + "", + Equal.cong(Http.Cut, String, k => Http.phrase.after(k), + Http.divide.go(String.append(v, SCon{' ', c}), " ", 1n), + Http.Cut{v, c}, sp_cut(v, c, e1)), + Equal.cong(Http.Cut, String, k => Http.phrase.code(k), + Http.divide.go(c, " ", 1n), Http.NoCut{}, sp_none(c, e2))) + +def Laws.phrase_line_none(v, e): + Equal.cong(Http.Cut, String, k => Http.phrase.after(k), + Http.divide.go(v, " ", 1n), Http.NoCut{}, sp_none(v, e)) + +def Laws.phrase_heard(text): + dz = Equal.sym(String, String.drop(text, 0n), text, M2.eq.drop_zero(text)) + %dz : {Http.phrase.to(_) == Http.phrase.to(text) : String} + {==} diff --git a/proofs/ReasonPhrasesFollowRfc.bend b/proofs/ReasonPhrasesFollowRfc.bend new file mode 100644 index 0000000..81dbaa8 --- /dev/null +++ b/proofs/ReasonPhrasesFollowRfc.bend @@ -0,0 +1,11 @@ +# proofs of laws/ReasonPhrasesFollowRfc.bend +import Base +import ../laws/ReasonPhrasesFollowRfc.bend as Laws + +def Laws.respond_writes_reason_and_close(_status, _hs, _body): + {==} + +# --- reason-phrases --- + +def Laws.reason_phrases(_code): + {==} diff --git a/proofs/RedirectsFollowLocation.bend b/proofs/RedirectsFollowLocation.bend new file mode 100644 index 0000000..1033367 --- /dev/null +++ b/proofs/RedirectsFollowLocation.bend @@ -0,0 +1,37 @@ +# proofs of laws/RedirectsFollowLocation.bend +import Base +import ../src/redirect.bend as Redirect +import ../laws/RedirectsFollowLocation.bend as Laws + +# --- redirects --- + +def Laws.redir_go(_status, e, _n, url, loc, next, et): + %e : {Redirect.redirect.on(url, loc, _) == Redirect.Go{next} : Redirect.Hop} + %et : {Redirect.redirect.hop(_) == Redirect.Go{next} : Redirect.Hop} + {==} + +# a response whose Location has no target stops, redirect or not +law redir_no_target.on: + for url: String + for +loc: String + for moved: Bool + for et: {None{} == Redirect.redirect.target(url, loc) : Maybe<&2, String>} + {Redirect.redirect.on(url, loc, moved) == Redirect.Stop{} : Redirect.Hop} + +def redir_no_target.on(_url, _loc, moved, et): + match moved: + case True{}: + %et : {Redirect.redirect.hop(_) == Redirect.Stop{} : Redirect.Hop} + {==} + case False{}: + {==} + +def Laws.redir_no_target(status, left, url, loc, et): + match left: + case 0n: + {==} + case 1n+p: + redir_no_target.on(url, loc, Redirect.redirect.is(status), et) + +def Laws.redir_plan_location(_left, _url, _status, _hs): + {==} diff --git a/proofs/RelativeLocationsMerge.bend b/proofs/RelativeLocationsMerge.bend new file mode 100644 index 0000000..8eb54e5 --- /dev/null +++ b/proofs/RelativeLocationsMerge.bend @@ -0,0 +1,55 @@ +# proofs of laws/RelativeLocationsMerge.bend +import Base +import ../src/redirect.bend as Redirect +import ../laws/RelativeLocationsMerge.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.against_query(_scheme, _host, _port, _path, _loc): + {==} + +def Laws.against_rel(_scheme, _host, _port, _path, _loc): + {==} + +def Laws.sort_query(loc, e1, e2, e3, e4): + %e1 : {Redirect.redirect.sort.of(_, String.starts_with(loc, "/"), + Redirect.redirect.stays(loc), String.starts_with(loc, "?")) == Redirect.Query{} : Redirect.Sort} + %e2 : {Redirect.redirect.sort.abs(_, Redirect.redirect.stays(loc), + String.starts_with(loc, "?")) == Redirect.Query{} : Redirect.Sort} + %e3 : {Redirect.redirect.sort.stay(_, String.starts_with(loc, "?")) == Redirect.Query{} : Redirect.Sort} + %e4 : {Redirect.redirect.sort.rel(_) == Redirect.Query{} : Redirect.Sort} + {==} + +def Laws.sort_rel(loc, e1, e2, e3, e4): + %e1 : {Redirect.redirect.sort.of(_, String.starts_with(loc, "/"), + Redirect.redirect.stays(loc), String.starts_with(loc, "?")) == Redirect.Rel{} : Redirect.Sort} + %e2 : {Redirect.redirect.sort.abs(_, Redirect.redirect.stays(loc), + String.starts_with(loc, "?")) == Redirect.Rel{} : Redirect.Sort} + %e3 : {Redirect.redirect.sort.stay(_, String.starts_with(loc, "?")) == Redirect.Rel{} : Redirect.Sort} + %e4 : {Redirect.redirect.sort.rel(_) == Redirect.Rel{} : Redirect.Sort} + {==} + +def Laws.rel_of(_base, _path, _query): + {==} + +def Laws.base_path(_path): + {==} + +def Laws.hier_first(_h, _t): + {==} + +def Laws.init_nil(ss, e): + match ss: + case []: + {==} + case h <> t: + M1.eq.bit.absurd(&2, {Redirect.redirect.init(h <> t) == [] : List<&2, String>}, + Equal.sym(Bool, True{}, False{}, e)) + +def Laws.init_cons(_s, _t): + {==} + +def Laws.init_last(_s): + {==} + +def Laws.init_keep(_s, _u, _r): + {==} diff --git a/proofs/RelativeTargetRejected.bend b/proofs/RelativeTargetRejected.bend new file mode 100644 index 0000000..846ae2c --- /dev/null +++ b/proofs/RelativeTargetRejected.bend @@ -0,0 +1,6 @@ +# proofs of laws/RelativeTargetRejected.bend +import Base +import ../laws/RelativeTargetRejected.bend as Laws + +def Laws.ask_rejects_relative_target(): + {==} diff --git a/proofs/ReplyHeadFramesBody.bend b/proofs/ReplyHeadFramesBody.bend new file mode 100644 index 0000000..a8c6a43 --- /dev/null +++ b/proofs/ReplyHeadFramesBody.bend @@ -0,0 +1,12 @@ +# proofs of laws/ReplyHeadFramesBody.bend +import Base +import ../laws/ReplyHeadFramesBody.bend as Laws + +def Laws.respond_head(_status, _hs, _n): + {==} + +def Laws.respond_text_is(_status, _hs, _body): + {==} + +def Laws.respond_head_onto(_status, _hs, _n, _rest): + {==} diff --git a/proofs/RequestBodyOfContentLengthKept.bend b/proofs/RequestBodyOfContentLengthKept.bend new file mode 100644 index 0000000..26b6393 --- /dev/null +++ b/proofs/RequestBodyOfContentLengthKept.bend @@ -0,0 +1,18 @@ +# proofs of laws/RequestBodyOfContentLengthKept.bend +import Base +import ../src/http.bend as Http +import ../laws/RequestBodyOfContentLengthKept.bend as Laws +import ./lemmas/NatOrder.bend as M1 + +def Laws.ask_clen_exact(method, target, hs, body): + Equal.trans(Http.Request, + Http.ask.clen(method, target, hs, Http.octs.len(body), body), + Http.ask.short(method, target, hs, Http.octs.len(body), body, False{}), + Http.Request{method, target, hs, body}, + Equal.cong(Bool, Http.Request, + b => Http.ask.short(method, target, hs, Http.octs.len(body), body, b), + Nat.is_lt(Http.octs.len(body), Http.octs.len(body)), False{}, + M1.lt_self(Http.octs.len(body))), + Equal.cong(String, Http.Request, + x => Http.Request{method, target, hs, x}, + Http.octs.take(body, Http.octs.len(body)), body, M1.take_all(body))) diff --git a/proofs/RequestDisagreeingContentLengthsRejected.bend b/proofs/RequestDisagreeingContentLengthsRejected.bend new file mode 100644 index 0000000..210b657 --- /dev/null +++ b/proofs/RequestDisagreeingContentLengthsRejected.bend @@ -0,0 +1,6 @@ +# proofs of laws/RequestDisagreeingContentLengthsRejected.bend +import Base +import ../laws/RequestDisagreeingContentLengthsRejected.bend as Laws + +def Laws.ask_rejects_disagreeing_cl(): + {==} diff --git a/proofs/RequestEqualContentLengthsFrameOneBody.bend b/proofs/RequestEqualContentLengthsFrameOneBody.bend new file mode 100644 index 0000000..ba9ec5f --- /dev/null +++ b/proofs/RequestEqualContentLengthsFrameOneBody.bend @@ -0,0 +1,6 @@ +# proofs of laws/RequestEqualContentLengthsFrameOneBody.bend +import Base +import ../laws/RequestEqualContentLengthsFrameOneBody.bend as Laws + +def Laws.ask_equal_content_lengths(): + {==} diff --git a/proofs/RequestHeaderFieldsOrdered.bend b/proofs/RequestHeaderFieldsOrdered.bend new file mode 100644 index 0000000..8733ce9 --- /dev/null +++ b/proofs/RequestHeaderFieldsOrdered.bend @@ -0,0 +1,664 @@ +# proofs of laws/RequestHeaderFieldsOrdered.bend +import Base +import ../src/http.bend as Http +import ../laws/RequestHeaderFieldsOrdered.bend as Laws +import ./lemmas/HeaderFieldSlots.bend as M1 + +def Laws.field_host(_name, e): + %e : {Http.field.of(_) == Http.FHost{} : Http.Field} + {==} + +def Laws.field_agent(_name, e): + %e : {Http.field.of(_) == Http.FAgent{} : Http.Field} + {==} + +def Laws.field_accept(_name, e): + %e : {Http.field.of(_) == Http.FAccept{} : Http.Field} + {==} + +def Laws.field_conn(_name, e): + %e : {Http.field.of(_) == Http.FConn{} : Http.Field} + {==} + +def Laws.field_clen(_name, e): + %e : {Http.field.of(_) == Http.FClen{} : Http.Field} + {==} + +def Laws.field_other(name, eh, ea, ec, en, el): + %eh : {Http.field.of.host(_, String.to_lower(name)) == Http.FOther{} + : Http.Field} + %ea : {Http.field.of.agent(_, String.to_lower(name)) == Http.FOther{} + : Http.Field} + %ec : {Http.field.of.accept(_, String.to_lower(name)) == Http.FOther{} + : Http.Field} + %en : {Http.field.of.conn(_, String.to_lower(name)) == Http.FOther{} + : Http.Field} + %el : {Http.field.of.clen(_) == Http.FOther{} : Http.Field} + {==} + +def Laws.request_text(_method, _host, _path, _hs, _body): + {==} + +# a list's tail after an empty append is the list +def hd.app_nil(+xs: List<&2, + Http.Header>) -> {List.append(&2, Http.Header, xs, []) == xs : List<&2, + Http.Header>}: + match xs: + case []: + {==} + case h <> t: + Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, x => h <> x, + List.append(&2, Http.Header, t, []), t, + hd.app_nil(t)) + +# one step of idem +def hd.idem.put(+f: Http.Field, +k: Bool, +h: Http.Header, +t: List<&2, + Http.Header>, + +ek: {k == Http.field.same(f, Http.header.field(h)) : Bool}, + +ih: {Http.fields.of(f, Http.fields.of(f, t)) + == Http.fields.of(f, t) : List<&2, Http.Header>}) + -> {Http.fields.of(f, Http.fields.put(k, h, Http.fields.of(f, t))) + == Http.fields.put(k, h, Http.fields.of(f, t)) : List<&2, Http.Header>}: + match k: + case True{}: + %ek : {Http.fields.put(_, h, Http.fields.of(f, Http.fields.of(f, t))) + == h <> Http.fields.of(f, t) : List<&2, Http.Header>} + Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, x => h <> x, + Http.fields.of(f, Http.fields.of(f, t)), Http.fields.of(f, t), + ih) + case False{}: + ih + +# the fields of a name among the fields of that name are all of them +def hd.idem(+f: Http.Field, +hs: List<&2, Http.Header>) + -> {Http.fields.of(f, Http.fields.of(f, hs)) + == Http.fields.of(f, hs) : List<&2, Http.Header>}: + match hs: + case []: + {==} + case h <> t: + hd.idem.put(f, Http.field.same(f, Http.header.field(h)), h, t, {==}, + hd.idem(f, t)) + +# a slot holds itself when the caller's list and the default are all of a +# name +def hd.or_keep(+f: Http.Field, +given: List<&2, Http.Header>, +d: List<&2, + Http.Header>, + +eg: {Http.fields.of(f, given) == given : List<&2, Http.Header>}, + +ed: {Http.fields.of(f, d) == d : List<&2, Http.Header>}) + -> {Http.fields.or(given, d) + == Http.fields.of(f, Http.fields.or(given, d)) : List<&2, Http.Header>}: + match given: + case []: + Equal.sym(List<&2, Http.Header>, Http.fields.of(f, d), d, ed) + case h <> t: + Equal.sym(List<&2, Http.Header>, Http.fields.of(f, h <> t), h <> t, eg) + +# a computed Content-Length is no other field +def hd.clen_gone(+f: Http.Field, + +efc: {False{} == Http.field.same(f, Http.FClen{}) : Bool}, +n: Nat, + +e: Bool) -> {[] == Http.fields.of(f, Http.clen.field(n, e)) : List<&2, + Http.Header>}: + match e: + case True{}: + {==} + case False{}: + %efc : {[] + == Http.fields.put(_, Http.H{"Content-Length", Nat.show(n)}, []) + : List<&2, Http.Header>} + {==} + +def Laws.fields_host_caller(host, hs, body, h, t, e): + Equal.trans(List<&2, Http.Header>, + Http.fields.of(Http.FHost{}, Http.request.fields(host, hs, body)), + List.append(&2, Http.Header, h <> t, []), h <> t, + M1.hd.of_fields(Http.FHost{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), + h <> t, [], [], [], [], + Equal.trans(List<&2, Http.Header>, h <> t, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}]), + Http.fields.of(Http.FHost{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}])), + Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, + x => Http.fields.or(x, [Http.H{"Host", host}]), h <> t, + Http.fields.of(Http.FHost{}, hs), + Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FHost{}, hs), + h <> t, e)), + hd.or_keep(Http.FHost{}, Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}], hd.idem(Http.FHost{}, hs), {==})), + M1.hd.or_gone(Http.FHost{}, Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}], + M1.hd.disj(Http.FHost{}, Http.FAgent{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FHost{}, Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}], + M1.hd.disj(Http.FHost{}, Http.FAccept{}, {==}, hs), {==}), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FHost{}, Http.fields.of(Http.FOther{}, hs)), [], + M1.hd.disj(Http.FHost{}, Http.FOther{}, {==}, hs)), + hd.clen_gone(Http.FHost{}, {==}, Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), + hd.app_nil(h <> t)) + +def Laws.fields_host_dflt(host, hs, body, e): + M1.hd.of_fields(Http.FHost{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), + [Http.H{"Host", host}], [], [], [], [], + Equal.trans(List<&2, Http.Header>, [Http.H{"Host", host}], + Http.fields.or(Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}]), + Http.fields.of(Http.FHost{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}])), + Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, + x => Http.fields.or(x, [Http.H{"Host", host}]), [], + Http.fields.of(Http.FHost{}, hs), + Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FHost{}, hs), + [], e)), + hd.or_keep(Http.FHost{}, Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}], hd.idem(Http.FHost{}, hs), {==})), + M1.hd.or_gone(Http.FHost{}, Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}], + M1.hd.disj(Http.FHost{}, Http.FAgent{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FHost{}, Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}], + M1.hd.disj(Http.FHost{}, Http.FAccept{}, {==}, hs), {==}), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FHost{}, Http.fields.of(Http.FOther{}, hs)), [], + M1.hd.disj(Http.FHost{}, Http.FOther{}, {==}, hs)), + hd.clen_gone(Http.FHost{}, {==}, Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))) + +def Laws.fields_agent_caller(host, hs, body, h, t, e): + Equal.trans(List<&2, Http.Header>, + Http.fields.of(Http.FAgent{}, Http.request.fields(host, hs, body)), + List.append(&2, Http.Header, h <> t, []), h <> t, + M1.hd.of_fields(Http.FAgent{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), + [], h <> t, [], [], [], + M1.hd.or_gone(Http.FAgent{}, Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}], + M1.hd.disj(Http.FAgent{}, Http.FHost{}, {==}, hs), {==}), + Equal.trans(List<&2, Http.Header>, h <> t, + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.of(Http.FAgent{}, + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}])), + Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, + x => Http.fields.or(x, [Http.H{"User-Agent", "ezhttp"}]), h <> t, + Http.fields.of(Http.FAgent{}, hs), + Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FAgent{}, hs), + h <> t, e)), + hd.or_keep(Http.FAgent{}, Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}], hd.idem(Http.FAgent{}, hs), {==})), + M1.hd.or_gone(Http.FAgent{}, Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}], + M1.hd.disj(Http.FAgent{}, Http.FAccept{}, {==}, hs), {==}), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FAgent{}, Http.fields.of(Http.FOther{}, hs)), [], + M1.hd.disj(Http.FAgent{}, Http.FOther{}, {==}, hs)), + hd.clen_gone(Http.FAgent{}, {==}, Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), + hd.app_nil(h <> t)) + +def Laws.fields_agent_dflt(host, hs, body, e): + M1.hd.of_fields(Http.FAgent{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), + [], [Http.H{"User-Agent", "ezhttp"}], [], [], [], + M1.hd.or_gone(Http.FAgent{}, Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}], + M1.hd.disj(Http.FAgent{}, Http.FHost{}, {==}, hs), {==}), + Equal.trans(List<&2, Http.Header>, [Http.H{"User-Agent", "ezhttp"}], + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.of(Http.FAgent{}, + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}])), + Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, + x => Http.fields.or(x, [Http.H{"User-Agent", "ezhttp"}]), [], + Http.fields.of(Http.FAgent{}, hs), + Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FAgent{}, hs), + [], e)), + hd.or_keep(Http.FAgent{}, Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}], hd.idem(Http.FAgent{}, hs), {==})), + M1.hd.or_gone(Http.FAgent{}, Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}], + M1.hd.disj(Http.FAgent{}, Http.FAccept{}, {==}, hs), {==}), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FAgent{}, Http.fields.of(Http.FOther{}, hs)), [], + M1.hd.disj(Http.FAgent{}, Http.FOther{}, {==}, hs)), + hd.clen_gone(Http.FAgent{}, {==}, Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))) + +def Laws.fields_accept_caller(host, hs, body, h, t, e): + Equal.trans(List<&2, Http.Header>, + Http.fields.of(Http.FAccept{}, Http.request.fields(host, hs, body)), + List.append(&2, Http.Header, h <> t, []), h <> t, + M1.hd.of_fields(Http.FAccept{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), + [], [], h <> t, [], [], + M1.hd.or_gone(Http.FAccept{}, Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}], + M1.hd.disj(Http.FAccept{}, Http.FHost{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FAccept{}, Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}], + M1.hd.disj(Http.FAccept{}, Http.FAgent{}, {==}, hs), {==}), + Equal.trans(List<&2, Http.Header>, h <> t, + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FAccept{}, + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}])), + Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, + x => Http.fields.or(x, [Http.H{"Accept", "*/*"}]), h <> t, + Http.fields.of(Http.FAccept{}, hs), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FAccept{}, hs), h <> t, e)), + hd.or_keep(Http.FAccept{}, Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}], hd.idem(Http.FAccept{}, hs), {==})), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FAccept{}, Http.fields.of(Http.FOther{}, hs)), [], + M1.hd.disj(Http.FAccept{}, Http.FOther{}, {==}, hs)), + hd.clen_gone(Http.FAccept{}, {==}, Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), + hd.app_nil(h <> t)) + +def Laws.fields_accept_dflt(host, hs, body, e): + M1.hd.of_fields(Http.FAccept{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), + [], [], [Http.H{"Accept", "*/*"}], [], [], + M1.hd.or_gone(Http.FAccept{}, Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}], + M1.hd.disj(Http.FAccept{}, Http.FHost{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FAccept{}, Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}], + M1.hd.disj(Http.FAccept{}, Http.FAgent{}, {==}, hs), {==}), + Equal.trans(List<&2, Http.Header>, [Http.H{"Accept", "*/*"}], + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FAccept{}, + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}])), + Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, + x => Http.fields.or(x, [Http.H{"Accept", "*/*"}]), [], + Http.fields.of(Http.FAccept{}, hs), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FAccept{}, hs), [], e)), + hd.or_keep(Http.FAccept{}, Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}], hd.idem(Http.FAccept{}, hs), {==})), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FAccept{}, Http.fields.of(Http.FOther{}, hs)), [], + M1.hd.disj(Http.FAccept{}, Http.FOther{}, {==}, hs)), + hd.clen_gone(Http.FAccept{}, {==}, Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))) + +def Laws.fields_conn(host, hs, body): + M1.hd.of_fields(Http.FConn{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), + [], [], [], [], [], + M1.hd.or_gone(Http.FConn{}, Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}], + M1.hd.disj(Http.FConn{}, Http.FHost{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FConn{}, Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}], + M1.hd.disj(Http.FConn{}, Http.FAgent{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FConn{}, Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}], + M1.hd.disj(Http.FConn{}, Http.FAccept{}, {==}, hs), {==}), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FConn{}, Http.fields.of(Http.FOther{}, hs)), [], + M1.hd.disj(Http.FConn{}, Http.FOther{}, {==}, hs)), + hd.clen_gone(Http.FConn{}, {==}, Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))) + +def Laws.fields_clen(host, hs, body): + M1.hd.of_fields(Http.FClen{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), + [], [], [], [], + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)), + M1.hd.or_gone(Http.FClen{}, Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}], + M1.hd.disj(Http.FClen{}, Http.FHost{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FClen{}, Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}], + M1.hd.disj(Http.FClen{}, Http.FAgent{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FClen{}, Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}], + M1.hd.disj(Http.FClen{}, Http.FAccept{}, {==}, hs), {==}), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FClen{}, Http.fields.of(Http.FOther{}, hs)), [], + M1.hd.disj(Http.FClen{}, Http.FOther{}, {==}, hs)), + M1.hd.clen_self(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n))) + +def Laws.fields_other(host, hs, body): + Equal.trans(List<&2, Http.Header>, + Http.fields.of(Http.FOther{}, Http.request.fields(host, hs, body)), + List.append(&2, Http.Header, Http.fields.of(Http.FOther{}, hs), []), + Http.fields.of(Http.FOther{}, hs), + M1.hd.of_fields(Http.FOther{}, + Http.fields.or(Http.fields.of(Http.FHost{}, hs), [Http.H{"Host", host}]), + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), + Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)), + [], [], [], Http.fields.of(Http.FOther{}, hs), [], + M1.hd.or_gone(Http.FOther{}, Http.fields.of(Http.FHost{}, hs), + [Http.H{"Host", host}], + M1.hd.disj(Http.FOther{}, Http.FHost{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FOther{}, Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}], + M1.hd.disj(Http.FOther{}, Http.FAgent{}, {==}, hs), {==}), + M1.hd.or_gone(Http.FOther{}, Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}], + M1.hd.disj(Http.FOther{}, Http.FAccept{}, {==}, hs), {==}), + Equal.sym(List<&2, Http.Header>, + Http.fields.of(Http.FOther{}, Http.fields.of(Http.FOther{}, hs)), + Http.fields.of(Http.FOther{}, hs), + hd.idem(Http.FOther{}, hs)), + hd.clen_gone(Http.FOther{}, {==}, Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), + hd.app_nil(Http.fields.of(Http.FOther{}, hs))) + +# string append is associative +def eq.app_assoc(+a: String, +b: String, +c: String) + -> {String.append(String.append(a, b), c) + == String.append(a, String.append(b, c)) : String}: + match a: + case SNil{}: + {==} + case SCon{h, t}: + Equal.cong(String, String, x => SCon{h, x}, + String.append(String.append(t, b), c), + String.append(t, String.append(b, c)), eq.app_assoc(t, b, c)) + +# the lines of an append are the lines of each part +def hd.lines_app(+a: List<&2, Http.Header>, +b: List<&2, Http.Header>) + -> {Http.headers.lines(List.append(&2, Http.Header, a, b)) + == String.append(Http.headers.lines(a), Http.headers.lines(b)) : String}: + match a: + case []: + {==} + case +h <> +t: + Equal.trans(String, + String.append(Http.header.line(h), + Http.headers.lines(List.append(&2, Http.Header, t, b))), + String.append(Http.header.line(h), + String.append(Http.headers.lines(t), Http.headers.lines(b))), + String.append(String.append(Http.header.line(h), + Http.headers.lines(t)), Http.headers.lines(b)), + Equal.cong(String, String, x => String.append(Http.header.line(h), x), + Http.headers.lines(List.append(&2, Http.Header, t, b)), + String.append(Http.headers.lines(t), Http.headers.lines(b)), + hd.lines_app(t, b)), + Equal.sym(String, + String.append(String.append(Http.header.line(h), + Http.headers.lines(t)), Http.headers.lines(b)), + String.append(Http.header.line(h), + String.append(Http.headers.lines(t), Http.headers.lines(b))), + eq.app_assoc(Http.header.line(h), Http.headers.lines(t), + Http.headers.lines(b)))) + +# with none of Host, User-Agent, Accept from the caller and only other +# fields, the request's fields are the defaults, the caller's, Content-Length +def hd.plain_fields(+host: String, +hs: List<&2, Http.Header>, +body: String, + +eh: {Http.fields.of(Http.FHost{}, hs) == [] : List<&2, Http.Header>}, + +ea: {Http.fields.of(Http.FAgent{}, hs) == [] : List<&2, Http.Header>}, + +ec: {Http.fields.of(Http.FAccept{}, hs) == [] : List<&2, Http.Header>}, + +eo: {Http.fields.of(Http.FOther{}, hs) == hs : List<&2, Http.Header>}) + -> {Http.request.fields(host, hs, body) == Http.H{"Host", host} + <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} + <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))) : List<&2, Http.Header>}: + %Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FHost{}, hs), [], eh) + : {List.append(&2, Http.Header, Http.fields.or(_, [Http.H{"Host", host}]), + List.append(&2, Http.Header, + Http.fields.or(Http.fields.of(Http.FAgent{}, hs), + [Http.H{"User-Agent", "ezhttp"}]), + List.append(&2, Http.Header, + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))))) == Http.H{"Host", host} + <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} + <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))) : List<&2, Http.Header>} + %Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FAgent{}, hs), [], ea) + : {List.append(&2, Http.Header, Http.fields.or([], [Http.H{"Host", host}]), + List.append(&2, Http.Header, + Http.fields.or(_, [Http.H{"User-Agent", "ezhttp"}]), + List.append(&2, Http.Header, + Http.fields.or(Http.fields.of(Http.FAccept{}, hs), + [Http.H{"Accept", "*/*"}]), Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))))) == Http.H{"Host", host} + <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} + <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))) : List<&2, Http.Header>} + %Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FAccept{}, hs), [], ec) + : {List.append(&2, Http.Header, Http.fields.or([], [Http.H{"Host", host}]), + List.append(&2, Http.Header, + Http.fields.or([], [Http.H{"User-Agent", "ezhttp"}]), + List.append(&2, Http.Header, + Http.fields.or(_, [Http.H{"Accept", "*/*"}]), + Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, Http.fields.of(Http.FOther{}, hs), + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))))) == Http.H{"Host", host} + <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} + <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))) : List<&2, Http.Header>} + %Equal.sym(List<&2, Http.Header>, Http.fields.of(Http.FOther{}, hs), hs, eo) + : {List.append(&2, Http.Header, Http.fields.or([], [Http.H{"Host", host}]), + List.append(&2, Http.Header, + Http.fields.or([], [Http.H{"User-Agent", "ezhttp"}]), + List.append(&2, Http.Header, + Http.fields.or([], [Http.H{"Accept", "*/*"}]), + Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, _, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))))) == Http.H{"Host", host} + <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} + <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))) : List<&2, Http.Header>} + {==} + +# the plain request's text after `Host: ` +def hd.plain_tail(+host: String, +hs: List<&2, Http.Header>, +body: String) + -> {String.append(String.append(String.append(host, "\r\n"), + Http.headers.lines(Http.H{"User-Agent", "ezhttp"} + <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))))), "\r\n" ++ body) + == String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" + ++ "Accept: */*\r\n" ++ "Connection: close\r\n" + ++ String.append(Http.headers.lines(hs), + String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))) : String}: + Equal.trans(String, + String.append(String.append(String.append(host, "\r\n"), + Http.headers.lines(Http.H{"User-Agent", "ezhttp"} + <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))))), "\r\n" ++ body), + String.append(String.append(host, "\r\n"), + String.append(Http.headers.lines(Http.H{"User-Agent", "ezhttp"} + <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body)), + String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" ++ "Accept: */*\r\n" + ++ "Connection: close\r\n" + ++ String.append(Http.headers.lines(hs), + String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))), + eq.app_assoc(String.append(host, "\r\n"), + Http.headers.lines(Http.H{"User-Agent", "ezhttp"} + <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body), + Equal.trans(String, + String.append(String.append(host, "\r\n"), + String.append(Http.headers.lines(Http.H{"User-Agent", "ezhttp"} + <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body)), + String.append(host, + String.append("\r\n", + String.append(Http.headers.lines(Http.H{"User-Agent", "ezhttp"} + <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body))), + String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" + ++ "Accept: */*\r\n" ++ "Connection: close\r\n" + ++ String.append(Http.headers.lines(hs), + String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))), + eq.app_assoc(host, "\r\n", + String.append(Http.headers.lines(Http.H{"User-Agent", "ezhttp"} + <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body)), + Equal.cong(String, String, + x => String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" + ++ "Accept: */*\r\n" ++ "Connection: close\r\n" ++ x), + String.append(Http.headers.lines(List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body), + String.append(Http.headers.lines(hs), + String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body)), + Equal.trans(String, + String.append(Http.headers.lines(List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body), + String.append(String.append(Http.headers.lines(hs), + Http.headers.lines(Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))), "\r\n" ++ body), + String.append(Http.headers.lines(hs), + String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body)), + Equal.cong(String, String, y => String.append(y, "\r\n" ++ body), + Http.headers.lines(List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))), + String.append(Http.headers.lines(hs), + Http.headers.lines(Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))), + hd.lines_app(hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))), + eq.app_assoc(Http.headers.lines(hs), + Http.headers.lines(Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))))) + +def Laws.request_plain(method, host, path, hs, body, eh, ea, ec, eo): + Equal.trans(String, + Http.request(method, host, path, hs, body), + method ++ " " ++ path ++ " HTTP/1.1\r\n" + ++ Http.headers.lines(Http.H{"Host", host} + <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} + <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n)))) ++ "\r\n" ++ body, + method ++ " " ++ path ++ " HTTP/1.1\r\n" ++ "Host: " + ++ String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" + ++ "Accept: */*\r\n" ++ "Connection: close\r\n" + ++ String.append(Http.headers.lines(hs), + String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))), + Equal.cong(List<&2, Http.Header>, String, + z => method ++ " " ++ path ++ " HTTP/1.1\r\n" ++ Http.headers.lines(z) + ++ "\r\n" ++ body, + Http.request.fields(host, hs, body), Http.H{"Host", host} + <> Http.H{"User-Agent", "ezhttp"} <> Http.H{"Accept", "*/*"} + <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), + hd.plain_fields(host, hs, body, eh, ea, ec, eo)), + Equal.cong(String, String, + x => method ++ " " ++ path ++ " HTTP/1.1\r\n" ++ "Host: " ++ x, + String.append(String.append(String.append(host, "\r\n"), + Http.headers.lines(Http.H{"User-Agent", "ezhttp"} + <> Http.H{"Accept", "*/*"} <> Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, hs, + Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))))), "\r\n" ++ body), + String.append(host, "\r\n" ++ "User-Agent: ezhttp\r\n" + ++ "Accept: */*\r\n" ++ "Connection: close\r\n" + ++ String.append(Http.headers.lines(hs), + String.append(Http.headers.lines(Http.clen.field(Http.utf8.len(body), + Nat.is_eq(Http.utf8.len(body), 0n))), "\r\n" ++ body))), + hd.plain_tail(host, hs, body))) diff --git a/proofs/RequestKeepsFieldNameSpelling.bend b/proofs/RequestKeepsFieldNameSpelling.bend new file mode 100644 index 0000000..8fd15d3 --- /dev/null +++ b/proofs/RequestKeepsFieldNameSpelling.bend @@ -0,0 +1,6 @@ +# proofs of laws/RequestKeepsFieldNameSpelling.bend +import Base +import ../laws/RequestKeepsFieldNameSpelling.bend as Laws + +def Laws.ask_keeps_field_name_spelling(): + {==} diff --git a/proofs/RequestLineAndHost.bend b/proofs/RequestLineAndHost.bend new file mode 100644 index 0000000..035e77a --- /dev/null +++ b/proofs/RequestLineAndHost.bend @@ -0,0 +1,31 @@ +# proofs of laws/RequestLineAndHost.bend +import Base +import ../src/http.bend as Http +import ../laws/RequestLineAndHost.bend as Laws + +# --- the client's requests --- + +def Laws.host_http_default(_host): + {==} + +def Laws.host_https_default(_host): + {==} + +def Laws.host_http_other(host, port, e): + %e : {Http.authority.of(host, port, _) == host ++ ":" ++ U32.show(port) + : String} + {==} + +def Laws.host_https_other(host, port, e): + %e : {Http.authority.of(host, port, _) == host ++ ":" ++ U32.show(port) + : String} + {==} + +def Laws.request_at(_method, _secure, _host, _port, _path, _hs, _body): + {==} + +def Laws.exchange_bad(_method, _why, _hs, _body): + {==} + +def Laws.exchange_loc(_method, _scheme, _host, _port, _path, _hs, _body): + {==} diff --git a/proofs/RequestLineOpensWithMethod.bend b/proofs/RequestLineOpensWithMethod.bend new file mode 100644 index 0000000..abd89c8 --- /dev/null +++ b/proofs/RequestLineOpensWithMethod.bend @@ -0,0 +1,11 @@ +# proofs of laws/RequestLineOpensWithMethod.bend +import Base +import ../src/http.bend as Http +import ../laws/RequestLineOpensWithMethod.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.request_names_method(method, host, path, hs, body): + M1.eq.starts_append(method, + " " ++ path ++ " HTTP/1.1\r\n" + ++ Http.headers.lines(Http.request.fields(host, hs, body)) + ++ "\r\n" ++ body) diff --git a/proofs/RequestNeedsBlankLine.bend b/proofs/RequestNeedsBlankLine.bend new file mode 100644 index 0000000..8dfc335 --- /dev/null +++ b/proofs/RequestNeedsBlankLine.bend @@ -0,0 +1,10 @@ +# proofs of laws/RequestNeedsBlankLine.bend +import Base +import ../src/http.bend as Http +import ../laws/RequestNeedsBlankLine.bend as Laws + +def Laws.ask_needs_blank_line(_raw, e): + %e : {Http.ask.cut(_) + == Http.Bad{"the request head has no blank line after it"} + : Http.Request} + {==} diff --git a/proofs/RequestNeedsRequestLine.bend b/proofs/RequestNeedsRequestLine.bend new file mode 100644 index 0000000..cff954c --- /dev/null +++ b/proofs/RequestNeedsRequestLine.bend @@ -0,0 +1,6 @@ +# proofs of laws/RequestNeedsRequestLine.bend +import Base +import ../laws/RequestNeedsRequestLine.bend as Laws + +def Laws.ask_needs_request_line(_body): + {==} diff --git a/proofs/RequestWithBothFramingsRejected.bend b/proofs/RequestWithBothFramingsRejected.bend new file mode 100644 index 0000000..fed48c4 --- /dev/null +++ b/proofs/RequestWithBothFramingsRejected.bend @@ -0,0 +1,6 @@ +# proofs of laws/RequestWithBothFramingsRejected.bend +import Base +import ../laws/RequestWithBothFramingsRejected.bend as Laws + +def Laws.ask_rejects_cl_and_te(): + {==} diff --git a/proofs/Respond200KeepsBody.bend b/proofs/Respond200KeepsBody.bend new file mode 100644 index 0000000..9fe4369 --- /dev/null +++ b/proofs/Respond200KeepsBody.bend @@ -0,0 +1,6 @@ +# proofs of laws/Respond200KeepsBody.bend +import Base +import ../laws/Respond200KeepsBody.bend as Laws + +def Laws.respond_200_keeps_body(): + {==} diff --git a/proofs/Respond204DropsBody.bend b/proofs/Respond204DropsBody.bend new file mode 100644 index 0000000..5230a90 --- /dev/null +++ b/proofs/Respond204DropsBody.bend @@ -0,0 +1,6 @@ +# proofs of laws/Respond204DropsBody.bend +import Base +import ../laws/Respond204DropsBody.bend as Laws + +def Laws.respond_204_drops_body(): + {==} diff --git a/proofs/Respond304DropsBody.bend b/proofs/Respond304DropsBody.bend new file mode 100644 index 0000000..f293c9f --- /dev/null +++ b/proofs/Respond304DropsBody.bend @@ -0,0 +1,6 @@ +# proofs of laws/Respond304DropsBody.bend +import Base +import ../laws/Respond304DropsBody.bend as Laws + +def Laws.respond_304_drops_body(): + {==} diff --git a/proofs/RespondOpensWithVersion.bend b/proofs/RespondOpensWithVersion.bend new file mode 100644 index 0000000..6446e1e --- /dev/null +++ b/proofs/RespondOpensWithVersion.bend @@ -0,0 +1,12 @@ +# proofs of laws/RespondOpensWithVersion.bend +import Base +import ../src/http.bend as Http +import ../laws/RespondOpensWithVersion.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.respond_opens_with_version(status, hs, body): + M1.eq.starts_append("HTTP/1.1 ", + U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" + ++ "Connection: close\r\n" ++ Http.headers.lines(hs) + ++ Http.clen.line(Http.utf8.len(body), Nat.is_eq(Http.utf8.len(body), 0n)) + ++ "\r\n" ++ body) diff --git a/proofs/ResponseCompleteByFraming.bend b/proofs/ResponseCompleteByFraming.bend new file mode 100644 index 0000000..6241084 --- /dev/null +++ b/proofs/ResponseCompleteByFraming.bend @@ -0,0 +1,204 @@ +# proofs of laws/ResponseCompleteByFraming.bend +import Base +import ../src/http.bend as Http +import ../laws/ResponseCompleteByFraming.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.whole_open(_f, _asked): + {==} + +def Laws.whole_head(f, asked, before, after, e): + %e : {Http.whole.step(asked, String.lines(before), after, _, + cut => Http.whole.go(f, asked, cut)) + == Http.whole.of(Http.frame(Bool.or(asked, Http.status.bare(Http.status.of(String.lines(before)))), + Http.head.go(String.lines(before), "transfer-encoding"), + Http.head.values(String.lines(before), "content-length")), after) : Bool} + {==} + +def Laws.status_of(_ls): + {==} + +def Laws.status_bare_some(_n): + {==} + +def Laws.status_bare_none(_m, e): + %e : {Http.status.bare(_) == False{} : Bool} + {==} + +def Laws.frame_bare(_te, _lens): + {==} + +def Laws.frame_chunked(_te, lens, e): + %e : {Http.frame.te(lens, _) == Http.Chunked{} : Http.Frame} + {==} + +def Laws.frame_sized(_te, lens, e1, e2, n, e3): + %e1 : {Http.frame.te(lens, _) == Http.Sized{n} : Http.Frame} + %e2 : {Http.frame.agree(lens, _) == Http.Sized{n} : Http.Frame} + %e3 : {Http.frame.sized(_) == Http.Sized{n} : Http.Frame} + {==} + +def Laws.frame_disagree(_te, lens, e1, e2): + %e1 : {Http.frame.te(lens, _) == Http.Closed{} : Http.Frame} + %e2 : {Http.frame.agree(lens, _) == Http.Closed{} : Http.Frame} + {==} + +def Laws.frame_unsized(_te, lens, e1, e2, e3): + %e1 : {Http.frame.te(lens, _) == Http.Closed{} : Http.Frame} + %e2 : {Http.frame.agree(lens, _) == Http.Closed{} : Http.Frame} + %e3 : {Http.frame.sized(_) == Http.Closed{} : Http.Frame} + {==} + +# --- counting octets without walking all of them (utf8.has) --- + +law lt_over: + for k: Nat + for w: Nat + for +l: Nat + for e: {True{} == Nat.is_lt(k, w) : Bool} + {Nat.is_lt(Nat.add(w, l), k) == False{} : Bool} + +def lt_over(k, w, l, e): + match k w: + case 0n 0n: + M1.eq.bit.absurd(&2, {Nat.is_lt(Nat.add(0n, l), 0n) == False{} : Bool}, + Equal.sym(Bool, True{}, False{}, e)) + case 0n 1n+p: + {==} + case 1n+q 0n: + M1.eq.bit.absurd(&2, {Nat.is_lt(Nat.add(0n, l), 1n+q) == False{} : Bool}, + Equal.sym(Bool, True{}, False{}, e)) + case 1n+q 1n+p: + lt_over(q, p, l, e) + +law lt_shift: + for k: Nat + for w: Nat + for +l: Nat + for e: {False{} == Nat.is_lt(k, w) : Bool} + {Nat.is_lt(Nat.add(w, l), k) == Nat.is_lt(l, Nat.sub(k, w)) : Bool} + +def lt_shift(k, w, l, e): + match k w: + case 0n 0n: + {==} + case 1n+q 0n: + {==} + case 0n 1n+p: + M1.eq.bit.absurd(&2, {Nat.is_lt(Nat.add(1n+p, l), 0n) + == Nat.is_lt(l, Nat.sub(0n, 1n+p)) : Bool}, e) + case 1n+q 1n+p: + lt_shift(q, p, l, e) + +law has_len.arm: + for n: Nat + for w: Nat + for +l: Nat + for r: Bool + for b: Bool + for e: {b == Nat.is_lt(n, w) : Bool} + for ih: {r == Bool.not(Nat.is_lt(l, Nat.sub(n, w))) : Bool} + {Http.octs.has.step(b, _u => r) == Bool.not(Nat.is_lt(Nat.add(w, l), n)) + : Bool} + +def has_len.arm(n, w, l, r, b, e, ih): + match b: + case True{}: + Equal.cong(Bool, Bool, x => Bool.not(x), False{}, + Nat.is_lt(Nat.add(w, l), n), + Equal.sym(Bool, Nat.is_lt(Nat.add(w, l), n), False{}, + lt_over(n, w, l, e))) + case False{}: + Equal.trans(Bool, r, Bool.not(Nat.is_lt(l, Nat.sub(n, w))), + Bool.not(Nat.is_lt(Nat.add(w, l), n)), ih, + Equal.cong(Bool, Bool, x => Bool.not(x), + Nat.is_lt(l, Nat.sub(n, w)), Nat.is_lt(Nat.add(w, l), n), + Equal.sym(Bool, Nat.is_lt(Nat.add(w, l), n), + Nat.is_lt(l, Nat.sub(n, w)), lt_shift(n, w, l, e)))) + +law has_len: + for s: String + for +n: Nat + {Http.octs.has(s, n) == Bool.not(Nat.is_lt(Http.octs.len(s), n)) : Bool} + +def has_len(s, n): + match s: + case SNil{}: + {==} + case SCon{+h, +t}: + +w = Http.octs.width(Char.to_u32(h)) + has_len.arm(n, w, Http.octs.len(t), Http.octs.has(t, Nat.sub(n, w)), + Nat.is_lt(n, w), {==}, has_len(t, Nat.sub(n, w))) + +def Laws.whole_bare(_body): + {==} + +def Laws.whole_sized(_n, _body): + {==} + +def Laws.whole_chunked(_body): + {==} + +def Laws.whole_closed(_body): + {==} + +def Laws.ended_fuel(_c): + {==} + +def Laws.ended_open(_f): + {==} + +def Laws.ended_bad(f, before, after, e): + %e : {Http.chunks.ended.hex(before, after, _, + cut => Http.chunks.ended(f, cut)) == True{} : Bool} + {==} + +def Laws.ended_last(f, before, after, e1, e2): + %e1 : {Http.chunks.ended.hex(before, after, _, + cut => Http.chunks.ended(f, cut)) == Http.chunks.trailer(after) : Bool} + %e2 : {Http.chunks.ended.size(after, Http.hexlen(before), _, + cut => Http.chunks.ended(f, cut)) == Http.chunks.trailer(after) : Bool} + {==} + +def Laws.ended_next(f, before, after, e1, e2, e3): + %e1 : {Http.chunks.ended.hex(before, after, _, + cut => Http.chunks.ended(f, cut)) == Http.chunks.ended(f, Http.divide(Http.octs.drop(after, + Nat.add(Http.hexlen(before), 2n)), "\r\n")) : Bool} + %e2 : {Http.chunks.ended.size(after, Http.hexlen(before), _, + cut => Http.chunks.ended(f, cut)) == Http.chunks.ended(f, Http.divide(Http.octs.drop(after, + Nat.add(Http.hexlen(before), 2n)), "\r\n")) : Bool} + %e3 : {Http.chunks.ended.data(after, Http.hexlen(before), _, + cut => Http.chunks.ended(f, cut)) == Http.chunks.ended(f, Http.divide(Http.octs.drop(after, + Nat.add(Http.hexlen(before), 2n)), "\r\n")) : Bool} + {==} + +def Laws.ended_short(f, before, after, e1, e2, e3): + %e1 : {Http.chunks.ended.hex(before, after, _, + cut => Http.chunks.ended(f, cut)) == False{} : Bool} + %e2 : {Http.chunks.ended.size(after, Http.hexlen(before), _, + cut => Http.chunks.ended(f, cut)) == False{} : Bool} + %e3 : {Http.chunks.ended.data(after, Http.hexlen(before), _, + cut => Http.chunks.ended(f, cut)) == False{} : Bool} + {==} + +def Laws.trailer_ended(_after): + {==} + +def Laws.chunk_ready(after, n): + Equal.cong(Bool, Bool, + x => Bool.and(x, String.starts_with(Http.octs.drop(after, n), "\r\n")), + Http.octs.has(after, Nat.add(n, 2n)), + Bool.not(Nat.is_lt(Http.octs.len(after), Nat.add(n, 2n))), + has_len(after, Nat.add(n, 2n))) + +def Laws.whole_go(_method, _raw): + {==} + +def Laws.whole_fuel(_asked, _c): + {==} + +def Laws.whole_interim(f, asked, before, after, e): + %e : {Http.whole.step(asked, String.lines(before), after, _, + cut => Http.whole.go(f, asked, cut)) + == Http.whole.go(f, asked, Http.divide(after, "\r\n\r\n")) : Bool} + {==} diff --git a/proofs/ResponseDisagreeingContentLengthsTorn.bend b/proofs/ResponseDisagreeingContentLengthsTorn.bend new file mode 100644 index 0000000..d7639e7 --- /dev/null +++ b/proofs/ResponseDisagreeingContentLengthsTorn.bend @@ -0,0 +1,6 @@ +# proofs of laws/ResponseDisagreeingContentLengthsTorn.bend +import Base +import ../laws/ResponseDisagreeingContentLengthsTorn.bend as Laws + +def Laws.parse_rejects_disagreeing_cl(): + {==} diff --git a/proofs/ResponseNeedsBlankLine.bend b/proofs/ResponseNeedsBlankLine.bend new file mode 100644 index 0000000..e055fd3 --- /dev/null +++ b/proofs/ResponseNeedsBlankLine.bend @@ -0,0 +1,10 @@ +# proofs of laws/ResponseNeedsBlankLine.bend +import Base +import ../src/http.bend as Http +import ../laws/ResponseNeedsBlankLine.bend as Laws + +def Laws.parse_needs_blank_line(_raw, e): + %e : {Http.parse.cut(_) + == Http.Torn{"the response head has no blank line after it"} + : Http.Reply} + {==} diff --git a/proofs/ResponseNeedsStatus.bend b/proofs/ResponseNeedsStatus.bend new file mode 100644 index 0000000..f386808 --- /dev/null +++ b/proofs/ResponseNeedsStatus.bend @@ -0,0 +1,10 @@ +# proofs of laws/ResponseNeedsStatus.bend +import Base +import ../src/http.bend as Http +import ../laws/ResponseNeedsStatus.bend as Laws + +def Laws.parse_needs_status(ls, body, e): + %e : {Http.parse.status(_, ls, body) + == Http.Torn{"the response has no status line"} + : Http.Reply} + {==} diff --git a/proofs/ResponseWithBothFramingsTorn.bend b/proofs/ResponseWithBothFramingsTorn.bend new file mode 100644 index 0000000..b79381a --- /dev/null +++ b/proofs/ResponseWithBothFramingsTorn.bend @@ -0,0 +1,6 @@ +# proofs of laws/ResponseWithBothFramingsTorn.bend +import Base +import ../laws/ResponseWithBothFramingsTorn.bend as Laws + +def Laws.parse_rejects_cl_and_te(): + {==} diff --git a/proofs/SchemeBeforeFirstColon.bend b/proofs/SchemeBeforeFirstColon.bend new file mode 100644 index 0000000..c967bb5 --- /dev/null +++ b/proofs/SchemeBeforeFirstColon.bend @@ -0,0 +1,9 @@ +# proofs of laws/SchemeBeforeFirstColon.bend +import Base +import ../src/url.bend as Url +import ../laws/SchemeBeforeFirstColon.bend as Laws + +def Laws.parse_scheme_first(_url, h, t, e): + %e : {Url.parse.scheme(_) + == Url.parse.rest(String.to_lower(h), String.join(t, ":")) : Url.Loc} + {==} diff --git a/proofs/SecondIsNextWord.bend b/proofs/SecondIsNextWord.bend new file mode 100644 index 0000000..a925355 --- /dev/null +++ b/proofs/SecondIsNextWord.bend @@ -0,0 +1,11 @@ +# proofs of laws/SecondIsNextWord.bend +import Base +import ../src/http.bend as Http +import ../laws/SecondIsNextWord.bend as Laws +import ../laws/SplitAtCharacter.bend as L1 +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.second_is_next_word(w, r, e): + Equal.cong(List<&2, String>, String, ps => Http.second(ps), + String.split(String.append(w, SCon{' ', r}), ' '), + w <> String.split(r, ' '), L1.split_cut(w, ' ', r, e)) diff --git a/proofs/SetCookieFieldsSelected.bend b/proofs/SetCookieFieldsSelected.bend new file mode 100644 index 0000000..f9831f0 --- /dev/null +++ b/proofs/SetCookieFieldsSelected.bend @@ -0,0 +1,18 @@ +# proofs of laws/SetCookieFieldsSelected.bend +import Base +import ../src/echo.bend as Echo +import ../laws/SetCookieFieldsSelected.bend as Laws + +def Laws.ck_values_nil(_hs, e): + %e : {Echo.values(_) == [] : List<&2, String>} + {==} + +def Laws.ck_values_hit(_name, value, t, e): + %e : {Echo.values.put(value, Echo.values(t), _) == value <> Echo.values(t) + : List<&2, String>} + {==} + +def Laws.ck_values_miss(_name, value, t, e): + %e : {Echo.values.put(value, Echo.values(t), _) == Echo.values(t) + : List<&2, String>} + {==} diff --git a/proofs/SetCookieParsesBack.bend b/proofs/SetCookieParsesBack.bend new file mode 100644 index 0000000..10c31c5 --- /dev/null +++ b/proofs/SetCookieParsesBack.bend @@ -0,0 +1,6 @@ +# proofs of laws/SetCookieParsesBack.bend +import Base +import ../laws/SetCookieParsesBack.bend as Laws + +def Laws.cookie_parse_round_trip(): + {==} diff --git a/proofs/SetCookieReadsPair.bend b/proofs/SetCookieReadsPair.bend new file mode 100644 index 0000000..d6e865f --- /dev/null +++ b/proofs/SetCookieReadsPair.bend @@ -0,0 +1,43 @@ +# proofs of laws/SetCookieReadsPair.bend +import Base +import ../src/qs.bend as Qs +import ../src/echo.bend as Echo +import ../laws/SetCookieReadsPair.bend as Laws + +# --- cookies sent back --- + +def Laws.ck_pair(_line): + {==} + +def Laws.ck_pair_none(_u, e): + %e : {Echo.pair.cut(_) == None{} : Maybe<&2, Qs.Kv()>} + {==} + +def Laws.ck_pair_some(_before, _after): + {==} + +def Laws.ck_named_empty(_name, _value): + {==} + +def Laws.ck_named_kept(_name, _value): + {==} + +def Laws.ck_trim(_s): + {==} + +def Laws.ck_lead_nil(_s, e): + %e : {Echo.wsp.lead(_) == "" : String} + {==} + +def Laws.ck_lead_wsp(h, t, e): + %e : {Echo.wsp.lead.if(t, h, _, _u => Echo.wsp.lead(t)) == Echo.wsp.lead(t) + : String} + {==} + +def Laws.ck_lead_other(h, t, e): + %e : {Echo.wsp.lead.if(t, h, _, _u => Echo.wsp.lead(t)) == SCon{h, t} + : String} + {==} + +def Laws.ck_wsp(_c): + {==} diff --git a/proofs/ShortRequestBodyIsBad.bend b/proofs/ShortRequestBodyIsBad.bend new file mode 100644 index 0000000..11f0ed6 --- /dev/null +++ b/proofs/ShortRequestBodyIsBad.bend @@ -0,0 +1,10 @@ +# proofs of laws/ShortRequestBodyIsBad.bend +import Base +import ../src/http.bend as Http +import ../laws/ShortRequestBodyIsBad.bend as Laws + +def Laws.ask_clen_short(method, target, hs, n, body, e): + %e : {Http.ask.short(method, target, hs, n, body, _) + == Http.Bad{"the request body stopped before its Content-Length"} + : Http.Request} + {==} diff --git a/proofs/ShowReplyOpensWithStatus.bend b/proofs/ShowReplyOpensWithStatus.bend new file mode 100644 index 0000000..56826fa --- /dev/null +++ b/proofs/ShowReplyOpensWithStatus.bend @@ -0,0 +1,7 @@ +# proofs of laws/ShowReplyOpensWithStatus.bend +import Base +import ../laws/ShowReplyOpensWithStatus.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.show_reply_opens_with_status(status, _hs, body): + M1.eq.starts_append(U32.show(status), " " ++ body) diff --git a/proofs/ShowTornIsMarked.bend b/proofs/ShowTornIsMarked.bend new file mode 100644 index 0000000..899e7d1 --- /dev/null +++ b/proofs/ShowTornIsMarked.bend @@ -0,0 +1,7 @@ +# proofs of laws/ShowTornIsMarked.bend +import Base +import ../laws/ShowTornIsMarked.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.show_torn_is_marked(why): + M1.eq.starts_nil(why) diff --git a/proofs/SplitAtCharacter.bend b/proofs/SplitAtCharacter.bend new file mode 100644 index 0000000..4575f94 --- /dev/null +++ b/proofs/SplitAtCharacter.bend @@ -0,0 +1,6 @@ +# proofs of laws/SplitAtCharacter.bend +import Base +import ../laws/SplitAtCharacter.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + + diff --git a/proofs/StaleAtMaxAge.bend b/proofs/StaleAtMaxAge.bend new file mode 100644 index 0000000..a88e80d --- /dev/null +++ b/proofs/StaleAtMaxAge.bend @@ -0,0 +1,6 @@ +# proofs of laws/StaleAtMaxAge.bend +import Base +import ../laws/StaleAtMaxAge.bend as Laws + +def Laws.fresh_max_age_limit(): + {==} diff --git a/proofs/StreamedRepliesAreChunked.bend b/proofs/StreamedRepliesAreChunked.bend new file mode 100644 index 0000000..ad6431e --- /dev/null +++ b/proofs/StreamedRepliesAreChunked.bend @@ -0,0 +1,43 @@ +# proofs of laws/StreamedRepliesAreChunked.bend +import Base +import ../src/http.bend as Http +import ../laws/StreamedRepliesAreChunked.bend as Laws + +def Laws.stream_head(status, e, hs): + %e : {"HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" + ++ "Connection: close\r\n" ++ Http.headers.lines(hs) + ++ Http.te.line(Bool.not(_)) ++ "\r\n" + == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" + ++ "Connection: close\r\n" ++ Http.headers.lines(hs) + ++ "Transfer-Encoding: chunked\r\n" ++ "\r\n" : String} + {==} + +def Laws.stream_head_bare(status, e, hs): + %e : {"HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" + ++ "Connection: close\r\n" ++ Http.headers.lines(hs) + ++ Http.te.line(Bool.not(_)) ++ "\r\n" + == "HTTP/1.1 " ++ U32.show(status) ++ " " ++ Http.reason(status) ++ "\r\n" + ++ "Connection: close\r\n" ++ Http.headers.lines(hs) ++ "" + ++ "\r\n" : String} + {==} + +def Laws.chunk_line(_n): + {==} + +def Laws.hex_at(_n, _k): + {==} + +def Laws.hex_nib(_v): + {==} + +def Laws.chunk_empty(n, e): + %e : {Http.chunk.frame.of(n, _) == Http.NoChunk{} : Http.Chunk} + {==} + +def Laws.chunk_some(n, e): + %e : {Http.chunk.frame.of(n, _) == Http.Chunk{Http.chunk.line(n)} : Http.Chunk} + {==} + +def Laws.chunk_end(_s, e): + %e : {Http.chunk.end() ++ _ == "0\r\n\r\n" : String} + {==} diff --git a/proofs/TakeDropSplitAtOneOffset.bend b/proofs/TakeDropSplitAtOneOffset.bend new file mode 100644 index 0000000..bff05d0 --- /dev/null +++ b/proofs/TakeDropSplitAtOneOffset.bend @@ -0,0 +1,32 @@ +# proofs of laws/TakeDropSplitAtOneOffset.bend +import Base +import ../src/http.bend as Http +import ../laws/TakeDropSplitAtOneOffset.bend as Laws + +# LAW: one step of the UTF-8 partition +law utf8_split.arm: + for -h: Char + for -t: String + for -m: Nat + for b: Bool + for ih: {String.append(Http.octs.take(t, m), Http.octs.drop(t, m)) == t : String} + {String.append( + Http.octs.take.step(b, h, _u => Http.octs.take(t, m)), + Http.octs.drop.step(b, h, t, _u => Http.octs.drop(t, m))) == SCon{h, t} : String} + +def utf8_split.arm(h, t, m, b, ih): + match b: + case True{}: + {==} + case False{}: + Equal.cong(String, String, x => SCon{h, x}, + String.append(Http.octs.take(t, m), Http.octs.drop(t, m)), t, ih) + +def Laws.utf8_split(s, n): + match s: + case SNil{}: + {==} + case SCon{+h, +t}: + utf8_split.arm(h, t, Nat.sub(n, Http.octs.width(Char.to_u32(h))), + Nat.is_lt(n, Http.octs.width(Char.to_u32(h))), + Laws.utf8_split(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))))) diff --git a/proofs/TimeoutZeroIsOneMs.bend b/proofs/TimeoutZeroIsOneMs.bend new file mode 100644 index 0000000..0ccb18e --- /dev/null +++ b/proofs/TimeoutZeroIsOneMs.bend @@ -0,0 +1,12 @@ +# proofs of laws/TimeoutZeroIsOneMs.bend +import Base +import ../src/wirespec.bend as Spec +import ../laws/TimeoutZeroIsOneMs.bend as Laws + +def Laws.ms_given(ms, e): + %e : {Spec.wire.ms.pick(ms, _) == ms : U32} + {==} + +def Laws.ms_zero(ms, e): + %e : {Spec.wire.ms.pick(ms, _) == 1 : U32} + {==} diff --git a/proofs/TimeoutsReadAsTimedOut.bend b/proofs/TimeoutsReadAsTimedOut.bend new file mode 100644 index 0000000..b3a0fd0 --- /dev/null +++ b/proofs/TimeoutsReadAsTimedOut.bend @@ -0,0 +1,20 @@ +# proofs of laws/TimeoutsReadAsTimedOut.bend +import Base +import ../src/wirespec.bend as Spec +import ../laws/TimeoutsReadAsTimedOut.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +# --- the client's timeout --- + +def Laws.why_late(_why): + {==} + +def Laws.why_other(_n, e, why): + %e : {Spec.wire.why.of(why, _) == why : String} + {==} + +def Laws.why_none(_why): + {==} + +def Laws.why_late_timed_out(why): + M1.eq.starts_append("timed out: ", why) diff --git a/proofs/UnescapeFindsReferences.bend b/proofs/UnescapeFindsReferences.bend new file mode 100644 index 0000000..6d80179 --- /dev/null +++ b/proofs/UnescapeFindsReferences.bend @@ -0,0 +1,180 @@ +# proofs of laws/UnescapeFindsReferences.bend +import Base +import ../src/unescape.bend as Ref +import ../laws/UnescapeFindsReferences.bend as Laws + +# --- HTML character references --- + +def Laws.ref_unescape(_s): + {==} + +def Laws.ref_scan_nil(_st): + {==} + +def Laws.ref_scan_cons(_c, _t, _st): + {==} + +def Laws.ref_text_amp(c, k, e): + %e : {Ref.text.of(c, k, _) == k(Ref.Amp{}) : String} + {==} + +def Laws.ref_text_other(c, k, e): + %e : {Ref.text.of(c, k, _) == SCon{c, k(Ref.Txt{})} : String} + {==} + +def Laws.ref_amp_hash(c, k, e): + %e : {Ref.amp(c, k, _) == k(Ref.Hash{}) : String} + {==} + +def Laws.ref_amp_name(c, k, e1, e2): + %e1 : {Ref.amp(c, k, _) == k(Ref.Name{SCon{c, ""}, 1n}) : String} + %e2 : {Ref.amp.name(c, k, _) == k(Ref.Name{SCon{c, ""}, 1n}) : String} + {==} + +def Laws.ref_amp_other(c, k, e1, e2): + %e1 : {Ref.amp(c, k, _) == SCon{'&', Ref.step(Ref.Txt{}, c, k)} : String} + %e2 : {Ref.amp.name(c, k, _) == SCon{'&', Ref.step(Ref.Txt{}, c, k)} + : String} + {==} + +def Laws.ref_hash_dec(c, k, e): + %e : {Ref.hash.dec(c, k, Ref.digit(False{}, c), _) + == k(Ref.Num{False{}, Ref.digit(False{}, c)}) : String} + {==} + +def Laws.ref_hash_x(c, k, e1, e2): + %e1 : {Ref.hash.dec(c, k, Ref.digit(False{}, c), _) == k(Ref.HashX{c}) + : String} + %e2 : {Ref.hash.x(c, k, _) == k(Ref.HashX{c}) : String} + {==} + +def Laws.ref_hash_other(c, k, e1, e2): + %e1 : {Ref.hash.dec(c, k, Ref.digit(False{}, c), _) + == "&#" ++ Ref.step(Ref.Txt{}, c, k) : String} + %e2 : {Ref.hash.x(c, k, _) == "&#" ++ Ref.step(Ref.Txt{}, c, k) : String} + {==} + +def Laws.ref_hashx_digit(x, c, k, e): + %e : {Ref.hash.hex.of(x, c, k, Ref.digit(True{}, c), _) + == k(Ref.Num{True{}, Ref.digit(True{}, c)}) : String} + {==} + +def Laws.ref_hashx_other(x, c, k, e): + %e : {Ref.hash.hex.of(x, c, k, Ref.digit(True{}, c), _) + == SCon{'&', SCon{'#', SCon{x, Ref.step(Ref.Txt{}, c, k)}}} : String} + {==} + +def Laws.ref_num_digit(hex, n, c, k, e): + %e : {Ref.num(hex, n, c, k, Ref.digit(hex, c), _) + == k(Ref.Num{hex, Ref.grow(hex, n, Ref.digit(hex, c))}) : String} + {==} + +def Laws.ref_num_semi(hex, n, c, k, e1, e2): + %e1 : {Ref.num(hex, n, c, k, Ref.digit(hex, c), _) + == Ref.code(n) ++ k(Ref.Txt{}) : String} + %e2 : {Ref.num.end(n, c, k, _) == Ref.code(n) ++ k(Ref.Txt{}) : String} + {==} + +def Laws.ref_num_other(hex, n, c, k, e1, e2): + %e1 : {Ref.num(hex, n, c, k, Ref.digit(hex, c), _) + == Ref.code(n) ++ Ref.step(Ref.Txt{}, c, k) : String} + %e2 : {Ref.num.end(n, c, k, _) == Ref.code(n) ++ Ref.step(Ref.Txt{}, c, k) + : String} + {==} + +def Laws.ref_name_semi(s, len, c, k, e): + %e : {Ref.name(s, len, c, k, _) == Ref.named(s ++ ";") ++ k(Ref.Txt{}) + : String} + {==} + +def Laws.ref_name_join(s, len, c, k, e1, e2): + %e1 : {Ref.name(s, len, c, k, _) + == k(Ref.Name{s ++ SCon{c, ""}, 1n+len}) : String} + %e2 : {Ref.name.more(s, len, c, k, _) + == k(Ref.Name{s ++ SCon{c, ""}, 1n+len}) : String} + {==} + +def Laws.ref_name_stop(s, len, c, k, e1, e2): + %e1 : {Ref.name(s, len, c, k, _) + == Ref.named(s) ++ Ref.step(Ref.Txt{}, c, k) : String} + %e2 : {Ref.name.more(s, len, c, k, _) + == Ref.named(s) ++ Ref.step(Ref.Txt{}, c, k) : String} + {==} + +def Laws.ref_end_text(_st, e): + %e : {Ref.end(_) == "" : String} + {==} + +def Laws.ref_end_amp(_st, e): + %e : {Ref.end(_) == "&" : String} + {==} + +def Laws.ref_end_hash(_st, e): + %e : {Ref.end(_) == "&#" : String} + {==} + +def Laws.ref_end_hashx(_x): + {==} + +def Laws.ref_end_num(_hex, _n): + {==} + +def Laws.ref_end_name(_s, _len): + {==} + +def Laws.ref_name_char(_c): + {==} + +def Laws.ref_digit_dec(u, e): + %e : {Bool.pick(U32, _, U32.sub(u, 48), 16) == U32.sub(u, 48) : U32} + {==} + +def Laws.ref_digit_dec_none(u, e): + %e : {Bool.pick(U32, _, U32.sub(u, 48), 16) == 16 : U32} + {==} + +def Laws.ref_digit_hex_dec(u, e): + %e : {Bool.pick(U32, _, U32.sub(u, 48), + Bool.pick(U32, Bool.and(U32.is_le(65, u), U32.is_le(u, 70)), + U32.sub(u, 55), + Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), + U32.sub(u, 87), 16))) == U32.sub(u, 48) : U32} + {==} + +def Laws.ref_digit_hex_upper(u, e1, e2): + %e1 : {Bool.pick(U32, _, U32.sub(u, 48), + Bool.pick(U32, Bool.and(U32.is_le(65, u), U32.is_le(u, 70)), + U32.sub(u, 55), + Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), + U32.sub(u, 87), 16))) == U32.sub(u, 55) : U32} + %e2 : {Bool.pick(U32, _, U32.sub(u, 55), + Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), + U32.sub(u, 87), 16)) == U32.sub(u, 55) : U32} + {==} + +def Laws.ref_digit_hex_lower(u, e1, e2, e3): + %e1 : {Bool.pick(U32, _, U32.sub(u, 48), + Bool.pick(U32, Bool.and(U32.is_le(65, u), U32.is_le(u, 70)), + U32.sub(u, 55), + Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), + U32.sub(u, 87), 16))) == U32.sub(u, 87) : U32} + %e2 : {Bool.pick(U32, _, U32.sub(u, 55), + Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), + U32.sub(u, 87), 16)) == U32.sub(u, 87) : U32} + %e3 : {Bool.pick(U32, _, U32.sub(u, 87), 16) == U32.sub(u, 87) : U32} + {==} + +def Laws.ref_digit_hex_none(u, e1, e2, e3): + %e1 : {Bool.pick(U32, _, U32.sub(u, 48), + Bool.pick(U32, Bool.and(U32.is_le(65, u), U32.is_le(u, 70)), + U32.sub(u, 55), + Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), + U32.sub(u, 87), 16))) == 16 : U32} + %e2 : {Bool.pick(U32, _, U32.sub(u, 55), + Bool.pick(U32, Bool.and(U32.is_le(97, u), U32.is_le(u, 102)), + U32.sub(u, 87), 16)) == 16 : U32} + %e3 : {Bool.pick(U32, _, U32.sub(u, 87), 16) == 16 : U32} + {==} + +def Laws.ref_grow(_hex, _n, _d): + {==} diff --git a/proofs/UnescapeUndoesEscape.bend b/proofs/UnescapeUndoesEscape.bend new file mode 100644 index 0000000..38c218e --- /dev/null +++ b/proofs/UnescapeUndoesEscape.bend @@ -0,0 +1,182 @@ +# proofs of laws/UnescapeUndoesEscape.bend +import Base +import ../src/html.bend as Html +import ../src/unescape.bend as Ref +import ../laws/UnescapeUndoesEscape.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +# --- unescape after escape --- + +# what a comparison that is not EQ cannot equal +def eq.cmp.emp(c: Cmp) -> Type: + match c: + case LT{}: + Unit + case EQ{}: + Empty + case GT{}: + Unit + +def eq.cmp.lt(e: {LT{} == EQ{} : Cmp}) -> Empty: + %e : eq.cmp.emp(_) + Unit{} + +def eq.cmp.gt(e: {GT{} == EQ{} : Cmp}) -> Empty: + %e : eq.cmp.emp(_) + Unit{} + +def eq.bool_cmp_eq(a: Bool, b: Bool, e: {Bool.cmp(a, b) == EQ{} : Cmp}) + -> {a == b : Bool}: + match a b: + case False{} False{}: + {==} + case False{} True{}: + M1.eq.bit.gone(&2, {False{} == True{} : Bool}, eq.cmp.lt(e)) + case True{} False{}: + M1.eq.bit.gone(&2, {True{} == False{} : Bool}, eq.cmp.gt(e)) + case True{} True{}: + {==} + +def eq.fin.t(ab: Bool, bb: Bool, t: Cmp, + e: {Word.cmp.fin(ab, bb, t) == EQ{} : Cmp}) -> {t == EQ{} : Cmp}: + match t: + case LT{}: + M1.eq.bit.gone(&2, {LT{} == EQ{} : Cmp}, eq.cmp.lt(e)) + case EQ{}: + {==} + case GT{}: + M1.eq.bit.gone(&2, {GT{} == EQ{} : Cmp}, eq.cmp.gt(e)) + +def eq.fin.b(ab: Bool, bb: Bool, t: Cmp, + e: {Word.cmp.fin(ab, bb, t) == EQ{} : Cmp}) -> {Bool.cmp(ab, bb) == EQ{} : Cmp}: + match t: + case LT{}: + M1.eq.bit.gone(&2, {Bool.cmp(ab, bb) == EQ{} : Cmp}, eq.cmp.lt(e)) + case EQ{}: + e + case GT{}: + M1.eq.bit.gone(&2, {Bool.cmp(ab, bb) == EQ{} : Cmp}, eq.cmp.gt(e)) + +def eq.word_cmp_eq(n: Nat, a: Word(n), b: Word(n), + +e: {Word.cmp(n, a, b) == EQ{} : Cmp}) -> {a == b : Word(n)}: + match n: + case 0n: + match a b: + case WNil{} WNil{}: + {==} + case 1n+(+p): + match a b: + case WCon{+ab, +at} WCon{+bb, +bt}: + be = eq.bool_cmp_eq(ab, bb, eq.fin.b(ab, bb, Word.cmp(p, at, bt), e)) + we = eq.word_cmp_eq(p, at, bt, eq.fin.t(ab, bb, Word.cmp(p, at, bt), e)) + %be : {WCon{ab, at} == WCon{_, bt} : Word(1n+p)} + %we : {WCon{ab, at} == WCon{ab, _} : Word(1n+p)} + {==} + +def eq.cmp_is_eq(t: Cmp, e: {Cmp.is_eq(t) == True{} : Bool}) + -> {t == EQ{} : Cmp}: + match t: + case LT{}: + M1.eq.bit.absurd(&2, {LT{} == EQ{} : Cmp}, e) + case EQ{}: + {==} + case GT{}: + M1.eq.bit.absurd(&2, {GT{} == EQ{} : Cmp}, e) + +def eq.char_eq_true(c: Char, d: Char, e: {Char.is_eq(c, d) == True{} : Bool}) + -> {c == d : Char}: + match c d: + case Chr{U32{+w}} Chr{U32{+v}}: + we = eq.word_cmp_eq(32n, w, v, eq.cmp_is_eq(Word.cmp(32n, w, v), e)) + %we : {Chr{U32{w}} == Chr{U32{_}} : Char} + {==} + +# a character that is not `&`, `<`, `>`, `"` or `'` scans as itself +def rt.other(+c: Char, x: String, e: {False{} == Char.is_eq(c, '&') : Bool}) + -> {Ref.scan(SCon{c, x}, Ref.Txt{}) == SCon{c, Ref.scan(x, Ref.Txt{})} + : String}: + %e : {Ref.text.of(c, st2 => Ref.scan(x, st2), _) + == SCon{c, Ref.scan(x, Ref.Txt{})} : String} + {==} + +def rt.sq(+c: Char, x: String, ea: {False{} == Char.is_eq(c, '&') : Bool}, + b: Bool, e: {Char.is_eq(c, '\'') == b : Bool}) + -> {Ref.scan(Html.one.sq(c, x, b), Ref.Txt{}) + == SCon{c, Ref.scan(x, Ref.Txt{})} : String}: + match b: + case True{}: + ce = Equal.sym(Char, c, '\'', eq.char_eq_true(c, '\'', e)) + %ce : {Ref.scan("'" ++ x, Ref.Txt{}) + == SCon{_, Ref.scan(x, Ref.Txt{})} : String} + {==} + case False{}: + rt.other(c, x, ea) + +def rt.dq(+c: Char, x: String, ea: {False{} == Char.is_eq(c, '&') : Bool}, + b: Bool, e: {Char.is_eq(c, '"') == b : Bool}) + -> {Ref.scan(Html.one.dq(c, x, b), Ref.Txt{}) + == SCon{c, Ref.scan(x, Ref.Txt{})} : String}: + match b: + case True{}: + ce = Equal.sym(Char, c, '"', eq.char_eq_true(c, '"', e)) + %ce : {Ref.scan(""" ++ x, Ref.Txt{}) + == SCon{_, Ref.scan(x, Ref.Txt{})} : String} + {==} + case False{}: + rt.sq(c, x, ea, Char.is_eq(c, '\''), {==}) + +def rt.gt(+c: Char, x: String, ea: {False{} == Char.is_eq(c, '&') : Bool}, + b: Bool, e: {Char.is_eq(c, '>') == b : Bool}) + -> {Ref.scan(Html.one.gt(c, x, b), Ref.Txt{}) + == SCon{c, Ref.scan(x, Ref.Txt{})} : String}: + match b: + case True{}: + ce = Equal.sym(Char, c, '>', eq.char_eq_true(c, '>', e)) + %ce : {Ref.scan(">" ++ x, Ref.Txt{}) + == SCon{_, Ref.scan(x, Ref.Txt{})} : String} + {==} + case False{}: + rt.dq(c, x, ea, Char.is_eq(c, '"'), {==}) + +def rt.lt(+c: Char, x: String, ea: {False{} == Char.is_eq(c, '&') : Bool}, + b: Bool, e: {Char.is_eq(c, '<') == b : Bool}) + -> {Ref.scan(Html.one.lt(c, x, b), Ref.Txt{}) + == SCon{c, Ref.scan(x, Ref.Txt{})} : String}: + match b: + case True{}: + ce = Equal.sym(Char, c, '<', eq.char_eq_true(c, '<', e)) + %ce : {Ref.scan("<" ++ x, Ref.Txt{}) + == SCon{_, Ref.scan(x, Ref.Txt{})} : String} + {==} + case False{}: + rt.gt(c, x, ea, Char.is_eq(c, '>'), {==}) + +def rt.amp(+c: Char, x: String, b: Bool, e: {Char.is_eq(c, '&') == b : Bool}) + -> {Ref.scan(Html.one.amp(c, x, b), Ref.Txt{}) + == SCon{c, Ref.scan(x, Ref.Txt{})} : String}: + match b: + case True{}: + ce = Equal.sym(Char, c, '&', eq.char_eq_true(c, '&', e)) + %ce : {Ref.scan("&" ++ x, Ref.Txt{}) + == SCon{_, Ref.scan(x, Ref.Txt{})} : String} + {==} + case False{}: + rt.lt(c, x, Equal.sym(Bool, Char.is_eq(c, '&'), False{}, e), + Char.is_eq(c, '<'), {==}) + +def rt.go(s: String) + -> {Ref.scan(Html.escape.onto(s, ""), Ref.Txt{}) == s : String}: + match s: + case SNil{}: + {==} + case SCon{+c, +t}: + Equal.trans(String, + Ref.scan(Html.one(c, Html.escape.onto(t, "")), Ref.Txt{}), + SCon{c, Ref.scan(Html.escape.onto(t, ""), Ref.Txt{})}, + SCon{c, t}, + rt.amp(c, Html.escape.onto(t, ""), Char.is_eq(c, '&'), {==}), + Equal.cong(String, String, y => SCon{c, y}, + Ref.scan(Html.escape.onto(t, ""), Ref.Txt{}), t, rt.go(t))) + +def Laws.ref_round(s): + rt.go(s) diff --git a/proofs/UnframedBodyIsRest.bend b/proofs/UnframedBodyIsRest.bend new file mode 100644 index 0000000..90d82a0 --- /dev/null +++ b/proofs/UnframedBodyIsRest.bend @@ -0,0 +1,6 @@ +# proofs of laws/UnframedBodyIsRest.bend +import Base +import ../laws/UnframedBodyIsRest.bend as Laws + +def Laws.unframed_is_rest(_st, _hs, _body): + {==} diff --git a/proofs/UnknownStatusHasEmptyReason.bend b/proofs/UnknownStatusHasEmptyReason.bend new file mode 100644 index 0000000..b60d6a5 --- /dev/null +++ b/proofs/UnknownStatusHasEmptyReason.bend @@ -0,0 +1,6 @@ +# proofs of laws/UnknownStatusHasEmptyReason.bend +import Base +import ../laws/UnknownStatusHasEmptyReason.bend as Laws + +def Laws.reason_unknown_empty(): + {==} diff --git a/proofs/UnquotePlusReadsSpaces.bend b/proofs/UnquotePlusReadsSpaces.bend new file mode 100644 index 0000000..52beecb --- /dev/null +++ b/proofs/UnquotePlusReadsSpaces.bend @@ -0,0 +1,10 @@ +# proofs of laws/UnquotePlusReadsSpaces.bend +import Base +import ../laws/UnquotePlusReadsSpaces.bend as Laws + +def Laws.qs_unquote_plus(_s): + {==} + +def Laws.qs_byte_plus(c, e): + %e : {Bool.pick(U32, Bool.and(True{}, _), 32, Char.to_u32(c)) == 32 : U32} + {==} diff --git a/proofs/UnquoteReadsEscapes.bend b/proofs/UnquoteReadsEscapes.bend new file mode 100644 index 0000000..0fe1605 --- /dev/null +++ b/proofs/UnquoteReadsEscapes.bend @@ -0,0 +1,115 @@ +# proofs of laws/UnquoteReadsEscapes.bend +import Base +import ../src/http.bend as Http +import ../src/qs.bend as Qs +import ../laws/UnquoteReadsEscapes.bend as Laws + +# --- percent-decoding and query strings --- + +def Laws.qs_unquote(_s): + {==} + +def Laws.qs_toks_nil(_plus, _st): + {==} + +def Laws.qs_toks_cons(_c, _t, _plus, _st): + {==} + +def Laws.qs_tok_wide(plus, c, k, e): + %e : {Qs.tok.wide(plus, c, k, _) == Qs.W{c} <> k(Qs.Plain{}) + : List<&2, Qs.Tok>} + {==} + +def Laws.qs_tok_open(plus, c, k, e1, e2): + %e1 : {Qs.tok.wide(plus, c, k, _) == k(Qs.Pct{}) : List<&2, Qs.Tok>} + %e2 : {Qs.tok.ascii(plus, c, k, _) == k(Qs.Pct{}) : List<&2, Qs.Tok>} + {==} + +def Laws.qs_tok_byte(plus, c, k, e1, e2): + %e1 : {Qs.tok.wide(plus, c, k, _) + == Qs.B{Qs.tok.byte(plus, c)} <> k(Qs.Plain{}) : List<&2, Qs.Tok>} + %e2 : {Qs.tok.ascii(plus, c, k, _) + == Qs.B{Qs.tok.byte(plus, c)} <> k(Qs.Plain{}) : List<&2, Qs.Tok>} + {==} + +def Laws.qs_byte_other(_plus, c, e): + %e : {Bool.pick(U32, _, 32, Char.to_u32(c)) == Char.to_u32(c) : U32} + {==} + +def Laws.qs_tok_pct_hex(plus, c, k, e): + %e : {Qs.tok.pct(plus, c, k, _) == k(Qs.Pct1{c}) : List<&2, Qs.Tok>} + {==} + +def Laws.qs_tok_pct_other(plus, c, k, e): + %e : {Qs.tok.pct(plus, c, k, _) + == Qs.B{37} <> Qs.tok.step(plus, Qs.Plain{}, c, k) : List<&2, Qs.Tok>} + {==} + +def Laws.qs_tok_pct1_hex(plus, d, c, k, e): + %e : {Qs.tok.pct1(plus, d, c, k, _) + == Qs.B{U32.add(U32.mul(Http.hex.val(d), 16), Http.hex.val(c))} + <> k(Qs.Plain{}) : List<&2, Qs.Tok>} + {==} + +def Laws.qs_tok_pct1_other(plus, d, c, k, e): + %e : {Qs.tok.pct1(plus, d, c, k, _) + == Qs.B{37} <> (Qs.B{Char.to_u32(d)} + <> Qs.tok.step(plus, Qs.Plain{}, c, k)) : List<&2, Qs.Tok>} + {==} + +def Laws.qs_tok_end_plain(_st, e): + %e : {Qs.tok.end(_) == [] : List<&2, Qs.Tok>} + {==} + +def Laws.qs_tok_end_pct(_st, e): + %e : {Qs.tok.end(_) == [Qs.B{37}] : List<&2, Qs.Tok>} + {==} + +def Laws.qs_tok_end_pct1(_d): + {==} + +def Laws.qs_tok_hex(_c): + {==} + +def Laws.qs_hexval_dec(u, e): + %e : {Bool.pick(U32, _, U32.sub(u, 48), + Bool.pick(U32, Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)), + U32.sub(u, 87), + Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), + U32.sub(u, 55), 16))) == U32.sub(u, 48) : U32} + {==} + +def Laws.qs_hexval_upper(u, e1, e2, e3): + %e1 : {Bool.pick(U32, _, U32.sub(u, 48), + Bool.pick(U32, Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)), + U32.sub(u, 87), + Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), + U32.sub(u, 55), 16))) == U32.sub(u, 55) : U32} + %e2 : {Bool.pick(U32, _, U32.sub(u, 87), + Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), + U32.sub(u, 55), 16)) == U32.sub(u, 55) : U32} + %e3 : {Bool.pick(U32, _, U32.sub(u, 55), 16) == U32.sub(u, 55) : U32} + {==} + +def Laws.qs_hexval_lower(u, e1, e2): + %e1 : {Bool.pick(U32, _, U32.sub(u, 48), + Bool.pick(U32, Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)), + U32.sub(u, 87), + Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), + U32.sub(u, 55), 16))) == U32.sub(u, 87) : U32} + %e2 : {Bool.pick(U32, _, U32.sub(u, 87), + Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), + U32.sub(u, 55), 16)) == U32.sub(u, 87) : U32} + {==} + +def Laws.qs_hexval_none(u, e1, e2, e3): + %e1 : {Bool.pick(U32, _, U32.sub(u, 48), + Bool.pick(U32, Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)), + U32.sub(u, 87), + Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), + U32.sub(u, 55), 16))) == 16 : U32} + %e2 : {Bool.pick(U32, _, U32.sub(u, 87), + Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), + U32.sub(u, 55), 16)) == 16 : U32} + %e3 : {Bool.pick(U32, _, U32.sub(u, 55), 16) == 16 : U32} + {==} diff --git a/proofs/UrlBadPortUsesDefault.bend b/proofs/UrlBadPortUsesDefault.bend new file mode 100644 index 0000000..4865f23 --- /dev/null +++ b/proofs/UrlBadPortUsesDefault.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlBadPortUsesDefault.bend +import Base +import ../laws/UrlBadPortUsesDefault.bend as Laws + +def Laws.url_bad_port_uses_default(): + {==} diff --git a/proofs/UrlEmptyPath.bend b/proofs/UrlEmptyPath.bend new file mode 100644 index 0000000..992e95a --- /dev/null +++ b/proofs/UrlEmptyPath.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlEmptyPath.bend +import Base +import ../laws/UrlEmptyPath.bend as Laws + +def Laws.url_empty_path(): + {==} diff --git a/proofs/UrlFragmentHidesQuery.bend b/proofs/UrlFragmentHidesQuery.bend new file mode 100644 index 0000000..80b8e9b --- /dev/null +++ b/proofs/UrlFragmentHidesQuery.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlFragmentHidesQuery.bend +import Base +import ../laws/UrlFragmentHidesQuery.bend as Laws + +def Laws.url_fragment_hides_query(): + {==} diff --git a/proofs/UrlHttpDefaultPort.bend b/proofs/UrlHttpDefaultPort.bend new file mode 100644 index 0000000..1db4626 --- /dev/null +++ b/proofs/UrlHttpDefaultPort.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlHttpDefaultPort.bend +import Base +import ../laws/UrlHttpDefaultPort.bend as Laws + +def Laws.url_http_default_port(): + {==} diff --git a/proofs/UrlHttpsDefaultPort.bend b/proofs/UrlHttpsDefaultPort.bend new file mode 100644 index 0000000..ae31dc5 --- /dev/null +++ b/proofs/UrlHttpsDefaultPort.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlHttpsDefaultPort.bend +import Base +import ../laws/UrlHttpsDefaultPort.bend as Laws + +def Laws.url_https_default_port(): + {==} diff --git a/proofs/UrlHttpsExplicitPort.bend b/proofs/UrlHttpsExplicitPort.bend new file mode 100644 index 0000000..b4c1bfa --- /dev/null +++ b/proofs/UrlHttpsExplicitPort.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlHttpsExplicitPort.bend +import Base +import ../laws/UrlHttpsExplicitPort.bend as Laws + +def Laws.url_https_explicit_port(): + {==} diff --git a/proofs/UrlIpv4Host.bend b/proofs/UrlIpv4Host.bend new file mode 100644 index 0000000..aa802c8 --- /dev/null +++ b/proofs/UrlIpv4Host.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlIpv4Host.bend +import Base +import ../laws/UrlIpv4Host.bend as Laws + +def Laws.url_ipv4_host(): + {==} diff --git a/proofs/UrlIpv4Port.bend b/proofs/UrlIpv4Port.bend new file mode 100644 index 0000000..a75b59b --- /dev/null +++ b/proofs/UrlIpv4Port.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlIpv4Port.bend +import Base +import ../laws/UrlIpv4Port.bend as Laws + +def Laws.url_ipv4_port(): + {==} diff --git a/proofs/UrlNeedsSlashes.bend b/proofs/UrlNeedsSlashes.bend new file mode 100644 index 0000000..231a41a --- /dev/null +++ b/proofs/UrlNeedsSlashes.bend @@ -0,0 +1,10 @@ +# proofs of laws/UrlNeedsSlashes.bend +import Base +import ../src/url.bend as Url +import ../laws/UrlNeedsSlashes.bend as Laws + +def Laws.parse_needs_slashes(scheme, rest, e): + %e : {Url.parse.body(scheme, rest, _) + == Url.Bad{scheme ++ ":" ++ rest ++ " has no //"} + : Url.Loc} + {==} diff --git a/proofs/UrlPathIsAbsolute.bend b/proofs/UrlPathIsAbsolute.bend new file mode 100644 index 0000000..a32ec02 --- /dev/null +++ b/proofs/UrlPathIsAbsolute.bend @@ -0,0 +1,11 @@ +# proofs of laws/UrlPathIsAbsolute.bend +import Base +import ../laws/UrlPathIsAbsolute.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.path_absolute(ps): + match ps: + case []: + {==} + case h <> t: + M1.eq.starts_nil(String.join(t, "/")) diff --git a/proofs/UrlQueryDropsFragment.bend b/proofs/UrlQueryDropsFragment.bend new file mode 100644 index 0000000..f78e9c3 --- /dev/null +++ b/proofs/UrlQueryDropsFragment.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlQueryDropsFragment.bend +import Base +import ../laws/UrlQueryDropsFragment.bend as Laws + +def Laws.url_query_drops_fragment(): + {==} diff --git a/proofs/UrlQueryKeepsQuestion.bend b/proofs/UrlQueryKeepsQuestion.bend new file mode 100644 index 0000000..8cd1d2c --- /dev/null +++ b/proofs/UrlQueryKeepsQuestion.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlQueryKeepsQuestion.bend +import Base +import ../laws/UrlQueryKeepsQuestion.bend as Laws + +def Laws.url_query_keeps_question(): + {==} diff --git a/proofs/UrlQueryOnEmptyPath.bend b/proofs/UrlQueryOnEmptyPath.bend new file mode 100644 index 0000000..085e98a --- /dev/null +++ b/proofs/UrlQueryOnEmptyPath.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlQueryOnEmptyPath.bend +import Base +import ../laws/UrlQueryOnEmptyPath.bend as Laws + +def Laws.url_query_on_empty_path(): + {==} diff --git a/proofs/UrlSchemeLowercased.bend b/proofs/UrlSchemeLowercased.bend new file mode 100644 index 0000000..8560b5c --- /dev/null +++ b/proofs/UrlSchemeLowercased.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlSchemeLowercased.bend +import Base +import ../laws/UrlSchemeLowercased.bend as Laws + +def Laws.url_scheme_lowercased(): + {==} diff --git a/proofs/UrlShowBadIsMarked.bend b/proofs/UrlShowBadIsMarked.bend new file mode 100644 index 0000000..2c6b99e --- /dev/null +++ b/proofs/UrlShowBadIsMarked.bend @@ -0,0 +1,7 @@ +# proofs of laws/UrlShowBadIsMarked.bend +import Base +import ../laws/UrlShowBadIsMarked.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.url_show_bad_is_marked(why): + M1.eq.starts_nil(why) diff --git a/proofs/UrlShowOpensWithScheme.bend b/proofs/UrlShowOpensWithScheme.bend new file mode 100644 index 0000000..4fac92e --- /dev/null +++ b/proofs/UrlShowOpensWithScheme.bend @@ -0,0 +1,7 @@ +# proofs of laws/UrlShowOpensWithScheme.bend +import Base +import ../laws/UrlShowOpensWithScheme.bend as Laws +import ./lemmas/BaseEqualities.bend as M1 + +def Laws.url_show_opens_with_scheme(scheme, host, port, path): + M1.eq.starts_append(scheme, " " ++ host ++ " " ++ U32.show(port) ++ " " ++ path) diff --git a/proofs/UrlTrailingSlash.bend b/proofs/UrlTrailingSlash.bend new file mode 100644 index 0000000..e359be4 --- /dev/null +++ b/proofs/UrlTrailingSlash.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlTrailingSlash.bend +import Base +import ../laws/UrlTrailingSlash.bend as Laws + +def Laws.url_trailing_slash(): + {==} diff --git a/proofs/UrlUserinfoLastAt.bend b/proofs/UrlUserinfoLastAt.bend new file mode 100644 index 0000000..8285f96 --- /dev/null +++ b/proofs/UrlUserinfoLastAt.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlUserinfoLastAt.bend +import Base +import ../laws/UrlUserinfoLastAt.bend as Laws + +def Laws.url_userinfo_last_at(): + {==} diff --git a/proofs/UrlUserinfoStripped.bend b/proofs/UrlUserinfoStripped.bend new file mode 100644 index 0000000..0ad4e77 --- /dev/null +++ b/proofs/UrlUserinfoStripped.bend @@ -0,0 +1,6 @@ +# proofs of laws/UrlUserinfoStripped.bend +import Base +import ../laws/UrlUserinfoStripped.bend as Laws + +def Laws.url_userinfo_stripped(): + {==} diff --git a/proofs/UrlencodeJoinsPairs.bend b/proofs/UrlencodeJoinsPairs.bend new file mode 100644 index 0000000..074b04e --- /dev/null +++ b/proofs/UrlencodeJoinsPairs.bend @@ -0,0 +1,21 @@ +# proofs of laws/UrlencodeJoinsPairs.bend +import Base +import ../src/qs.bend as Qs +import ../laws/UrlencodeJoinsPairs.bend as Laws + +def Laws.qs_ue_nil(_ps, e): + %e : {Qs.urlencode(_) == "" : String} + {==} + +def Laws.qs_ue_cons(_name, _value, _t): + {==} + +def Laws.qs_more_nil(_ps, e): + %e : {Qs.form.more(_) == "" : String} + {==} + +def Laws.qs_more_cons(_name, _value, _t): + {==} + +def Laws.qs_pair(_name, _value): + {==} diff --git a/proofs/Utf8DecodesWithReplacement.bend b/proofs/Utf8DecodesWithReplacement.bend new file mode 100644 index 0000000..01d3899 --- /dev/null +++ b/proofs/Utf8DecodesWithReplacement.bend @@ -0,0 +1,175 @@ +# proofs of laws/Utf8DecodesWithReplacement.bend +import Base +import ../src/qs.bend as Qs +import ../laws/Utf8DecodesWithReplacement.bend as Laws + +def Laws.qs_dec_nil(_st): + {==} + +def Laws.qs_dec_cons(_t, _rest, _st): + {==} + +def Laws.qs_end_idle(_st, e): + %e : {Qs.utf.end(_) == "" : String} + {==} + +def Laws.qs_end_need(_cp, _left, _lo, _hi): + {==} + +def Laws.qs_idle_wide(_c, _k): + {==} + +def Laws.qs_idle_byte(_b, _k): + {==} + +def Laws.qs_lead_on_ascii(_b, _k): + {==} + +def Laws.qs_lead_on_bad(_b, _k): + {==} + +def Laws.qs_lead_on_opens(_b, _k, _cp, _left, _lo, _hi): + {==} + +def Laws.qs_lead_ascii(b, e): + %e : {Qs.lead.ascii(b, _) == Qs.Ascii{} : Qs.Lead} + {==} + +def Laws.qs_lead_low(b, e1, e2): + %e1 : {Qs.lead.ascii(b, _) == Qs.Bad{} : Qs.Lead} + %e2 : {Qs.lead.low(b, _) == Qs.Bad{} : Qs.Lead} + {==} + +def Laws.qs_lead_two(b, e1, e2, e3): + %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{U32.and(b, 31), 0n, 128, 191} + : Qs.Lead} + %e2 : {Qs.lead.low(b, _) == Qs.Opens{U32.and(b, 31), 0n, 128, 191} + : Qs.Lead} + %e3 : {Qs.lead.two(b, _) == Qs.Opens{U32.and(b, 31), 0n, 128, 191} + : Qs.Lead} + {==} + +def Laws.qs_lead_e0(b, e1, e2, e3, e4): + %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{0, 1n, 160, 191} : Qs.Lead} + %e2 : {Qs.lead.low(b, _) == Qs.Opens{0, 1n, 160, 191} : Qs.Lead} + %e3 : {Qs.lead.two(b, _) == Qs.Opens{0, 1n, 160, 191} : Qs.Lead} + %e4 : {Qs.lead.e0(b, _) == Qs.Opens{0, 1n, 160, 191} : Qs.Lead} + {==} + +def Laws.qs_lead_e1(b, e1, e2, e3, e4, e5): + %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + %e2 : {Qs.lead.low(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + %e3 : {Qs.lead.two(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + %e4 : {Qs.lead.e0(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + %e5 : {Qs.lead.e1(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + {==} + +def Laws.qs_lead_ed(b, e1, e2, e3, e4, e5, e6): + %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} + %e2 : {Qs.lead.low(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} + %e3 : {Qs.lead.two(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} + %e4 : {Qs.lead.e0(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} + %e5 : {Qs.lead.e1(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} + %e6 : {Qs.lead.ed(b, _) == Qs.Opens{13, 1n, 128, 159} : Qs.Lead} + {==} + +def Laws.qs_lead_ee(b, e1, e2, e3, e4, e5, e6, e7): + %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + %e2 : {Qs.lead.low(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + %e3 : {Qs.lead.two(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + %e4 : {Qs.lead.e0(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + %e5 : {Qs.lead.e1(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + %e6 : {Qs.lead.ed(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + %e7 : {Qs.lead.ee(b, _) == Qs.Opens{U32.and(b, 15), 1n, 128, 191} + : Qs.Lead} + {==} + +def Laws.qs_lead_f0(b, e1, e2, e3, e4, e5, e6, e7, e8): + %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} + %e2 : {Qs.lead.low(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} + %e3 : {Qs.lead.two(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} + %e4 : {Qs.lead.e0(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} + %e5 : {Qs.lead.e1(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} + %e6 : {Qs.lead.ed(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} + %e7 : {Qs.lead.ee(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} + %e8 : {Qs.lead.f0(b, _) == Qs.Opens{0, 2n, 144, 191} : Qs.Lead} + {==} + +def Laws.qs_lead_f1(b, e1, e2, e3, e4, e5, e6, e7, e8, e9): + %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} + : Qs.Lead} + %e2 : {Qs.lead.low(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} + : Qs.Lead} + %e3 : {Qs.lead.two(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} + : Qs.Lead} + %e4 : {Qs.lead.e0(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} + : Qs.Lead} + %e5 : {Qs.lead.e1(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} + : Qs.Lead} + %e6 : {Qs.lead.ed(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} + : Qs.Lead} + %e7 : {Qs.lead.ee(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} + : Qs.Lead} + %e8 : {Qs.lead.f0(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} + : Qs.Lead} + %e9 : {Qs.lead.f1(b, _) == Qs.Opens{U32.and(b, 7), 2n, 128, 191} + : Qs.Lead} + {==} + +def Laws.qs_lead_f4(b, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10): + %e1 : {Qs.lead.ascii(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} + %e2 : {Qs.lead.low(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} + %e3 : {Qs.lead.two(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} + %e4 : {Qs.lead.e0(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} + %e5 : {Qs.lead.e1(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} + %e6 : {Qs.lead.ed(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} + %e7 : {Qs.lead.ee(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} + %e8 : {Qs.lead.f0(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} + %e9 : {Qs.lead.f1(b, _) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} + %e10 : {Qs.lead.f4(_) == Qs.Opens{4, 2n, 128, 143} : Qs.Lead} + {==} + +def Laws.qs_lead_high(b, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10): + %e1 : {Qs.lead.ascii(b, _) == Qs.Bad{} : Qs.Lead} + %e2 : {Qs.lead.low(b, _) == Qs.Bad{} : Qs.Lead} + %e3 : {Qs.lead.two(b, _) == Qs.Bad{} : Qs.Lead} + %e4 : {Qs.lead.e0(b, _) == Qs.Bad{} : Qs.Lead} + %e5 : {Qs.lead.e1(b, _) == Qs.Bad{} : Qs.Lead} + %e6 : {Qs.lead.ed(b, _) == Qs.Bad{} : Qs.Lead} + %e7 : {Qs.lead.ee(b, _) == Qs.Bad{} : Qs.Lead} + %e8 : {Qs.lead.f0(b, _) == Qs.Bad{} : Qs.Lead} + %e9 : {Qs.lead.f1(b, _) == Qs.Bad{} : Qs.Lead} + %e10 : {Qs.lead.f4(_) == Qs.Bad{} : Qs.Lead} + {==} + +def Laws.qs_need_wide(_cp, _left, _lo, _hi, _c, _k): + {==} + +def Laws.qs_need_last(cp, _lo, _hi, b, k, e): + %e : {Qs.utf.byte(cp, 0n, b, k, _) + == SCon{Chr{Qs.utf.add(cp, b)}, k(Qs.Idle{})} : String} + {==} + +def Laws.qs_need_more(cp, l, _lo, _hi, b, k, e): + %e : {Qs.utf.byte(cp, 1n+l, b, k, _) + == k(Qs.Need{Qs.utf.add(cp, b), l, 128, 191}) : String} + {==} + +def Laws.qs_need_miss(cp, left, _lo, _hi, b, k, e): + %e : {Qs.utf.byte(cp, left, b, k, _) + == SCon{Chr{65533}, Qs.utf.lead(b, k, Qs.lead(b))} : String} + {==} + +def Laws.qs_add(_cp, _b): + {==} diff --git a/proofs/WithServesGetOctetsAndContext.bend b/proofs/WithServesGetOctetsAndContext.bend new file mode 100644 index 0000000..7ccdc4c --- /dev/null +++ b/proofs/WithServesGetOctetsAndContext.bend @@ -0,0 +1,6 @@ +# proofs of laws/WithServesGetOctetsAndContext.bend +import Base +import ../laws/WithServesGetOctetsAndContext.bend as Laws + +def Laws.step_octets(_method, _target, _hs, _body): + {==} diff --git a/proofs/lemmas/BaseEqualities.bend b/proofs/lemmas/BaseEqualities.bend new file mode 100644 index 0000000..2e96cc8 --- /dev/null +++ b/proofs/lemmas/BaseEqualities.bend @@ -0,0 +1,161 @@ +# Base facts: a value compared with itself, booleans, starts_with, split +import Base +import ../../laws/SplitAtCharacter.bend as L1 +import ../../laws/defs/CharacterAbsent.bend as D1 + +# --- Base facts --- + +def eq.bool_cmp_self(b: Bool) -> {Bool.cmp(b, b) == EQ{} : Cmp}: + match b: + case False{}: + {==} + case True{}: + {==} + +def eq.word_cmp_self(n: Nat, w: Word(n)) -> {Word.cmp(n, w, w) == EQ{} : Cmp}: + match n: + case 0n: + match w: + case WNil{}: + {==} + case 1n+p: + match w: + case WCon{+b, +t}: + Equal.trans(Cmp, Word.cmp.fin(b, b, Word.cmp(p, t, t)), + Word.cmp.fin(b, b, EQ{}), EQ{}, + Equal.cong(Cmp, Cmp, c => Word.cmp.fin(b, b, c), + Word.cmp(p, t, t), EQ{}, eq.word_cmp_self(p, t)), + eq.bool_cmp_self(b)) + +def eq.u32_cmp_self(u: U32) -> {U32.cmp(u, u) == EQ{} : Cmp}: + match u: + case U32{+w}: + eq.word_cmp_self(32n, w) + +def eq.char_eq_self(c: Char) -> {Char.is_eq(c, c) == True{} : Bool}: + match c: + case Chr{+x}: + Equal.cong(Cmp, Bool, r => Cmp.is_eq(r), U32.cmp(x, x), EQ{}, + eq.u32_cmp_self(x)) + +def eq.bit.emp(b: Bool) -> Type: + match b: + case False{}: + Unit + case True{}: + Empty + +def eq.bit.clash(e: {False{} == True{} : Bool}) -> Empty: + %e : eq.bit.emp(_) + Unit{} + +def eq.bit.gone(a, -A: Kind(a), e: Empty) -> A: + match e: + +def eq.bit.absurd(a, -A: Kind(a), e: {False{} == True{} : Bool}) -> A: + eq.bit.gone(a, A, eq.bit.clash(e)) + +def eq.and.left(u: Bool, v: Bool, e: {Bool.and(u, v) == True{} : Bool}) + -> {u == True{} : Bool}: + match u: + case True{}: + {==} + case False{}: + e + +def eq.and.right(u: Bool, v: Bool, e: {Bool.and(u, v) == True{} : Bool}) + -> {v == True{} : Bool}: + match u: + case True{}: + e + case False{}: + eq.bit.absurd(&2, {v == True{} : Bool}, e) + +def eq.not.true(u: Bool, e: {Bool.not(u) == True{} : Bool}) + -> {u == False{} : Bool}: + match u: + case True{}: + eq.bit.absurd(&2, {True{} == False{} : Bool}, e) + case False{}: + {==} + +def eq.starts_nil(s: String) -> {String.starts_with(s, "") == True{} : Bool}: + match s: + case SNil{}: + {==} + case SCon{h, t}: + {==} + +def eq.starts_append(a: String, b: String) + -> {String.starts_with(String.append(a, b), a) == True{} : Bool}: + match a: + case SNil{}: + eq.starts_nil(b) + case SCon{+h, +t}: + Equal.trans(Bool, + String.starts_with.go.if(t, String.append(t, b), Char.is_eq(h, h)), + String.starts_with.go.if(t, String.append(t, b), True{}), + True{}, + Equal.cong(Bool, Bool, + x => String.starts_with.go.if(t, String.append(t, b), x), + Char.is_eq(h, h), True{}, eq.char_eq_self(h)), + eq.starts_append(t, b)) + +law split_whole.step: + for +h: Char + for +t: String + for +c: Char + for e: {False{} == Char.is_eq(h, c) : Bool} + for ih: {String.split(t, c) == [t] : List<&2, String>} + {String.split(SCon{h, t}, c) == [SCon{h, t}] : List<&2, String>} + +def split_whole.step(h, t, c, e, ih): + %e : {String.split.fin(h, String.split(t, c), _) == [SCon{h, t}] + : List<&2, String>} + Equal.cong(List<&2, String>, List<&2, String>, + r => String.split.fin(h, r, False{}), + String.split(t, c), [t], ih) + +def L1.split_whole(s, c, e): + match s: + case SNil{}: + {==} + case SCon{+h, +t}: + split_whole.step(h, t, c, + Equal.sym(Bool, Char.is_eq(h, c), False{}, + eq.not.true(Char.is_eq(h, c), + eq.and.left(Bool.not(Char.is_eq(h, c)), D1.avoids(t, c), e))), + L1.split_whole(t, c, + eq.and.right(Bool.not(Char.is_eq(h, c)), D1.avoids(t, c), e))) + +law split_cut.step: + for +h: Char + for +t: String + for +c: Char + for r: String + for e: {False{} == Char.is_eq(h, c) : Bool} + for ih: {String.split(String.append(t, SCon{c, r}), c) == t <> String.split(r, c) + : List<&2, String>} + {String.split(String.append(SCon{h, t}, SCon{c, r}), c) + == SCon{h, t} <> String.split(r, c) : List<&2, String>} + +def split_cut.step(h, t, c, r, e, ih): + %e : {String.split.fin(h, String.split(String.append(t, SCon{c, r}), c), _) + == SCon{h, t} <> String.split(r, c) : List<&2, String>} + Equal.cong(List<&2, String>, List<&2, String>, + x => String.split.fin(h, x, False{}), + String.split(String.append(t, SCon{c, r}), c), t <> String.split(r, c), ih) + +def L1.split_cut(s, c, r, e): + match s: + case SNil{}: + Equal.cong(Bool, List<&2, String>, + x => String.split.fin(c, String.split(r, c), x), + Char.is_eq(c, c), True{}, eq.char_eq_self(c)) + case SCon{+h, +t}: + split_cut.step(h, t, c, r, + Equal.sym(Bool, Char.is_eq(h, c), False{}, + eq.not.true(Char.is_eq(h, c), + eq.and.left(Bool.not(Char.is_eq(h, c)), D1.avoids(t, c), e))), + L1.split_cut(t, c, r, + eq.and.right(Bool.not(Char.is_eq(h, c)), D1.avoids(t, c), e))) diff --git a/proofs/lemmas/DropNothing.bend b/proofs/lemmas/DropNothing.bend new file mode 100644 index 0000000..19f513e --- /dev/null +++ b/proofs/lemmas/DropNothing.bend @@ -0,0 +1,9 @@ +# dropping zero characters leaves a text as it is +import Base + +def eq.drop_zero(s: String) -> {String.drop(s, 0n) == s : String}: + match s: + case SNil{}: + {==} + case SCon{h, t}: + {==} diff --git a/proofs/lemmas/HeaderFieldSlots.bend b/proofs/lemmas/HeaderFieldSlots.bend new file mode 100644 index 0000000..a95edde --- /dev/null +++ b/proofs/lemmas/HeaderFieldSlots.bend @@ -0,0 +1,364 @@ +# a request's header fields of one name, slot by slot +import Base +import ../../src/http.bend as Http +import ./BaseEqualities.bend as M1 + +# two fields field.same calls the same are the same +def hd.same_sound(+a: Http.Field, +b: Http.Field, + +e: {True{} == Http.field.same(a, b) : Bool}) -> {a == b : Http.Field}: + match a: + case Http.FHost{}: + match b: + case Http.FHost{}: + {==} + case Http.FAgent{}: + M1.eq.bit.absurd(&2, {Http.FHost{} == Http.FAgent{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAccept{}: + M1.eq.bit.absurd(&2, {Http.FHost{} == Http.FAccept{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FConn{}: + M1.eq.bit.absurd(&2, {Http.FHost{} == Http.FConn{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FClen{}: + M1.eq.bit.absurd(&2, {Http.FHost{} == Http.FClen{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FOther{}: + M1.eq.bit.absurd(&2, {Http.FHost{} == Http.FOther{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAgent{}: + match b: + case Http.FHost{}: + M1.eq.bit.absurd(&2, {Http.FAgent{} == Http.FHost{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAgent{}: + {==} + case Http.FAccept{}: + M1.eq.bit.absurd(&2, {Http.FAgent{} == Http.FAccept{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FConn{}: + M1.eq.bit.absurd(&2, {Http.FAgent{} == Http.FConn{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FClen{}: + M1.eq.bit.absurd(&2, {Http.FAgent{} == Http.FClen{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FOther{}: + M1.eq.bit.absurd(&2, {Http.FAgent{} == Http.FOther{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAccept{}: + match b: + case Http.FHost{}: + M1.eq.bit.absurd(&2, {Http.FAccept{} == Http.FHost{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAgent{}: + M1.eq.bit.absurd(&2, {Http.FAccept{} == Http.FAgent{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAccept{}: + {==} + case Http.FConn{}: + M1.eq.bit.absurd(&2, {Http.FAccept{} == Http.FConn{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FClen{}: + M1.eq.bit.absurd(&2, {Http.FAccept{} == Http.FClen{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FOther{}: + M1.eq.bit.absurd(&2, {Http.FAccept{} == Http.FOther{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FConn{}: + match b: + case Http.FHost{}: + M1.eq.bit.absurd(&2, {Http.FConn{} == Http.FHost{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAgent{}: + M1.eq.bit.absurd(&2, {Http.FConn{} == Http.FAgent{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAccept{}: + M1.eq.bit.absurd(&2, {Http.FConn{} == Http.FAccept{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FConn{}: + {==} + case Http.FClen{}: + M1.eq.bit.absurd(&2, {Http.FConn{} == Http.FClen{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FOther{}: + M1.eq.bit.absurd(&2, {Http.FConn{} == Http.FOther{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FClen{}: + match b: + case Http.FHost{}: + M1.eq.bit.absurd(&2, {Http.FClen{} == Http.FHost{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAgent{}: + M1.eq.bit.absurd(&2, {Http.FClen{} == Http.FAgent{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAccept{}: + M1.eq.bit.absurd(&2, {Http.FClen{} == Http.FAccept{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FConn{}: + M1.eq.bit.absurd(&2, {Http.FClen{} == Http.FConn{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FClen{}: + {==} + case Http.FOther{}: + M1.eq.bit.absurd(&2, {Http.FClen{} == Http.FOther{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FOther{}: + match b: + case Http.FHost{}: + M1.eq.bit.absurd(&2, {Http.FOther{} == Http.FHost{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAgent{}: + M1.eq.bit.absurd(&2, {Http.FOther{} == Http.FAgent{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FAccept{}: + M1.eq.bit.absurd(&2, {Http.FOther{} == Http.FAccept{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FConn{}: + M1.eq.bit.absurd(&2, {Http.FOther{} == Http.FConn{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FClen{}: + M1.eq.bit.absurd(&2, {Http.FOther{} == Http.FClen{} : Http.Field}, + Equal.sym(Bool, True{}, False{}, e)) + case Http.FOther{}: + {==} + +# one step of of_app: the head kept or not +def hd.of_app.put(+f: Http.Field, +k: Bool, +h: Http.Header, +t: List<&2, + Http.Header>, +b: List<&2, Http.Header>, + +ih: {Http.fields.of(f, List.append(&2, Http.Header, t, b)) + == List.append(&2, Http.Header, Http.fields.of(f, t), + Http.fields.of(f, b)) : List<&2, Http.Header>}) + -> {Http.fields.put(k, h, + Http.fields.of(f, List.append(&2, Http.Header, t, b))) + == List.append(&2, Http.Header, + Http.fields.put(k, h, Http.fields.of(f, t)), + Http.fields.of(f, b)) : List<&2, Http.Header>}: + match k: + case True{}: + Equal.cong(List<&2, Http.Header>, List<&2, Http.Header>, x => h <> x, + Http.fields.of(f, List.append(&2, Http.Header, t, b)), + List.append(&2, Http.Header, Http.fields.of(f, t), + Http.fields.of(f, b)), ih) + case False{}: + ih + +# the fields of a name in an append are the ones in each part +def hd.of_app(+f: Http.Field, +a: List<&2, Http.Header>, +b: List<&2, + Http.Header>) + -> {Http.fields.of(f, List.append(&2, Http.Header, a, b)) + == List.append(&2, Http.Header, Http.fields.of(f, a), + Http.fields.of(f, b)) : List<&2, Http.Header>}: + match a: + case []: + {==} + case h <> t: + hd.of_app.put(f, Http.field.same(f, Http.header.field(h)), h, t, b, + hd.of_app(f, t, b)) + +# of_app, the other way round +def hd.of_app.r(+f: Http.Field, +a: List<&2, Http.Header>, +b: List<&2, + Http.Header>) + -> {List.append(&2, Http.Header, Http.fields.of(f, a), Http.fields.of(f, b)) + == Http.fields.of(f, List.append(&2, Http.Header, a, b)) : List<&2, + Http.Header>}: + Equal.sym(List<&2, Http.Header>, + Http.fields.of(f, List.append(&2, Http.Header, a, b)), + List.append(&2, Http.Header, Http.fields.of(f, a), Http.fields.of(f, b)), + hd.of_app(f, a, b)) + +# one step of disj +def hd.disj.put(+f: Http.Field, +g: Http.Field, + +efg: {False{} == Http.field.same(f, g) : Bool}, +k: Bool, +h: Http.Header, + +t: List<&2, Http.Header>, + +ek: {k == Http.field.same(g, Http.header.field(h)) : Bool}, + +ih: {Http.fields.of(f, Http.fields.of(g, t)) == [] : List<&2, Http.Header>}) + -> {Http.fields.of(f, Http.fields.put(k, h, Http.fields.of(g, t))) + == [] : List<&2, Http.Header>}: + match k: + case True{}: + %Equal.trans(Bool, False{}, Http.field.same(f, g), + Http.field.same(f, Http.header.field(h)), efg, + Equal.cong(Http.Field, Bool, y => Http.field.same(f, y), g, + Http.header.field(h), hd.same_sound(g, Http.header.field(h), ek))) + : {Http.fields.put(_, h, Http.fields.of(f, Http.fields.of(g, t))) + == [] : List<&2, Http.Header>} + ih + case False{}: + ih + +# the fields of one name among the fields of another are none +def hd.disj(+f: Http.Field, +g: Http.Field, + +efg: {False{} == Http.field.same(f, g) : Bool}, +hs: List<&2, Http.Header>) + -> {Http.fields.of(f, Http.fields.of(g, hs)) == [] : List<&2, Http.Header>}: + match hs: + case []: + {==} + case h <> t: + hd.disj.put(f, g, efg, Http.field.same(g, Http.header.field(h)), h, t, + {==}, hd.disj(f, g, efg, t)) + +# a slot holds none of a name when neither the caller's list nor the default +# does +def hd.or_gone(+f: Http.Field, +given: List<&2, Http.Header>, +d: List<&2, + Http.Header>, + +eg: {Http.fields.of(f, given) == [] : List<&2, Http.Header>}, + +ed: {Http.fields.of(f, d) == [] : List<&2, Http.Header>}) + -> {[] == Http.fields.of(f, Http.fields.or(given, d)) : List<&2, + Http.Header>}: + match given: + case []: + Equal.sym(List<&2, Http.Header>, Http.fields.of(f, d), [], ed) + case h <> t: + Equal.sym(List<&2, Http.Header>, Http.fields.of(f, h <> t), [], eg) + +# a computed Content-Length is a Content-Length field +def hd.clen_self(+n: Nat, +e: Bool) + -> {Http.clen.field(n, e) + == Http.fields.of(Http.FClen{}, Http.clen.field(n, e)) : List<&2, + Http.Header>}: + match e: + case True{}: + {==} + case False{}: + {==} + +# the fields of a name in a request's fields, slot by slot +def hd.of_fields(+f: Http.Field, +s1: List<&2, Http.Header>, +s2: List<&2, + Http.Header>, +s3: List<&2, Http.Header>, +o: List<&2, Http.Header>, + +c: List<&2, Http.Header>, + +r1: List<&2, Http.Header>, +r2: List<&2, Http.Header>, +r3: List<&2, + Http.Header>, +ro: List<&2, Http.Header>, +rc: List<&2, Http.Header>, + +e1: {r1 == Http.fields.of(f, s1) : List<&2, Http.Header>}, + +e2: {r2 == Http.fields.of(f, s2) : List<&2, Http.Header>}, + +e3: {r3 == Http.fields.of(f, s3) : List<&2, Http.Header>}, + +eo: {ro == Http.fields.of(f, o) : List<&2, Http.Header>}, + +ec: {rc == Http.fields.of(f, c) : List<&2, Http.Header>}) + -> {Http.fields.of(f, + List.append(&2, Http.Header, s1, + List.append(&2, Http.Header, s2, + List.append(&2, Http.Header, s3, Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, o, c))))) + == List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>}: + %hd.of_app.r(f, s1, + List.append(&2, Http.Header, s2, + List.append(&2, Http.Header, s3, Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, o, c)))) : {_ + == List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} + %hd.of_app.r(f, s2, + List.append(&2, Http.Header, s3, Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, o, c))) : {List.append(&2, Http.Header, + Http.fields.of(f, s1), _) + == List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} + %hd.of_app.r(f, s3, Http.H{"Connection", "close"} + <> List.append(&2, Http.Header, o, c)) + : {List.append(&2, Http.Header, Http.fields.of(f, s1), + List.append(&2, Http.Header, Http.fields.of(f, s2), _)) + == List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} + %hd.of_app.r(f, o, c) + : {List.append(&2, Http.Header, Http.fields.of(f, s1), + List.append(&2, Http.Header, Http.fields.of(f, s2), + List.append(&2, Http.Header, Http.fields.of(f, s3), + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, _)))) + == List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} + %e1 : {List.append(&2, Http.Header, _, + List.append(&2, Http.Header, Http.fields.of(f, s2), + List.append(&2, Http.Header, Http.fields.of(f, s3), + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, Http.fields.of(f, o), Http.fields.of(f, c)))))) + == List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} + %e2 : {List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, _, + List.append(&2, Http.Header, Http.fields.of(f, s3), + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, Http.fields.of(f, o), Http.fields.of(f, c)))))) + == List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} + %e3 : {List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, _, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, Http.fields.of(f, o), Http.fields.of(f, c)))))) + == List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} + %eo : {List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, _, Http.fields.of(f, c)))))) + == List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} + %ec : {List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, List.append(&2, Http.Header, ro, _))))) + == List.append(&2, Http.Header, r1, + List.append(&2, Http.Header, r2, + List.append(&2, Http.Header, r3, + Http.fields.put(Http.field.same(f, + Http.header.field(Http.H{"Connection", "close"})), + Http.H{"Connection", "close"}, + List.append(&2, Http.Header, ro, rc))))) : List<&2, Http.Header>} + {==} diff --git a/proofs/lemmas/NatOrder.bend b/proofs/lemmas/NatOrder.bend new file mode 100644 index 0000000..73c52f5 --- /dev/null +++ b/proofs/lemmas/NatOrder.bend @@ -0,0 +1,97 @@ +# order and subtraction on natural numbers, and taking all of a text +import Base +import ../../src/http.bend as Http + +law lt_zero: + for n: Nat + {Nat.is_lt(n, 0n) == False{} : Bool} + +def lt_zero(n): + match n: + case 0n: + {==} + case 1n+p: + {==} + +law lt_add: + for a: Nat + for +b: Nat + {Nat.is_lt(Nat.add(a, b), a) == False{} : Bool} + +def lt_add(a, b): + match a: + case 0n: + lt_zero(b) + case 1n+p: + lt_add(p, b) + +law lt_self: + for n: Nat + {Nat.is_lt(n, n) == False{} : Bool} + +def lt_self(n): + match n: + case 0n: + {==} + case 1n+p: + lt_self(p) + +law sub_zero: + for n: Nat + {Nat.sub(n, 0n) == n : Nat} + +def sub_zero(n): + match n: + case 0n: + {==} + case 1n+p: + {==} + +law sub_add: + for a: Nat + for +b: Nat + {Nat.sub(Nat.add(a, b), a) == b : Nat} + +def sub_add(a, b): + match a: + case 0n: + sub_zero(b) + case 1n+p: + sub_add(p, b) + +law take_all: + for +s: String + {Http.octs.take(s, Http.octs.len(s)) == s : String} + +def take_all(s): + match s: + case SNil{}: + {==} + case SCon{+h, +t}: + Equal.trans(String, + Http.octs.take(SCon{h, t}, Http.octs.len(SCon{h, t})), + SCon{h, Http.octs.take(t, Nat.sub( + Nat.add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t)), + Http.octs.width(Char.to_u32(h))))}, + SCon{h, t}, + Equal.cong(Bool, String, + b => Http.octs.take.step(b, h, _u => Http.octs.take(t, Nat.sub( + Nat.add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t)), + Http.octs.width(Char.to_u32(h))))), + Nat.is_lt(Nat.add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t)), + Http.octs.width(Char.to_u32(h))), + False{}, + lt_add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t))), + Equal.trans(String, + SCon{h, Http.octs.take(t, Nat.sub( + Nat.add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t)), + Http.octs.width(Char.to_u32(h))))}, + SCon{h, Http.octs.take(t, Http.octs.len(t))}, + SCon{h, t}, + Equal.cong(Nat, String, m => SCon{h, Http.octs.take(t, m)}, + Nat.sub(Nat.add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t)), + Http.octs.width(Char.to_u32(h))), + Http.octs.len(t), + sub_add(Http.octs.width(Char.to_u32(h)), Http.octs.len(t))), + Equal.cong(String, String, x => SCon{h, x}, + Http.octs.take(t, Http.octs.len(t)), t, take_all(t)))) diff --git a/proofs/lemmas/OctetLength.bend b/proofs/lemmas/OctetLength.bend new file mode 100644 index 0000000..cbd853a --- /dev/null +++ b/proofs/lemmas/OctetLength.bend @@ -0,0 +1,12 @@ +# a text's count of octets is its length +import Base +import ../../src/http.bend as Http +import ../../laws/OneOctetPerChar.bend as L1 + +def L1.octs_len(s): + match s: + case SNil{}: + {==} + case SCon{h, t}: + Equal.cong(Nat, Nat, x => 1n+x, Http.octs.len(t), String.length(t), + L1.octs_len(t)) diff --git a/proofs/lemmas/OctetTakeDrop.bend b/proofs/lemmas/OctetTakeDrop.bend new file mode 100644 index 0000000..19616cc --- /dev/null +++ b/proofs/lemmas/OctetTakeDrop.bend @@ -0,0 +1,54 @@ +# octets taken and dropped are String.take and String.drop +import Base +import ../../src/http.bend as Http +import ../../laws/OneOctetPerChar.bend as L1 + +# --- octets after a prefix --- + +# the first |b| characters of b ++ r are b +def take_prefix(b: String, r: String) + -> {String.take(String.append(b, r), String.length(b)) == b : String}: + match b: + case SNil{}: + match r: + case SNil{}: + {==} + case SCon{h, t}: + {==} + case SCon{h, t}: + Equal.cong(String, String, x => SCon{h, x}, + String.take(String.append(t, r), String.length(t)), t, + take_prefix(t, r)) + +def L1.octs_take(s, n): + match s: + case SNil{}: + {==} + case SCon{h, t}: + match n: + case 0n: + {==} + case 1n+k: + match k: + case 0n: + Equal.cong(String, String, x => SCon{h, x}, Http.octs.take(t, 0n), + String.take(t, 0n), L1.octs_take(t, 0n)) + case 1n+j: + Equal.cong(String, String, x => SCon{h, x}, + Http.octs.take(t, 1n+j), String.take(t, 1n+j), + L1.octs_take(t, 1n+j)) + +def L1.octs_drop(s, n): + match s: + case SNil{}: + {==} + case SCon{h, t}: + match n: + case 0n: + {==} + case 1n+k: + match k: + case 0n: + L1.octs_drop(t, 0n) + case 1n+j: + L1.octs_drop(t, 1n+j) diff --git a/proofs/lemmas/WalksOverAppendedText.bend b/proofs/lemmas/WalksOverAppendedText.bend new file mode 100644 index 0000000..712e38a --- /dev/null +++ b/proofs/lemmas/WalksOverAppendedText.bend @@ -0,0 +1,1430 @@ +# text walks (cut, drop, skip, the chunk walk) over appended text +import Base +import ../../src/http.bend as Http +import ../../laws/ReadStateMatchesWholeText.bend as L1 +import ./BaseEqualities.bend as M1 +import ./DropNothing.bend as M2 +import ./NatOrder.bend as M3 + +# --- the chunk walk, read by read --- + +# what a skip leaves: the text after the octets it walked off +def sk.text(s: Http.Skip) -> String: + match s: + case Http.Short{o}: + "" + case Http.Over{l, r}: + r + +# whether a text is at least the skipped octets and j more +def sk.has(s: Http.Skip, +j: Nat) -> Bool: + match s: + case Http.Short{o}: + Http.octs.has("", Nat.add(o, j)) + case Http.Over{l, r}: + Http.octs.has(r, Nat.add(l, j)) + +# the text after the skipped octets and j more +def sk.drop(s: Http.Skip, +j: Nat) -> String: + match s: + case Http.Short{o}: + "" + case Http.Over{l, r}: + Http.octs.drop(r, Nat.add(l, j)) + +# a skip carried on into more text +def sk.then(s: Http.Skip, b: String) -> Http.Skip: + match s: + case Http.Short{o}: + Http.octs.skip(b, o) + case Http.Over{l, r}: + Http.Over{l, r ++ b} + +law width_pos: + for +u: U32 + {Nat.is_lt(0n, Http.octs.width(u)) == True{} : Bool} + +def width_pos(_u): + {==} + +law has_zero: + for s: String + {Http.octs.has(s, 0n) == True{} : Bool} + +def has_zero(s): + match s: + case SNil{}: + {==} + case SCon{+h, t}: + +w = Http.octs.width(Char.to_u32(h)) + %Equal.sym(Bool, Nat.is_lt(0n, w), True{}, width_pos(Char.to_u32(h))) + : {Http.octs.has.step(_, _u => Http.octs.has(t, Nat.sub(0n, w))) + == True{} : Bool} + {==} + +law drop_zero8: + for s: String + {Http.octs.drop(s, 0n) == s : String} + +def drop_zero8(s): + match s: + case SNil{}: + {==} + case SCon{+h, +t}: + +w = Http.octs.width(Char.to_u32(h)) + %Equal.sym(Bool, Nat.is_lt(0n, w), True{}, width_pos(Char.to_u32(h))) + : {Http.octs.drop.step(_, h, t, _u => Http.octs.drop(t, Nat.sub(0n, w))) + == SCon{h, t} : String} + {==} + +law lt_grow: + for n: Nat + for w: Nat + for +j: Nat + for e: {False{} == Nat.is_lt(n, w) : Bool} + {Nat.is_lt(Nat.add(n, j), w) == False{} : Bool} + +def lt_grow(n, w, j, e): + match n w: + case 0n 0n: + M3.lt_zero(j) + case 1n+q 0n: + {==} + case 0n 1n+p: + M1.eq.bit.absurd(&2, {Nat.is_lt(Nat.add(0n, j), 1n+p) == False{} : Bool}, e) + case 1n+q 1n+p: + lt_grow(q, p, j, e) + +law sub_grow: + for n: Nat + for w: Nat + for +j: Nat + for e: {False{} == Nat.is_lt(n, w) : Bool} + {Nat.sub(Nat.add(n, j), w) == Nat.add(Nat.sub(n, w), j) : Nat} + +def sub_grow(n, w, j, e): + match n w: + case 0n 0n: + M3.sub_zero(j) + case 1n+q 0n: + {==} + case 0n 1n+p: + M1.eq.bit.absurd(&2, {Nat.sub(Nat.add(0n, j), 1n+p) + == Nat.add(Nat.sub(0n, 1n+p), j) : Nat}, e) + case 1n+q 1n+p: + sub_grow(q, p, j, e) + +law skip_text.arm: + for -h: Char + for -t: String + for -n: Nat + for -w: Nat + for b: Bool + for ih: {Http.octs.drop(t, Nat.sub(n, w)) + == sk.text(Http.octs.skip(t, Nat.sub(n, w))) : String} + {Http.octs.drop.step(b, h, t, _u => Http.octs.drop(t, Nat.sub(n, w))) + == sk.text(Http.octs.skip.step(b, n, h, t, + _u => Http.octs.skip(t, Nat.sub(n, w)))) : String} + +def skip_text.arm(_h, _t, _n, _w, b, ih): + match b: + case True{}: + {==} + case False{}: + ih + +law skip_text: + for s: String + for +n: Nat + {Http.octs.drop(s, n) == sk.text(Http.octs.skip(s, n)) : String} + +def skip_text(s, n): + match s: + case SNil{}: + {==} + case SCon{+h, +t}: + +w = Http.octs.width(Char.to_u32(h)) + skip_text.arm(h, t, n, w, Nat.is_lt(n, w), skip_text(t, Nat.sub(n, w))) + +law skip_has.arm: + for +h: Char + for +t: String + for +n: Nat + for +j: Nat + for b: Bool + for +e: {b == Nat.is_lt(n, Http.octs.width(Char.to_u32(h))) : Bool} + for ih: {Http.octs.has(t, Nat.add(Nat.sub(n, Http.octs.width(Char.to_u32(h))), j)) + == sk.has(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : Bool} + {Http.octs.has(SCon{h, t}, Nat.add(n, j)) + == sk.has(Http.octs.skip.step(b, n, h, t, + _u => Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))))), j) : Bool} + +def skip_has.arm(h, t, n, j, b, e, ih): + match b: + case True{}: + {==} + case False{}: + %Equal.sym(Bool, Nat.is_lt(Nat.add(n, j), Http.octs.width(Char.to_u32(h))), False{}, lt_grow(n, + Http.octs.width(Char.to_u32(h)), j, e)) + : {Http.octs.has.step(_, + _u => Http.octs.has(t, Nat.sub(Nat.add(n, j), Http.octs.width(Char.to_u32(h))))) + == sk.has(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : Bool} + %Equal.sym(Nat, Nat.sub(Nat.add(n, j), Http.octs.width(Char.to_u32(h))), Nat.add(Nat.sub(n, + Http.octs.width(Char.to_u32(h))), j), + sub_grow(n, Http.octs.width(Char.to_u32(h)), j, e)) + : {Http.octs.has(t, _) + == sk.has(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : Bool} + ih + +law skip_has: + for s: String + for +n: Nat + for +j: Nat + {Http.octs.has(s, Nat.add(n, j)) == sk.has(Http.octs.skip(s, n), j) : Bool} + +def skip_has(s, n, j): + match s: + case SNil{}: + {==} + case SCon{+h, +t}: + skip_has.arm(h, t, n, j, Nat.is_lt(n, Http.octs.width(Char.to_u32(h))), {==}, + skip_has(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))), j)) + +law skip_drop.arm: + for +h: Char + for +t: String + for +n: Nat + for +j: Nat + for b: Bool + for +e: {b == Nat.is_lt(n, Http.octs.width(Char.to_u32(h))) : Bool} + for ih: {Http.octs.drop(t, Nat.add(Nat.sub(n, Http.octs.width(Char.to_u32(h))), j)) + == sk.drop(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : String} + {Http.octs.drop(SCon{h, t}, Nat.add(n, j)) + == sk.drop(Http.octs.skip.step(b, n, h, t, + _u => Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))))), j) : String} + +def skip_drop.arm(h, t, n, j, b, e, ih): + match b: + case True{}: + {==} + case False{}: + %Equal.sym(Bool, Nat.is_lt(Nat.add(n, j), Http.octs.width(Char.to_u32(h))), False{}, lt_grow(n, + Http.octs.width(Char.to_u32(h)), j, e)) + : {Http.octs.drop.step(_, h, t, + _u => Http.octs.drop(t, Nat.sub(Nat.add(n, j), Http.octs.width(Char.to_u32(h))))) + == sk.drop(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : String} + %Equal.sym(Nat, Nat.sub(Nat.add(n, j), Http.octs.width(Char.to_u32(h))), Nat.add(Nat.sub(n, + Http.octs.width(Char.to_u32(h))), j), + sub_grow(n, Http.octs.width(Char.to_u32(h)), j, e)) + : {Http.octs.drop(t, _) + == sk.drop(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), j) : String} + ih + +law skip_drop: + for s: String + for +n: Nat + for +j: Nat + {Http.octs.drop(s, Nat.add(n, j)) == sk.drop(Http.octs.skip(s, n), j) + : String} + +def skip_drop(s, n, j): + match s: + case SNil{}: + {==} + case SCon{+h, +t}: + skip_drop.arm(h, t, n, j, Nat.is_lt(n, Http.octs.width(Char.to_u32(h))), {==}, + skip_drop(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))), j)) + +law skip_app.arm: + for +h: Char + for +t: String + for +b: String + for +n: Nat + for c: Bool + for ih: {Http.octs.skip(t ++ b, Nat.sub(n, Http.octs.width(Char.to_u32(h)))) + == sk.then(Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h)))), b) : Http.Skip} + {Http.octs.skip.step(c, n, h, t ++ b, + _u => Http.octs.skip(t ++ b, Nat.sub(n, Http.octs.width(Char.to_u32(h))))) + == sk.then(Http.octs.skip.step(c, n, h, t, + _u => Http.octs.skip(t, Nat.sub(n, Http.octs.width(Char.to_u32(h))))), b) : Http.Skip} + +def skip_app.arm(_h, _t, _b, _n, c, ih): + match c: + case True{}: + {==} + case False{}: + ih + +law skip_app: + for s: String + for +b: String + for +n: Nat + {Http.octs.skip(s ++ b, n) == sk.then(Http.octs.skip(s, n), b) : Http.Skip} + +def skip_app(s, b, n): + match s: + case SNil{}: + {==} + case SCon{+h, +t}: + skip_app.arm(h, t, b, n, Nat.is_lt(n, Http.octs.width(Char.to_u32(h))), skip_app(t, b, Nat.sub(n, + Http.octs.width(Char.to_u32(h))))) + +law has_app.arm: + for +h: Char + for +t: String + for +b: String + for +k: Nat + for c: Bool + for e: {True{} == Http.octs.has.step(c, _u => Http.octs.has(t, Nat.sub(k, Http.octs.width(Char.to_u32(h))))) + : Bool} + for ih: @e2: {True{} == Http.octs.has(t, Nat.sub(k, Http.octs.width(Char.to_u32(h)))) : Bool} -> + {Http.octs.has(t ++ b, Nat.sub(k, Http.octs.width(Char.to_u32(h)))) == True{} : Bool} + {Http.octs.has.step(c, _u => Http.octs.has(t ++ b, Nat.sub(k, Http.octs.width(Char.to_u32(h))))) + == True{} : Bool} + +def has_app.arm(_h, _t, _b, _k, c, e, ih): + match c: + case True{}: + {==} + case False{}: + ih(e) + +law has_app: + for s: String + for +b: String + for +k: Nat + for e: {True{} == Http.octs.has(s, k) : Bool} + {Http.octs.has(s ++ b, k) == True{} : Bool} + +def has_app(s, b, k, e): + match s k: + case SNil{} 0n: + has_zero(b) + case SNil{} 1n+p: + M1.eq.bit.absurd(&2, {Http.octs.has(b, 1n+p) == True{} : Bool}, + Equal.sym(Bool, True{}, False{}, e)) + case SCon{+h, +t} _: + has_app.arm(h, t, b, k, Nat.is_lt(k, Http.octs.width(Char.to_u32(h))), e, + e2 => has_app(t, b, Nat.sub(k, Http.octs.width(Char.to_u32(h))), e2)) + +law drop_app.arm: + for +h: Char + for +t: String + for +b: String + for +k: Nat + for c: Bool + for e: {True{} == Http.octs.has.step(c, _u => Http.octs.has(t, Nat.sub(k, Http.octs.width(Char.to_u32(h))))) + : Bool} + for ih: @e2: {True{} == Http.octs.has(t, Nat.sub(k, Http.octs.width(Char.to_u32(h)))) : Bool} -> + {Http.octs.drop(t ++ b, Nat.sub(k, Http.octs.width(Char.to_u32(h)))) == Http.octs.drop(t, Nat.sub(k, + Http.octs.width(Char.to_u32(h)))) ++ b + : String} + {Http.octs.drop.step(c, h, t ++ b, _u => Http.octs.drop(t ++ b, Nat.sub(k, Http.octs.width(Char.to_u32(h))))) + == Http.octs.drop.step(c, h, t, _u => Http.octs.drop(t, Nat.sub(k, Http.octs.width(Char.to_u32(h))))) ++ b + : String} + +def drop_app.arm(_h, _t, _b, _k, c, e, ih): + match c: + case True{}: + {==} + case False{}: + ih(e) + +law drop_app: + for s: String + for +b: String + for +k: Nat + for e: {True{} == Http.octs.has(s, k) : Bool} + {Http.octs.drop(s ++ b, k) == Http.octs.drop(s, k) ++ b : String} + +def drop_app(s, b, k, e): + match s k: + case SNil{} 0n: + drop_zero8(b) + case SNil{} 1n+p: + M1.eq.bit.absurd(&2, {Http.octs.drop(b, 1n+p) == b : String}, + Equal.sym(Bool, True{}, False{}, e)) + case SCon{+h, +t} _: + drop_app.arm(h, t, b, k, Nat.is_lt(k, Http.octs.width(Char.to_u32(h))), e, + e2 => drop_app(t, b, Nat.sub(k, Http.octs.width(Char.to_u32(h))), e2)) + +law sw_app.arm: + for +xt: String + for +yt: String + for +b: String + for c: Bool + for e: {True{} == String.starts_with.go.if(yt, xt, c) : Bool} + for ih: @e2: {True{} == String.starts_with.go(yt, xt) : Bool} -> + {String.starts_with.go(yt, xt ++ b) == True{} : Bool} + {String.starts_with.go.if(yt, xt ++ b, c) == True{} : Bool} + +def sw_app.arm(_xt, _yt, _b, c, e, ih): + match c: + case True{}: + ih(e) + case False{}: + M1.eq.bit.absurd(&2, {False{} == True{} : Bool}, + Equal.sym(Bool, True{}, False{}, e)) + +law sw_app: + for s: String + for p: String + for +b: String + for e: {True{} == String.starts_with(s, p) : Bool} + {String.starts_with(s ++ b, p) == True{} : Bool} + +def sw_app(s, p, b, e): + match s p: + case _ SNil{}: + {==} + case SNil{} SCon{y, yt}: + M1.eq.bit.absurd(&2, {String.starts_with.go(SCon{y, yt}, b) == True{} : Bool}, + Equal.sym(Bool, True{}, False{}, e)) + case SCon{+x, +xt} SCon{+y, +yt}: + sw_app.arm(xt, yt, b, Char.is_eq(x, y), e, e2 => sw_app(xt, yt, b, e2)) + +law contains_nil: + for s: String + {String.contains(s, "") == True{} : Bool} + +def contains_nil(s): + match s: + case SNil{}: + {==} + case SCon{h, t}: + {==} + +law contains_if: + for +t: String + for +p: String + for c: Bool + for e: {String.contains(t, p) == True{} : Bool} + {String.contains.if(t, p, c) == True{} : Bool} + +def contains_if(_t, _p, c, e): + match c: + case True{}: + {==} + case False{}: + e + +law contains_app.arm: + for +h: Char + for +t: String + for +p: String + for +b: String + for c: Bool + for +ec: {c == String.starts_with(SCon{h, t}, p) : Bool} + for e: {True{} == String.contains.if(t, p, c) : Bool} + for ih: @e2: {True{} == String.contains(t, p) : Bool} -> + {String.contains(t ++ b, p) == True{} : Bool} + {String.contains.if(t ++ b, p, String.starts_with(SCon{h, t ++ b}, p)) + == True{} : Bool} + +def contains_app.arm(h, t, p, b, c, ec, e, ih): + match c: + case True{}: + %Equal.sym(Bool, String.starts_with(SCon{h, t} ++ b, p), True{}, + sw_app(SCon{h, t}, p, b, ec)) + : {String.contains.if(t ++ b, p, _) == True{} : Bool} + {==} + case False{}: + contains_if(t ++ b, p, String.starts_with(SCon{h, t ++ b}, p), ih(e)) + +law contains_app: + for s: String + for +p: String + for +b: String + for e: {True{} == String.contains(s, p) : Bool} + {String.contains(s ++ b, p) == True{} : Bool} + +def contains_app(s, p, b, e): + match s p: + case SNil{} SNil{}: + contains_nil(b) + case SNil{} SCon{y, yt}: + M1.eq.bit.absurd(&2, {String.contains(b, SCon{y, yt}) == True{} : Bool}, + Equal.sym(Bool, True{}, False{}, e)) + case SCon{+h, +t} _: + contains_app.arm(h, t, p, b, String.starts_with(SCon{h, t}, p), {==}, e, + e2 => contains_app(t, p, b, e2)) + +law or_true: + for x: Bool + {Bool.or(x, True{}) == True{} : Bool} + +def or_true(x): + match x: + case True{}: + {==} + case False{}: + {==} + +law or_app: + for x: Bool + for y: Bool + for +x2: Bool + for +y2: Bool + for e: {True{} == Bool.or(x, y) : Bool} + for fx: @ex: {True{} == x : Bool} -> {x2 == True{} : Bool} + for fy: @ey: {True{} == y : Bool} -> {y2 == True{} : Bool} + {Bool.or(x2, y2) == True{} : Bool} + +def or_app(x, _y, x2, y2, e, fx, fy): + match x: + case True{}: + %Equal.sym(Bool, x2, True{}, fx({==})) : {Bool.or(_, y2) == True{} : Bool} + {==} + case False{}: + %Equal.sym(Bool, y2, True{}, fy(e)) + : {Bool.or(x2, _) == True{} : Bool} + or_true(x2) + +law trailer_app: + for +after: String + for +b: String + for e: {True{} == Http.chunks.trailer(after) : Bool} + {Http.chunks.trailer(after ++ b) == True{} : Bool} + +def trailer_app(after, b, e): + or_app(String.starts_with(after, "\r\n"), String.contains(after, "\r\n\r\n"), + String.starts_with(after ++ b, "\r\n"), + String.contains(after ++ b, "\r\n\r\n"), e, + ex => sw_app(after, "\r\n", b, ex), + ey => contains_app(after, "\r\n\r\n", b, ey)) + +# whether a divide found its pattern +def cut.is(c: Http.Cut) -> Bool: + match c: + case Http.NoCut{}: + False{} + case Http.Cut{x, y}: + True{} + +# a divide's second half with more text after it +def cut.app(c: Http.Cut, b: String) -> Http.Cut: + match c: + case Http.NoCut{}: + Http.NoCut{} + case Http.Cut{x, y}: + Http.Cut{x, y ++ b} + +law cut_is_push: + for h: Char + for r: Http.Cut + {cut.is(Http.divide.push(h, r)) == cut.is(r) : Bool} + +def cut_is_push(_h, r): + match r: + case Http.NoCut{}: + {==} + case Http.Cut{x, y}: + {==} + +law cut_app_push: + for h: Char + for r: Http.Cut + for +b: String + {cut.app(Http.divide.push(h, r), b) == Http.divide.push(h, cut.app(r, b)) + : Http.Cut} + +def cut_app_push(_h, r, _b): + match r: + case Http.NoCut{}: + {==} + case Http.Cut{x, y}: + {==} + +law len_app: + for a: String + for +b: String + {String.length(a ++ b) == Nat.add(String.length(a), String.length(b)) : Nat} + +def len_app(a, b): + match a: + case SNil{}: + {==} + case SCon{+h, +t}: + Equal.cong(Nat, Nat, x => 1n+x, String.length(t ++ b), + Nat.add(String.length(t), String.length(b)), len_app(t, b)) + +law lt_succ: + for l: Nat + for p: Nat + for e: {False{} == Nat.is_lt(l, p) : Bool} + {Nat.is_lt(1n+l, p) == False{} : Bool} + +def lt_succ(l, p, e): + match l p: + case _ 0n: + {==} + case 0n 1n+q: + M1.eq.bit.absurd(&2, {Nat.is_lt(1n, 1n+q) == False{} : Bool}, e) + case 1n+k 1n+q: + lt_succ(k, q, e) + +law sw_len.arm: + for +xt: String + for +yt: String + for c: Bool + for e: {True{} == String.starts_with.go.if(yt, xt, c) : Bool} + for ih: @e2: {True{} == String.starts_with.go(yt, xt) : Bool} -> + {Nat.is_lt(String.length(xt), String.length(yt)) == False{} : Bool} + {Nat.is_lt(String.length(xt), String.length(yt)) == False{} : Bool} + +def sw_len.arm(xt, yt, c, e, ih): + match c: + case True{}: + ih(e) + case False{}: + M1.eq.bit.absurd(&2, {Nat.is_lt(String.length(xt), String.length(yt)) + == False{} : Bool}, Equal.sym(Bool, True{}, False{}, e)) + +law sw_len: + for s: String + for p: String + for e: {True{} == String.starts_with(s, p) : Bool} + {Nat.is_lt(String.length(s), String.length(p)) == False{} : Bool} + +def sw_len(s, p, e): + match s p: + case _ SNil{}: + M3.lt_zero(String.length(s)) + case SNil{} SCon{y, yt}: + M1.eq.bit.absurd(&2, {Nat.is_lt(0n, 1n+String.length(yt)) == False{} : Bool}, + Equal.sym(Bool, True{}, False{}, e)) + case SCon{+x, +xt} SCon{+y, +yt}: + sw_len.arm(xt, yt, Char.is_eq(x, y), e, e2 => sw_len(xt, yt, e2)) + +law sw_long.arm: + for +xt: String + for +yt: String + for +b: String + for c: Bool + for ih: {String.starts_with.go(yt, xt ++ b) == String.starts_with.go(yt, xt) + : Bool} + {String.starts_with.go.if(yt, xt ++ b, c) + == String.starts_with.go.if(yt, xt, c) : Bool} + +def sw_long.arm(_xt, _yt, _b, c, ih): + match c: + case True{}: + ih + case False{}: + {==} + +law sw_long: + for s: String + for p: String + for +b: String + for e: {False{} == Nat.is_lt(String.length(s), String.length(p)) : Bool} + {String.starts_with(s ++ b, p) == String.starts_with(s, p) : Bool} + +def sw_long(s, p, b, e): + match s p: + case _ SNil{}: + {==} + case SNil{} SCon{y, yt}: + M1.eq.bit.absurd(&2, {String.starts_with(b, SCon{y, yt}) + == String.starts_with(SNil{}, SCon{y, yt}) : Bool}, e) + case SCon{+x, +xt} SCon{+y, +yt}: + sw_long.arm(xt, yt, b, Char.is_eq(x, y), sw_long(xt, yt, b, e)) + +law sdrop_app.arm: + for +xt: String + for +yt: String + for +b: String + for c: Bool + for e: {True{} == String.starts_with.go.if(yt, xt, c) : Bool} + for ih: @e2: {True{} == String.starts_with.go(yt, xt) : Bool} -> + {String.drop(xt ++ b, String.length(yt)) + == String.drop(xt, String.length(yt)) ++ b : String} + {String.drop(xt ++ b, String.length(yt)) + == String.drop(xt, String.length(yt)) ++ b : String} + +def sdrop_app.arm(xt, yt, b, c, e, ih): + match c: + case True{}: + ih(e) + case False{}: + M1.eq.bit.absurd(&2, {String.drop(xt ++ b, String.length(yt)) + == String.drop(xt, String.length(yt)) ++ b : String}, + Equal.sym(Bool, True{}, False{}, e)) + +law sdrop_app: + for s: String + for p: String + for +b: String + for e: {True{} == String.starts_with(s, p) : Bool} + {String.drop(s ++ b, String.length(p)) == String.drop(s, String.length(p)) ++ b + : String} + +def sdrop_app(s, p, b, e): + match s p: + case SNil{} SNil{}: + M2.eq.drop_zero(b) + case SCon{x, xt} SNil{}: + {==} + case SNil{} SCon{y, yt}: + M1.eq.bit.absurd(&2, {String.drop(b, 1n+String.length(yt)) + == String.drop(SNil{}, 1n+String.length(yt)) ++ b : String}, + Equal.sym(Bool, True{}, False{}, e)) + case SCon{+x, +xt} SCon{+y, +yt}: + sdrop_app.arm(xt, yt, b, Char.is_eq(x, y), e, e2 => sdrop_app(xt, yt, b, e2)) + +law cut_len.arm: + for +h: Char + for +t: String + for +p: String + for c: Bool + for +ec: {c == String.starts_with(SCon{h, t}, p) : Bool} + for e: {True{} == cut.is(Http.divide.step(c, h, t, String.length(p), + _u => Http.divide.go(t, p, String.length(p)))) : Bool} + for ih: @e2: {True{} == cut.is(Http.divide.go(t, p, String.length(p))) : Bool} -> + {Nat.is_lt(String.length(t), String.length(p)) == False{} : Bool} + {Nat.is_lt(1n+String.length(t), String.length(p)) == False{} : Bool} + +def cut_len.arm(h, t, p, c, ec, e, ih): + match c: + case True{}: + sw_len(SCon{h, t}, p, ec) + case False{}: + +ih2 = ih(Equal.trans(Bool, True{}, + cut.is(Http.divide.push(h, Http.divide.go(t, p, String.length(p)))), + cut.is(Http.divide.go(t, p, String.length(p))), e, + cut_is_push(h, Http.divide.go(t, p, String.length(p))))) + lt_succ(String.length(t), String.length(p), + Equal.sym(Bool, Nat.is_lt(String.length(t), String.length(p)), False{}, + ih2)) + +law cut_len: + for s: String + for +p: String + for e: {True{} == cut.is(Http.divide.go(s, p, String.length(p))) : Bool} + {Nat.is_lt(String.length(s), String.length(p)) == False{} : Bool} + +def cut_len(s, p, e): + match s: + case SNil{}: + M1.eq.bit.absurd(&2, {Nat.is_lt(0n, String.length(p)) == False{} : Bool}, + Equal.sym(Bool, True{}, False{}, e)) + case SCon{+h, +t}: + cut_len.arm(h, t, p, String.starts_with(SCon{h, t}, p), {==}, e, + e2 => cut_len(t, p, e2)) + +law divide_app.arm: + for +h: Char + for +t: String + for +p: String + for +b: String + for c: Bool + for +ec: {c == String.starts_with(SCon{h, t}, p) : Bool} + for e: {True{} == cut.is(Http.divide.step(c, h, t, String.length(p), + _u => Http.divide.go(t, p, String.length(p)))) : Bool} + for ih: @e2: {True{} == cut.is(Http.divide.go(t, p, String.length(p))) : Bool} -> + {Http.divide.go(t ++ b, p, String.length(p)) + == cut.app(Http.divide.go(t, p, String.length(p)), b) : Http.Cut} + {Http.divide.go(SCon{h, t ++ b}, p, String.length(p)) + == cut.app(Http.divide.step(c, h, t, String.length(p), + _u => Http.divide.go(t, p, String.length(p))), b) : Http.Cut} + +def divide_app.arm(h, t, p, b, c, ec, e, ih): + match c: + case True{}: + %Equal.sym(Bool, String.starts_with(SCon{h, t} ++ b, p), True{}, + sw_app(SCon{h, t}, p, b, ec)) + : {Http.divide.step(_, h, t ++ b, String.length(p), + _u => Http.divide.go(t ++ b, p, String.length(p))) + == Http.Cut{"", String.drop(SCon{h, t}, String.length(p)) ++ b} + : Http.Cut} + Equal.cong(String, Http.Cut, x => Http.Cut{"", x}, + String.drop(SCon{h, t} ++ b, String.length(p)), + String.drop(SCon{h, t}, String.length(p)) ++ b, + sdrop_app(SCon{h, t}, p, b, ec)) + case False{}: + +e2 = Equal.trans(Bool, True{}, + cut.is(Http.divide.push(h, Http.divide.go(t, p, String.length(p)))), + cut.is(Http.divide.go(t, p, String.length(p))), e, + cut_is_push(h, Http.divide.go(t, p, String.length(p)))) + +long = lt_succ(String.length(t), String.length(p), + Equal.sym(Bool, Nat.is_lt(String.length(t), String.length(p)), False{}, + cut_len(t, p, e2))) + +same = Equal.trans(Bool, String.starts_with(SCon{h, t} ++ b, p), + String.starts_with(SCon{h, t}, p), False{}, + sw_long(SCon{h, t}, p, b, Equal.sym(Bool, + Nat.is_lt(1n+String.length(t), String.length(p)), False{}, long)), + Equal.sym(Bool, False{}, String.starts_with(SCon{h, t}, p), ec)) + %Equal.sym(Bool, String.starts_with(SCon{h, t} ++ b, p), False{}, same) + : {Http.divide.step(_, h, t ++ b, String.length(p), + _u => Http.divide.go(t ++ b, p, String.length(p))) + == cut.app(Http.divide.push(h, Http.divide.go(t, p, String.length(p))), + b) : Http.Cut} + Equal.trans(Http.Cut, + Http.divide.push(h, Http.divide.go(t ++ b, p, String.length(p))), + Http.divide.push(h, cut.app(Http.divide.go(t, p, String.length(p)), b)), + cut.app(Http.divide.push(h, Http.divide.go(t, p, String.length(p))), b), + Equal.cong(Http.Cut, Http.Cut, r => Http.divide.push(h, r), + Http.divide.go(t ++ b, p, String.length(p)), + cut.app(Http.divide.go(t, p, String.length(p)), b), ih(e2)), + Equal.sym(Http.Cut, + cut.app(Http.divide.push(h, Http.divide.go(t, p, String.length(p))), b), + Http.divide.push(h, cut.app(Http.divide.go(t, p, String.length(p)), b)), + cut_app_push(h, Http.divide.go(t, p, String.length(p)), b))) + +law divide_app: + for s: String + for +p: String + for +b: String + for e: {True{} == cut.is(Http.divide.go(s, p, String.length(p))) : Bool} + {Http.divide.go(s ++ b, p, String.length(p)) + == cut.app(Http.divide.go(s, p, String.length(p)), b) : Http.Cut} + +def divide_app(s, p, b, e): + match s: + case SNil{}: + M1.eq.bit.absurd(&2, {Http.divide.go(b, p, String.length(p)) + == cut.app(Http.NoCut{}, b) : Http.Cut}, + Equal.sym(Bool, True{}, False{}, e)) + case SCon{+h, +t}: + divide_app.arm(h, t, p, b, String.starts_with(SCon{h, t}, p), {==}, e, + e2 => divide_app(t, p, b, e2)) + +law lt0_add2: + for o: Nat + {Nat.is_lt(0n, Nat.add(o, 2n)) == True{} : Bool} + +def lt0_add2(o): + match o: + case 0n: + {==} + case 1n+p: + {==} + +law walk_done.crlf: + for +g: Nat + for +l: Nat + for +r: String + for +after: String + for +n: Nat + for rd: Bool + for +ed: {Http.octs.drop(after, Nat.add(n, 2n)) + == Http.octs.drop(r, Nat.add(l, 2n)) : String} + for ih: @s: String -> + {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) + : Bool} + {Http.walk.done(Http.walk.crlf(g, l, r, rd, s => Http.walk(g, s))) + == Http.chunks.ended.data(after, n, rd, cut => Http.chunks.ended(g, cut)) : Bool} + +def walk_done.crlf(g, l, r, after, n, rd, ed, ih): + match rd: + case True{}: + %Equal.sym(String, Http.octs.drop(after, Nat.add(n, 2n)), + Http.octs.drop(r, Nat.add(l, 2n)), ed) + : {Http.walk.done(Http.walk(g, Http.octs.drop(r, Nat.add(l, 2n)))) + == Http.chunks.ended(g, Http.divide(_, "\r\n")) : Bool} + ih(Http.octs.drop(r, Nat.add(l, 2n))) + case False{}: + {==} + +law walk_done.data.arm: + for +g: Nat + for +after: String + for +n: Nat + for sk: Http.Skip + for +eh: {Http.octs.has(after, Nat.add(n, 2n)) == sk.has(sk, 2n) : Bool} + for +et: {Http.octs.drop(after, n) == sk.text(sk) : String} + for +ed: {Http.octs.drop(after, Nat.add(n, 2n)) == sk.drop(sk, 2n) : String} + for ih: @s: String -> + {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) + : Bool} + {Http.walk.done(Http.walk.data(g, sk, s => Http.walk(g, s))) + == Http.chunks.ended.data(after, n, Http.chunks.ready(after, n), cut => Http.chunks.ended(g, cut)) + : Bool} + +def walk_done.data.arm(g, after, n, sk, eh, et, ed, ih): + match sk: + case Http.Short{+o}: + +hf = Equal.trans(Bool, Http.octs.has(after, Nat.add(n, 2n)), + Bool.not(Nat.is_lt(0n, Nat.add(o, 2n))), False{}, eh, + Equal.cong(Bool, Bool, x => Bool.not(x), Nat.is_lt(0n, Nat.add(o, 2n)), + True{}, lt0_add2(o))) + %Equal.sym(Bool, Http.octs.has(after, Nat.add(n, 2n)), False{}, hf) + : {False{} == Http.chunks.ended.data(after, n, + Bool.and(_, String.starts_with(Http.octs.drop(after, n), "\r\n")), + cut => Http.chunks.ended(g, cut)) : Bool} + {==} + case Http.Over{+l, +r}: + %Equal.sym(Bool, Http.octs.has(after, Nat.add(n, 2n)), + Http.octs.has(r, Nat.add(l, 2n)), eh) + : {Http.walk.done(Http.walk.crlf(g, l, r, Bool.and( + Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(r, "\r\n")), s => Http.walk(g, s))) + == Http.chunks.ended.data(after, n, + Bool.and(_, String.starts_with(Http.octs.drop(after, n), "\r\n")), + cut => Http.chunks.ended(g, cut)) : Bool} + %Equal.sym(String, Http.octs.drop(after, n), r, et) + : {Http.walk.done(Http.walk.crlf(g, l, r, Bool.and( + Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(r, "\r\n")), s => Http.walk(g, s))) + == Http.chunks.ended.data(after, n, + Bool.and(Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(_, "\r\n")), + cut => Http.chunks.ended(g, cut)) : Bool} + walk_done.crlf(g, l, r, after, n, Bool.and(Http.octs.has(r, Nat.add(l, 2n)), + String.starts_with(r, "\r\n")), ed, ih) + +law walk_done.data: + for +g: Nat + for +after: String + for +n: Nat + for sk: Http.Skip + for +esk: {sk == Http.octs.skip(after, n) : Http.Skip} + for ih: @s: String -> + {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) + : Bool} + {Http.walk.done(Http.walk.data(g, sk, s => Http.walk(g, s))) + == Http.chunks.ended.data(after, n, Http.chunks.ready(after, n), cut => Http.chunks.ended(g, cut)) + : Bool} + +def walk_done.data(g, after, n, sk, esk, ih): + +eh = Equal.trans(Bool, Http.octs.has(after, Nat.add(n, 2n)), + sk.has(Http.octs.skip(after, n), 2n), sk.has(sk, 2n), + skip_has(after, n, 2n), + Equal.cong(Http.Skip, Bool, x => sk.has(x, 2n), Http.octs.skip(after, n), sk, + Equal.sym(Http.Skip, sk, Http.octs.skip(after, n), esk))) + +et = Equal.trans(String, Http.octs.drop(after, n), + sk.text(Http.octs.skip(after, n)), sk.text(sk), + skip_text(after, n), + Equal.cong(Http.Skip, String, x => sk.text(x), Http.octs.skip(after, n), sk, + Equal.sym(Http.Skip, sk, Http.octs.skip(after, n), esk))) + +ed = Equal.trans(String, Http.octs.drop(after, Nat.add(n, 2n)), + sk.drop(Http.octs.skip(after, n), 2n), sk.drop(sk, 2n), + skip_drop(after, n, 2n), + Equal.cong(Http.Skip, String, x => sk.drop(x, 2n), Http.octs.skip(after, n), sk, + Equal.sym(Http.Skip, sk, Http.octs.skip(after, n), esk))) + walk_done.data.arm(g, after, n, sk, eh, et, ed, ih) + +law walk_done.trail: + for g: Nat + for rest: String + for t: Bool + {Http.walk.done(Http.walk.trail(g, rest, t)) == t : Bool} + +def walk_done.trail(_g, _rest, t): + match t: + case True{}: + {==} + case False{}: + {==} + +law walk_done.size: + for +g: Nat + for rest: String + for +after: String + for +n: Nat + for z: Bool + for ih: @s: String -> + {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) + : Bool} + {Http.walk.done(Http.walk.size(g, rest, after, n, z, s => Http.walk(g, s))) + == Http.chunks.ended.size(after, n, z, cut => Http.chunks.ended(g, cut)) : Bool} + +def walk_done.size(g, rest, after, n, z, ih): + match z: + case True{}: + walk_done.trail(g, rest, Http.chunks.trailer(after)) + case False{}: + walk_done.data(g, after, n, Http.octs.skip(after, n), {==}, ih) + +law walk_done.hex: + for +g: Nat + for rest: String + for +before: String + for +after: String + for ok: Bool + for ih: @s: String -> + {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) + : Bool} + {Http.walk.done(Http.walk.hex(g, rest, before, after, ok, s => Http.walk(g, s))) + == Http.chunks.ended.hex(before, after, ok, cut => Http.chunks.ended(g, cut)) : Bool} + +def walk_done.hex(g, rest, before, after, ok, ih): + match ok: + case False{}: + {==} + case True{}: + walk_done.size(g, rest, after, Http.hexlen(before), + Nat.is_eq(Http.hexlen(before), 0n), ih) + +law walk_done.at: + for +g: Nat + for rest: String + for c: Http.Cut + for ih: @s: String -> + {Http.walk.done(Http.walk(g, s)) == Http.chunks.ended(g, Http.divide(s, "\r\n")) + : Bool} + {Http.walk.done(Http.walk.at(g, rest, c, s => Http.walk(g, s))) + == Http.chunks.ended.at(c, cut => Http.chunks.ended(g, cut)) : Bool} + +def walk_done.at(g, rest, c, ih): + match c: + case Http.NoCut{}: + {==} + case Http.Cut{+before, +after}: + walk_done.hex(g, rest, before, after, + Http.hex.well(Http.chunks.token(before)), ih) + +def L1.walk_done(f, rest): + match f: + case 0n: + {==} + case 1n+f2: + +g = f2 + walk_done.at(g, rest, Http.divide(rest, "\r\n"), + s => L1.walk_done(g, s)) + +law wm.ready: + for +g: Nat + for rest: String + for +b: String + for +l: Nat + for +r: String + for +erd: {Bool.and(Http.octs.has(r, Nat.add(l, 2n)), + String.starts_with(r, "\r\n")) == True{} : Bool} + for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.crlf(Nat.add(g, + String.length(b)), l, r ++ b, Bool.and( + Http.octs.has(r ++ b, Nat.add(l, 2n)), String.starts_with(r ++ b, "\r\n")), s => Http.walk(Nat.add(g, + String.length(b)), s)) + : Http.Walk} + for ih: @s: String -> + {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) + : Http.Walk} + {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk(g, Http.octs.drop(r, + Nat.add(l, 2n))), String.length(b), b) + : Http.Walk} + +def wm.ready(g, rest, b, l, r, erd, s1, ih): + +eh = Equal.sym(Bool, Http.octs.has(r, Nat.add(l, 2n)), True{}, + M1.eq.and.left(Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(r, "\r\n"), erd)) + +es = Equal.sym(Bool, String.starts_with(r, "\r\n"), True{}, + M1.eq.and.right(Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(r, "\r\n"), erd)) + +h2 = has_app(r, b, Nat.add(l, 2n), eh) + +s2 = sw_app(r, "\r\n", b, es) + +rd2 = Equal.trans(Bool, Bool.and(Http.octs.has(r ++ b, Nat.add(l, 2n)), + String.starts_with(r ++ b, "\r\n")), + Bool.and(True{}, String.starts_with(r ++ b, "\r\n")), True{}, + Equal.cong(Bool, Bool, x => Bool.and(x, String.starts_with(r ++ b, "\r\n")), + Http.octs.has(r ++ b, Nat.add(l, 2n)), True{}, h2), s2) + Equal.trans(Http.Walk, Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b), + Http.walk.crlf(Nat.add(g, String.length(b)), l, r ++ b, True{}, s => Http.walk(Nat.add(g, String.length(b)), s)), + Http.walk.more(Http.walk(g, Http.octs.drop(r, Nat.add(l, 2n))), String.length(b), b), + Equal.trans(Http.Walk, Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b), + Http.walk.crlf(Nat.add(g, String.length(b)), l, r ++ b, Bool.and( + Http.octs.has(r ++ b, Nat.add(l, 2n)), String.starts_with(r ++ b, "\r\n")), s => Http.walk(Nat.add(g, + String.length(b)), s)), + Http.walk.crlf(Nat.add(g, String.length(b)), l, r ++ b, True{}, s => Http.walk(Nat.add(g, + String.length(b)), s)), s1, + Equal.cong(Bool, Http.Walk, x => Http.walk.crlf(Nat.add(g, String.length(b)), l, r ++ b, x, + s => Http.walk(Nat.add(g, String.length(b)), s)), + Bool.and(Http.octs.has(r ++ b, Nat.add(l, 2n)), + String.starts_with(r ++ b, "\r\n")), True{}, rd2)), + Equal.trans(Http.Walk, + Http.walk(Nat.add(g, String.length(b)), Http.octs.drop(r ++ b, Nat.add(l, 2n))), + Http.walk(Nat.add(g, String.length(b)), Http.octs.drop(r, Nat.add(l, 2n)) ++ b), + Http.walk.more(Http.walk(g, Http.octs.drop(r, Nat.add(l, 2n))), String.length(b), b), + Equal.cong(String, Http.Walk, x => Http.walk(Nat.add(g, String.length(b)), x), + Http.octs.drop(r ++ b, Nat.add(l, 2n)), + Http.octs.drop(r, Nat.add(l, 2n)) ++ b, drop_app(r, b, Nat.add(l, 2n), eh)), + ih(Http.octs.drop(r, Nat.add(l, 2n))))) + +law wm.crlf: + for +g: Nat + for rest: String + for +b: String + for +l: Nat + for +r: String + for rd: Bool + for +erd: {rd == Bool.and(Http.octs.has(r, Nat.add(l, 2n)), + String.starts_with(r, "\r\n")) : Bool} + for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.data(Nat.add(g, + String.length(b)), Http.Over{l, r ++ b}, s => Http.walk(Nat.add(g, String.length(b)), s)) + : Http.Walk} + for ih: @s: String -> + {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) + : Http.Walk} + {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.crlf(g, l, r, rd, + s => Http.walk(g, s)), String.length(b), b) : Http.Walk} + +def wm.crlf(g, rest, b, l, r, rd, erd, s1, ih): + match rd: + case True{}: + wm.ready(g, rest, b, l, r, Equal.sym(Bool, True{}, Bool.and( + Http.octs.has(r, Nat.add(l, 2n)), String.starts_with(r, "\r\n")), erd), s1, ih) + case False{}: + s1 + +law wm.data: + for +g: Nat + for rest: String + for +b: String + for sk: Http.Skip + for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.data(Nat.add(g, + String.length(b)), sk.then(sk, b), s => Http.walk(Nat.add(g, String.length(b)), s)) : Http.Walk} + for ih: @s: String -> + {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) + : Http.Walk} + {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.data(g, sk, + s => Http.walk(g, s)), String.length(b), b) : Http.Walk} + +def wm.data(g, rest, b, sk, s1, ih): + match sk: + case Http.Short{o}: + s1 + case Http.Over{+l, +r}: + wm.crlf(g, rest, b, l, r, Bool.and(Http.octs.has(r, Nat.add(l, 2n)), + String.starts_with(r, "\r\n")), {==}, s1, ih) + +law wm.trail: + for +g: Nat + for +rest: String + for +b: String + for +after: String + for t: Bool + for +et: {t == Http.chunks.trailer(after) : Bool} + for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.trail(Nat.add(g, + String.length(b)), rest ++ b, + Http.chunks.trailer(after ++ b)) : Http.Walk} + {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.trail(g, rest, t), + String.length(b), b) : Http.Walk} + +def wm.trail(g, rest, b, after, t, et, s1): + match t: + case True{}: + Equal.trans(Http.Walk, Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b), + Http.walk.trail(Nat.add(g, String.length(b)), rest ++ b, Http.chunks.trailer(after ++ b)), + Http.Ended{}, s1, + Equal.cong(Bool, Http.Walk, x => Http.walk.trail(Nat.add(g, String.length(b)), rest ++ b, x), + Http.chunks.trailer(after ++ b), True{}, trailer_app(after, b, et))) + case False{}: + {==} + +law wm.size: + for +g: Nat + for +rest: String + for +b: String + for +after: String + for +n: Nat + for z: Bool + for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.size(Nat.add(g, + String.length(b)), rest ++ b, after ++ b, n, z, s => Http.walk(Nat.add(g, String.length(b)), s)) + : Http.Walk} + for ih: @s: String -> + {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) + : Http.Walk} + {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.size(g, rest, after, n, + z, s => Http.walk(g, s)), String.length(b), b) + : Http.Walk} + +def wm.size(g, rest, b, after, n, z, s1, ih): + match z: + case True{}: + wm.trail(g, rest, b, after, Http.chunks.trailer(after), {==}, s1) + case False{}: + wm.data(g, rest, b, Http.octs.skip(after, n), + Equal.trans(Http.Walk, Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b), + Http.walk.data(Nat.add(g, String.length(b)), Http.octs.skip(after ++ b, n), + s => Http.walk(Nat.add(g, String.length(b)), s)), + Http.walk.data(Nat.add(g, String.length(b)), sk.then(Http.octs.skip(after, n), b), + s => Http.walk(Nat.add(g, String.length(b)), s)), s1, + Equal.cong(Http.Skip, Http.Walk, x => Http.walk.data(Nat.add(g, String.length(b)), x, + s => Http.walk(Nat.add(g, String.length(b)), s)), + Http.octs.skip(after ++ b, n), sk.then(Http.octs.skip(after, n), b), + skip_app(after, b, n))), ih) + +law wm.hex: + for +g: Nat + for +rest: String + for +b: String + for +before: String + for +after: String + for ok: Bool + for +s1: {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.hex(Nat.add(g, + String.length(b)), rest ++ b, before, after ++ b, ok, s => Http.walk(Nat.add(g, String.length(b)), s)) + : Http.Walk} + for ih: @s: String -> + {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) + : Http.Walk} + {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.hex(g, rest, before, + after, ok, s => Http.walk(g, s)), String.length(b), b) + : Http.Walk} + +def wm.hex(g, rest, b, before, after, ok, s1, ih): + match ok: + case False{}: + s1 + case True{}: + wm.size(g, rest, b, after, Http.hexlen(before), + Nat.is_eq(Http.hexlen(before), 0n), s1, ih) + +law wm.at: + for +g: Nat + for +rest: String + for +b: String + for c: Http.Cut + for +ec: {c == Http.divide(rest, "\r\n") : Http.Cut} + for ih: @s: String -> + {Http.walk(Nat.add(g, String.length(b)), s ++ b) == Http.walk.more(Http.walk(g, s), String.length(b), b) + : Http.Walk} + {Http.walk(Nat.add(1n+g, String.length(b)), rest ++ b) == Http.walk.more(Http.walk.at(g, rest, c, + s => Http.walk(g, s)), String.length(b), b) : Http.Walk} + +def wm.at(g, rest, b, c, ec, ih): + match c: + case Http.NoCut{}: + {==} + case Http.Cut{+before, +after}: + +cut = Equal.cong(Http.Cut, Bool, x => cut.is(x), Http.Cut{before, after}, + Http.divide(rest, "\r\n"), ec) + +dv = Equal.trans(Http.Cut, Http.divide(rest ++ b, "\r\n"), + cut.app(Http.divide(rest, "\r\n"), b), Http.Cut{before, after ++ b}, + divide_app(rest, "\r\n", b, cut), + Equal.cong(Http.Cut, Http.Cut, x => cut.app(x, b), Http.divide(rest, "\r\n"), + Http.Cut{before, after}, + Equal.sym(Http.Cut, Http.Cut{before, after}, Http.divide(rest, "\r\n"), ec))) + wm.hex(g, rest, b, before, after, Http.hex.well(Http.chunks.token(before)), + Equal.cong(Http.Cut, Http.Walk, x => Http.walk.at(Nat.add(g, String.length(b)), rest ++ b, x, + s => Http.walk(Nat.add(g, String.length(b)), s)), + Http.divide(rest ++ b, "\r\n"), Http.Cut{before, after ++ b}, dv), ih) + +def L1.walk_more(f, rest, b): + match f: + case 0n: + {==} + case 1n+f2: + +g = f2 + wm.at(g, rest, b, Http.divide(rest, "\r\n"), {==}, + s => L1.walk_more(g, s, b)) + +law hd.body: + for f: Http.Frame + for +after: String + {Http.hear.done(Http.hear.body(f, after)) == Http.whole.of(f, after) : Bool} + +def hd.body(f, after): + match f: + case Http.Bare{}: + {==} + case Http.Sized{n}: + {==} + case Http.Chunked{}: + L1.walk_done(String.length(after), after) + case Http.Closed{}: + {==} + +law hd.step: + for +g: Nat + for +asked: Bool + for +ls: List<&2, String> + for +after: String + for i: Bool + for ih: @s: String -> + {Http.hear.done(Http.hear.go(g, asked, s)) + == Http.whole.go(g, asked, Http.divide(s, "\r\n\r\n")) : Bool} + {Http.hear.done(Http.hear.step(asked, ls, after, i, s => Http.hear.go(g, asked, s))) + == Http.whole.step(asked, ls, after, i, cut => Http.whole.go(g, asked, cut)) : Bool} + +def hd.step(_g, asked, ls, after, i, ih): + match i: + case True{}: + ih(after) + case False{}: + hd.body(Http.frame(Bool.or(asked, Http.status.bare(Http.status.of(ls))), + Http.head.go(ls, "transfer-encoding"), + Http.head.values(ls, "content-length")), after) + +law hd.at: + for +g: Nat + for +asked: Bool + for raw: String + for c: Http.Cut + for ih: @s: String -> + {Http.hear.done(Http.hear.go(g, asked, s)) + == Http.whole.go(g, asked, Http.divide(s, "\r\n\r\n")) : Bool} + {Http.hear.done(Http.hear.at(g, asked, raw, c, s => Http.hear.go(g, asked, s))) + == Http.whole.at(asked, c, cut => Http.whole.go(g, asked, cut)) : Bool} + +def hd.at(g, asked, _raw, c, ih): + match c: + case Http.NoCut{}: + {==} + case Http.Cut{before, +after}: + +ls = String.lines(before) + hd.step(g, asked, ls, after, Http.status.interim(Http.status.of(ls)), ih) + +law hd: + for f: Nat + for +asked: Bool + for +raw: String + {Http.hear.done(Http.hear.go(f, asked, raw)) + == Http.whole.go(f, asked, Http.divide(raw, "\r\n\r\n")) : Bool} + +def hd(f, asked, raw): + match f: + case 0n: + {==} + case 1n+f2: + +g = f2 + hd.at(g, asked, raw, Http.divide(raw, "\r\n\r\n"), s => hd(g, asked, s)) + +def L1.hear_done(method, raw): + hd(String.length(raw), String.eq(method, "HEAD"), raw) + +law add_assoc: + for x: Nat + for +y: Nat + for +z: Nat + {Nat.add(Nat.add(x, y), z) == Nat.add(x, Nat.add(y, z)) : Nat} + +def add_assoc(x, y, z): + match x: + case 0n: + {==} + case 1n+p: + Equal.cong(Nat, Nat, k => 1n+k, Nat.add(Nat.add(p, y), z), + Nat.add(p, Nat.add(y, z)), add_assoc(p, y, z)) + +law u8len_app: + for a: String + for +b: String + {Http.octs.len(a ++ b) == Nat.add(Http.octs.len(a), Http.octs.len(b)) : Nat} + +def u8len_app(a, b): + match a: + case SNil{}: + {==} + case SCon{+h, +t}: + +w = Http.octs.width(Char.to_u32(h)) + Equal.trans(Nat, Nat.add(w, Http.octs.len(t ++ b)), + Nat.add(w, Nat.add(Http.octs.len(t), Http.octs.len(b))), + Nat.add(Nat.add(w, Http.octs.len(t)), Http.octs.len(b)), + Equal.cong(Nat, Nat, k => Nat.add(w, k), Http.octs.len(t ++ b), + Nat.add(Http.octs.len(t), Http.octs.len(b)), u8len_app(t, b)), + Equal.sym(Nat, Nat.add(Nat.add(w, Http.octs.len(t)), Http.octs.len(b)), + Nat.add(w, Nat.add(Http.octs.len(t), Http.octs.len(b))), + add_assoc(w, Http.octs.len(t), Http.octs.len(b)))) + +law hm.body: + for f: Http.Frame + for +after: String + for +b: String + {Http.hear.body(f, after ++ b) == Http.hear.more(Http.hear.body(f, after), b) + : Http.Hear} + +def hm.body(f, after, b): + match f: + case Http.Bare{}: + {==} + case Http.Sized{+n}: + Equal.cong(Nat, Http.Hear, k => Http.Count{n, k}, Http.octs.len(after ++ b), + Nat.add(Http.octs.len(after), Http.octs.len(b)), u8len_app(after, b)) + case Http.Chunked{}: + Equal.cong(Http.Walk, Http.Hear, w => Http.Walking{w}, + Http.walk(String.length(after ++ b), after ++ b), + Http.walk.more(Http.walk(String.length(after), after), String.length(b), b), + Equal.trans(Http.Walk, Http.walk(String.length(after ++ b), after ++ b), + Http.walk(Nat.add(String.length(after), String.length(b)), after ++ b), + Http.walk.more(Http.walk(String.length(after), after), String.length(b), b), + Equal.cong(Nat, Http.Walk, k => Http.walk(k, after ++ b), + String.length(after ++ b), Nat.add(String.length(after), String.length(b)), + len_app(after, b)), + L1.walk_more(String.length(after), after, b))) + case Http.Closed{}: + {==} + +law hm.step: + for +g: Nat + for +asked: Bool + for +raw: String + for +b: String + for +ls: List<&2, String> + for +after: String + for i: Bool + for +s1: {Http.hear.go(Nat.add(1n+g, String.length(b)), asked, raw ++ b) == Http.hear.step(asked, ls, + after ++ b, i, s => Http.hear.go(Nat.add(g, String.length(b)), asked, s)) : Http.Hear} + for ih: @s: String -> + {Http.hear.go(Nat.add(g, String.length(b)), asked, s ++ b) + == Http.hear.more(Http.hear.go(g, asked, s), b) : Http.Hear} + {Http.hear.go(Nat.add(1n+g, String.length(b)), asked, raw ++ b) == Http.hear.more(Http.hear.step(asked, ls, + after, i, s => Http.hear.go(g, asked, s)), b) + : Http.Hear} + +def hm.step(g, asked, raw, b, ls, after, i, s1, ih): + match i: + case True{}: + Equal.trans(Http.Hear, Http.hear.go(Nat.add(1n+g, String.length(b)), asked, raw ++ b), + Http.hear.go(Nat.add(g, String.length(b)), asked, after ++ b), + Http.hear.more(Http.hear.go(g, asked, after), b), s1, ih(after)) + case False{}: + +fr = Http.frame(Bool.or(asked, Http.status.bare(Http.status.of(ls))), + Http.head.go(ls, "transfer-encoding"), + Http.head.values(ls, "content-length")) + Equal.trans(Http.Hear, Http.hear.go(Nat.add(1n+g, String.length(b)), asked, raw ++ b), + Http.hear.body(fr, after ++ b), + Http.hear.more(Http.hear.body(fr, after), b), s1, hm.body(fr, after, b)) + +law hm.at: + for +g: Nat + for +asked: Bool + for +raw: String + for +b: String + for c: Http.Cut + for +ec: {c == Http.divide(raw, "\r\n\r\n") : Http.Cut} + for ih: @s: String -> + {Http.hear.go(Nat.add(g, String.length(b)), asked, s ++ b) + == Http.hear.more(Http.hear.go(g, asked, s), b) : Http.Hear} + {Http.hear.go(Nat.add(1n+g, String.length(b)), asked, raw ++ b) == Http.hear.more(Http.hear.at(g, asked, + raw, c, s => Http.hear.go(g, asked, s)), b) : Http.Hear} + +def hm.at(g, asked, raw, b, c, ec, ih): + match c: + case Http.NoCut{}: + {==} + case Http.Cut{+before, +after}: + +cut = Equal.cong(Http.Cut, Bool, x => cut.is(x), Http.Cut{before, after}, + Http.divide(raw, "\r\n\r\n"), ec) + +dv = Equal.trans(Http.Cut, Http.divide(raw ++ b, "\r\n\r\n"), + cut.app(Http.divide(raw, "\r\n\r\n"), b), Http.Cut{before, after ++ b}, + divide_app(raw, "\r\n\r\n", b, cut), + Equal.cong(Http.Cut, Http.Cut, x => cut.app(x, b), Http.divide(raw, "\r\n\r\n"), + Http.Cut{before, after}, + Equal.sym(Http.Cut, Http.Cut{before, after}, Http.divide(raw, "\r\n\r\n"), ec))) + +ls = String.lines(before) + hm.step(g, asked, raw, b, ls, after, Http.status.interim(Http.status.of(ls)), + Equal.cong(Http.Cut, Http.Hear, + x => Http.hear.at(Nat.add(g, String.length(b)), asked, raw ++ b, x, s => Http.hear.go(Nat.add(g, + String.length(b)), asked, s)), + Http.divide(raw ++ b, "\r\n\r\n"), Http.Cut{before, after ++ b}, dv), ih) + +law hm: + for f: Nat + for +asked: Bool + for +raw: String + for +b: String + {Http.hear.go(Nat.add(f, String.length(b)), asked, raw ++ b) + == Http.hear.more(Http.hear.go(f, asked, raw), b) : Http.Hear} + +def hm(f, asked, raw, b): + match f: + case 0n: + {==} + case 1n+f2: + +g = f2 + hm.at(g, asked, raw, b, Http.divide(raw, "\r\n\r\n"), {==}, s => hm(g, asked, s, b)) + +def L1.hear_more(method, a, b): + +asked = String.eq(method, "HEAD") + Equal.sym(Http.Hear, Http.hear.of(method, a ++ b), + Http.hear.more(Http.hear.of(method, a), b), + Equal.trans(Http.Hear, Http.hear.go(String.length(a ++ b), asked, a ++ b), + Http.hear.go(Nat.add(String.length(a), String.length(b)), asked, a ++ b), + Http.hear.more(Http.hear.go(String.length(a), asked, a), b), + Equal.cong(Nat, Http.Hear, k => Http.hear.go(k, asked, a ++ b), + String.length(a ++ b), Nat.add(String.length(a), String.length(b)), len_app(a, b)), + hm(String.length(a), asked, a, b))) diff --git a/src/body.bend b/src/body.bend index 32b4e0d..abc838e 100644 --- a/src/body.bend +++ b/src/body.bend @@ -12,7 +12,7 @@ type Body is Type: Octets{bytes: Bytes.Bytes} # what the request text needs to know of a body: its text, or how many -# octets follow it (SPEC.md HTTP-CLI-10) +# octets follow it (laws/OctetBodiesSentUnchanged.bend) type Shape is Data: ShapeText{text: String} ShapeOctets{n: U32} diff --git a/src/client.bend b/src/client.bend index 6340a97..10fb707 100644 --- a/src/client.bend +++ b/src/client.bend @@ -1,6 +1,41 @@ # ezhttp/src/client: fetch a URL. URL and HTTP parsing joined to the wire effect. # Answers a structured Response (status + headers + body), not a hub-style # "0\n…" string contract. +# +# Trusted, not proved: this module and src/wire.bend act on +# src/wirespec.bend's answers in order. For a `NoUrl` they answer `Err` with +# its reason and open nothing; otherwise they resolve the exchange's host +# (`Dns.resolve.literal.all`, else `Dns.lookup.all`), connect to its +# addresses in turn until one connects, run the TLS handshake with the host +# when the exchange is secure, and send `wire.payload` of the request +# followed by the body's octets (`body.shape`), each call with the deadline +# `wire.ms.of` gives; a failed call is answered `wire.fail` of its phase, +# its error number and its reason (the last address's, for the connect), and +# nothing after it is done. Then, from the state `wire.got(method)` for the +# exchange's method, they receive up to 65536 octets at a time and append +# each receive's octets, packed, to those before it (`Bytes.append`); they +# judge each receive with `wire.read.count` of its count of octets, and only +# when that answers none with `wire.read.next` of a copy of its octets as +# text, dropped after the judging; they read again with the state the +# judging gives back until it answers `Heard`, closing the socket on every +# path, and give up with status 56 after 65536 receives. A failed exchange +# is `Err` with the reason `wire.failed` reads from its answer. Otherwise +# the answer is the octets received, and the response is what `packed.reply` +# reads from them by the exchange's method: a torn one as `Err` with its +# reason, any other as a `Response` with its status, its header fields and +# reason phrase as text (`Qs.octets.headers`, `Qs.octets.text`), and its +# body's octets, packed, as `bytes`; `body_text` (`client.text`) decodes +# them as UTF-8 only when called. Every client function builds its exchange +# with `wire.exchange.body` from the shape of the body it is given. +# `http.request_opts` sends the method, headers and body it is given, and +# `http.post_opts`, `http.put_opts` and `http.delete_opts` are it with +# `POST`, `PUT` and `DELETE` and `follow` unset. `http.timed_out` is false +# for a `Response` and `wire.timed_out` of an `Err`'s reason. It is IO +# sequencing over bend-kit-wire's effects, which no law can state, and this +# module reaches those effects, so no law file can import even its pure +# helpers (`http.answer`, `client.timed_out`). Every decision it acts on is +# src/wirespec.bend's, proved under laws/ (the client's guarantees, such as +# laws/RequestLineAndHost.bend). import Base import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes import ./url.bend as Url @@ -14,15 +49,16 @@ import ./qs.bend as Qs import ./packed.bend as Packed # structured client result: transport/parse error, or an HTTP response: its -# status, its header fields and reason phrase as text (decoded from the octets -# received as UTF-8, SPEC.md HTTP-BODY-4, HTTP-CLI-11), and its body's octets -# as they arrived, packed (`client.text` decodes them when asked) -type Response is Type: # noqa: L001 the wire effect keeps src/client.bend out of law files (HTTP-TRUST-5) +# status, its header fields and reason phrase as text (decoded from the +# octets received as UTF-8, laws/OctetsDecodeAsUtf8.bend, +# laws/ReasonPhraseFromFinalHead.bend), and its body's octets as they +# arrived, packed (`client.text` decodes them when asked) +type Response is Type: # noqa: L001 the wire effect keeps src/client.bend out of law files (trusted above) Err{why: String} Response{status: U32, headers: List<&2, Http.Header>, reason: String, bytes: Bytes.Bytes} -# a body's octets decoded as UTF-8 (SPEC.md HTTP-BODY-4) +# a body's octets decoded as UTF-8 (laws/OctetsDecodeAsUtf8.bend) def client.text(b: Bytes.Bytes) -> String: Qs.octets.text(Bytes.to_string(b)) @@ -122,7 +158,17 @@ def client.after(p: Redirect.Hop & Response, go: String -> IO(Response)) # GET, following redirects while `left` hops remain; `body` goes with this # hop only. The answer is the last response, which is a redirect only when -# the hops ran out or its Location is not one redirect.bend follows +# the hops ran out or its Location is not one redirect.bend follows. +# +# Trusted, not proved: it sends a GET to the URL it is given (with the +# caller's body on the first hop only), asks `redirect.plan` about the +# response with the hops it has left (10 to start), sends the next GET to a +# `Go` target with the same headers and timeout and one hop fewer, and +# answers the response on a `Stop`, on a failed exchange, or when no hops +# are left. It is IO sequencing over the wire effect, which no law can +# state; every decision it acts on is src/redirect.bend's, proved under +# laws/ (the redirect guarantees, such as +# laws/RedirectsFollowLocation.bend). def client.follow(+left: Nat, +url: String, +headers: List<&2, Http.Header>, body: Body.Body, +ms: U32) -> IO(Response): match left: diff --git a/src/echo.bend b/src/echo.bend index 8892c3c..8a6f73a 100644 --- a/src/echo.bend +++ b/src/echo.bend @@ -2,7 +2,8 @@ # Set-Cookie fields, reduced to names and values (RFC 6265 §5.2 parsing, # §5.3 step 11 replacement, §5.4 serialization). No attribute is read: no # Domain, Path, Expires, Max-Age, Secure or HttpOnly, so nothing expires and -# every cookie goes back. Pure. SPEC.md HTTP-CK lists what is guaranteed. +# every cookie goes back. Pure. What is guaranteed is under laws/, such as +# laws/CookieHeaderEchoesStore.bend. import Base import ./http.bend as Http import ./qs.bend as Qs diff --git a/src/entities.bend b/src/entities.bend index 0f188a4..e017ed7 100644 --- a/src/entities.bend +++ b/src/entities.bend @@ -1,6 +1,16 @@ # ezhttp/src/entities: the HTML Standard's named character references # (HTML §13.5, https://html.spec.whatwg.org/entities.json), 2231 names. -# Generated by tools/entities.py; do not edit by hand (SPEC.md HTTP-TRUST-7). +# Generated by tools/entities.py; do not edit by hand. +# +# Trusted, not proved: this is the HTML Standard's named character +# references table: every name in it once, without its leading `&`, with the +# one or two code points it stands for. tools/entities.py generates it from +# the standard's entities.json (https://html.spec.whatwg.org/entities.json, +# the same table as Python's `html.entities.html5`), checking that each +# entry's code points spell its characters; no law reaches that: a law can +# say how the table is read (laws/NamedReferencesLongestMatch.bend), not +# that its rows are the standard's. No test compares html_unescape against +# Python's `html.unescape`. import Base # one named character reference: its name (with its `;` when it has one) diff --git a/src/form.bend b/src/form.bend index ae37c3d..6a59ae9 100644 --- a/src/form.bend +++ b/src/form.bend @@ -3,9 +3,10 @@ # decision is form.plan's, over the parts with each file's content replaced # by whether the delimiter occurs in it; the plan lays the body out as runs # of octets and file contents, and form.build puts them together, so a -# file's bytes reach the wire unchanged (SPEC.md HTTP-CLI-10). The caller -# picks the boundary and the builder refuses one a reader could misread. -# SPEC.md HTTP-FORM lists what is guaranteed. +# file's bytes reach the wire unchanged +# (laws/OctetBodiesSentUnchanged.bend). The caller picks the boundary and +# the builder refuses one a reader could misread. What is guaranteed is +# under laws/, such as laws/FormRefusesBadRequests.bend. import Base import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes import ./body.bend as Body @@ -35,7 +36,7 @@ type Plan is Data: Plan{ctype: String, segs: List<&2, Seg>} # a built form (the Content-Type field value and the body), or why not -type Form is Type: # noqa: L001 it holds Bytes, which no law can compare; form.plan's laws cover it (HTTP-TRUST-8) +type Form is Type: # noqa: L001 it holds Bytes, which no law can compare; form.plan's laws cover it (trusted above) NoForm{why: String} Form{ctype: String, body: Body.Body} @@ -263,11 +264,21 @@ def form.check(+b: String, +ps: List<&2, Part>, ok: Bool) -> Plan: form.check.clash(b, ps, form.clash(b, ps)) # a multipart/form-data body's plan with this boundary and these parts, or -# why not (SPEC.md HTTP-FORM) +# why not (laws/FormRefusesBadRequests.bend) def form.plan(+b: String, ps: List<&2, Part>) -> Plan: form.check(b, ps, form.boundary.ok(b)) -# --- the body (SPEC.md HTTP-TRUST-8) --- +# --- the body --- +# +# Trusted, not proved: `form.body` looks for the delimiter in each file +# part's bytes with `Bytes.find`, plans the body with `form.plan`, and +# answers the plan's refusal, or the body its runs and the files' bytes make +# in order (`form.build`), under the plan's Content-Type. `form.build` puts +# the plan's runs and the files' `Bytes` together with bend-kit-bytes +# (`Bytes.from_string`, `Bytes.concat`, trusted in src/wire.bend); packed +# `Array` words are out of the laws' reach. Every decision (what is +# refused, in which order, and the layout) is `form.plan`'s and is proved +# (laws/FormRefusesBadRequests.bend to laws/FormNamesEscaped.bend). # whether a search found the delimiter, with the bytes handed back def form.found(r: Bytes.Bytes & Maybe<&2, U32>) -> Bytes.Bytes & Bool: @@ -338,6 +349,6 @@ def form.built(+b: String, r: List<&2, Part> & List<&1, Bytes.Bytes>) -> Form: form.build(form.plan(b, ps), fs) # a multipart/form-data body with this boundary and these parts, or why not -# (SPEC.md HTTP-FORM, HTTP-TRUST-8) +# (laws/FormRefusesBadRequests.bend, trusted above) def form.body(+b: String, us: List<&1, Upload>) -> Form: form.built(b, form.split(Body.octets.chars(form.delim(b)), us)) diff --git a/src/html.bend b/src/html.bend index b234885..2710a69 100644 --- a/src/html.bend +++ b/src/html.bend @@ -1,7 +1,7 @@ # ezhttp/src/html: HTML escaping, the way markupsafe.escape and Jinja's # autoescape do it. `&`, `<`, `>`, `"` and `'` are written as `&`, `<`, # `>`, `"` and `'`; every other character is written as itself. -# SPEC.md HTTP-HTML lists what is guaranteed. +# What is guaranteed is under laws/, such as laws/HtmlEscapeCharByChar.bend. import Base # one character that is not `&`, `<`, `>` or `"`, as `'` or as itself diff --git a/src/http.bend b/src/http.bend index 7484c82..c6c1157 100644 --- a/src/http.bend +++ b/src/http.bend @@ -139,8 +139,9 @@ def utf8.len(s: String) -> Nat: case SCon{h, t}: Nat.add(utf8.width(Char.to_u32(h)), utf8.len(t)) -# What is received is octets, one character each (SPEC.md HTTP-BODY-1): the -# octs.* walks below count a character as one octet. +# What is received is octets, one character each +# (laws/OneOctetPerChar.bend): the octs.* walks below count a character as +# one octet. # octets one received character stands for: one def octs.width(+_u: U32) -> Nat: @@ -513,7 +514,7 @@ def clen.line(+n: Nat, empty: Bool) -> String: headers.lines(clen.field(n, empty)) # the fields a request names: the ones ezhttp writes a default for, the ones -# it always computes, and every other (SPEC.md HTTP-CLI-8) +# it always computes, and every other (laws/RequestHeaderFieldsOrdered.bend) type Field is Data: FHost{} FAgent{} @@ -842,7 +843,7 @@ def parse.chunks(status: U32, headers: List<&2, Header>, +body: String) -> Reply parse.chunked(status, headers, body, chunks.scan(String.length(body), divide(body, "\r\n"))) -# --- a response planned from its head (SPEC.md HTTP-CLI-15) --- +# --- a response planned from its head (laws/HeadPlannedBeforeBody.bend) --- # what a final head says of its response, before its body is touched: torn, # or its status, its fields and how its body is framed @@ -997,7 +998,7 @@ def reason(+code: U32) -> String: reason.in(reasons(), code) # the head of a reply whose body is n octets, onto what follows it -# (SPEC.md HTTP-SRV-6) +# (laws/ReplyHeadFramesBody.bend) def respond.head.onto(+status: U32, headers: List<&2, Header>, +n: Nat, rest: String) -> String: "HTTP/1.1 " ++ U32.show(status) ++ " " ++ reason(status) ++ "\r\n" @@ -1029,7 +1030,7 @@ def te.line(te: Bool) -> String: "" # the head of a streamed reply: chunked unless the status has no body -# (RFC 9112 §6.1, §7.1; SPEC.md HTTP-SRV-7) +# (RFC 9112 §6.1, §7.1; laws/StreamedRepliesAreChunked.bend) def respond.stream(+status: U32, headers: List<&2, Header>) -> String: "HTTP/1.1 " ++ U32.show(status) ++ " " ++ reason(status) ++ "\r\n" ++ "Connection: close\r\n" @@ -1066,7 +1067,7 @@ def chunk.frame.of(+n: U32, empty: Bool) -> Chunk: case False{}: Chunk{chunk.line(n)} -# the frame of a chunk of n octets (SPEC.md HTTP-SRV-7) +# the frame of a chunk of n octets (laws/StreamedRepliesAreChunked.bend) def chunk.frame(+n: U32) -> Chunk: chunk.frame.of(n, U32.is_eq(n, 0)) @@ -1478,7 +1479,8 @@ def parse.to(+method: String, +raw: String) -> Reply: parse.to.go(String.length(raw), String.eq(method, "HEAD"), divide(raw, "\r\n\r\n")) -# --- when a response has all arrived, judged read by read (SPEC.md HTTP-CLI-12) --- +# --- when a response has all arrived, judged read by read --- +# laws/ReadStateMatchesWholeText.bend # # The client judges the text it has received after every read. Judging the # whole text each time (whole) costs the whole text each time; the state @@ -1724,8 +1726,9 @@ def walk.more.n(w: Walk, +m: Nat) -> Walk: Tail{fuel, left, rest} # whether a state can be advanced by a count of octets alone, without their -# text: a whole response, a body framed by the close or by Content-Length, or -# a chunk's data that still owes at least that many (SPEC.md HTTP-CLI-6) +# text: a whole response, a body framed by the close or by Content-Length, +# or a chunk's data that still owes at least that many +# (laws/ReadsJudgedByCount.bend) def hear.counts(h: Hear, +m: Nat) -> Bool: match h: case Heads{fuel, asked, raw}: diff --git a/src/packed.bend b/src/packed.bend index c5355b8..ba4e59f 100644 --- a/src/packed.bend +++ b/src/packed.bend @@ -1,24 +1,59 @@ # ezhttp/src/packed: a response read off its packed octets (bend-kit-bytes), # so its body is never turned into text. Only the heads and the chunk-size # lines are read as text, a few octets each; every decision is -# src/http.bend's, the same one its text parse makes (SPEC.md HTTP-CLI-15): -# which head is final and what its plan is (head.final), its reason phrase -# (phrase.line), and what each chunk-size line says (chunk.size). This module -# only finds those lines and copies the body's octets out (SPEC.md -# HTTP-TRUST-9). +# src/http.bend's, the same one its text parse makes +# (laws/HeadPlannedBeforeBody.bend): which head is final and what its plan +# is (head.final), its reason phrase (phrase.line), and what each chunk-size +# line says (chunk.size). This module only finds those lines and copies the +# body's octets out (trusted below). +# +# Trusted, not proved: `packed.reply` reads a response off its packed octets +# as `Http.parse.to` reads the same octets as text +# (laws/InterimResponsesSkipped.bend), acting on src/http.bend's decisions +# in order. From offset 0 it finds each head's end, the first CRLF CRLF at +# or after it (`Bytes.find.from`), at most as many heads as there are +# octets, and reads that head's lines as text; none is torn with `the +# response head has no blank line after it`. It skips the head when +# `interim.skip.of` says so of its status and whether any octet follows its +# blank line, and reads on from there; otherwise its plan is `head.final` of +# the method and its lines, and its reason phrase `phrase.line` of its first +# line (laws/ReasonPhraseFromFinalHead.bend, +# laws/HeadPlannedBeforeBody.bend). An `Unplanned` head is torn with its +# reason. A `Planned` one has its status and fields, and a body by its +# framing: no octets when `Bare`; under `Sized{n}`, torn with `the response +# body stopped before its Content-Length` when fewer than `n` octets follow +# the head, else exactly the first `n` of them +# (laws/ContentLengthFramesBody.bend); every octet after the head when +# `Closed`; when `Chunked`, the chunks from the head's end, at most as many +# as there are octets after it (else `the chunked body is truncated`): a +# chunk-size line with no CRLF after it ends the body; otherwise +# `chunk.size` of the line before its CRLF is acted on: `SizeBad` is torn +# with its reason, `SizeLast` ends the body, and `SizeData{n}` is torn with +# `chunk.short()` unless `n` octets and then CR LF follow the line's CRLF, +# and is otherwise exactly those `n` octets, the body going on at the +# chunk-size line after their CRLF (laws/ChunkDataUnchanged.bend). The body +# is the chunks' octets in order (`Bytes.concat`). It finds lines and copies +# octets out of packed `Array` words with bend-kit-bytes (trusted in +# src/wire.bend), which no law reaches. Every decision it acts on is +# src/http.bend's and is proved to be the text parse's: which head is final, +# its plan, its reason phrase and each chunk-size line's step. Over a corpus +# of responses (sized, short, to the close, chunked with extensions, +# trailers, short and non-hex chunks, TE with CL, disagreeing CLs, interim +# heads, 204, 304, no blank line, no status line) read by `GET` and `HEAD`, +# it answers exactly what `Http.parse.to` answers (bench/parse.bend). import Base import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes import ./http.bend as Http # a response read off its octets: torn, or its status, its header fields and # reason phrase as octet text, and its body's octets -type Got is Type: # noqa: L001 holds packed Bytes, which no law reaches (HTTP-TRUST-9) +type Got is Type: # noqa: L001 holds packed Bytes, which no law reaches (trusted above) Torn{why: String} Got{status: U32, headers: List<&2, Http.Header>, reason: String, body: Bytes.Bytes} # a chunked body's octets so far, newest first, or why it was torn -type Parts is Type: # noqa: L001 holds packed Bytes, which no law reaches (HTTP-TRUST-9) +type Parts is Type: # noqa: L001 holds packed Bytes, which no law reaches (trusted above) Bad{why: String} Parts{parts: List<&1, Bytes.Bytes>} @@ -206,6 +241,7 @@ def packed.reply.of(+method: String, r: Bytes.Bytes & U32) -> Got: packed.heads(U32.to_nat(len), String.eq(method, "HEAD"), b, 0, len) # the response to a request with this method, read off its octets as -# parse.to reads its text (SPEC.md HTTP-CLI-9, HTTP-CLI-15, HTTP-TRUST-9) +# parse.to reads its text (laws/InterimResponsesSkipped.bend, +# laws/HeadPlannedBeforeBody.bend, trusted above) def packed.reply(+method: String, b: Bytes.Bytes) -> Got: packed.reply.of(method, Bytes.length(b)) diff --git a/src/pct.bend b/src/pct.bend index f14239e..9c3af77 100644 --- a/src/pct.bend +++ b/src/pct.bend @@ -2,7 +2,8 @@ # urllib.parse.quote does it. Unreserved characters (§2.3) are written as # themselves; the path form also keeps "/"; every other character is written # as the bytes of its UTF-8 encoding (RFC 3629 §3), each as "%" and two -# upper-case hex digits. SPEC.md HTTP-URL lists what is guaranteed. +# upper-case hex digits. What is guaranteed is under laws/, such as +# laws/QuoteKeepsUnreserved.bend. import Base # RFC 3986 §2.3 unreserved: ALPHA / DIGIT / "-" / "." / "_" / "~" diff --git a/src/qs.bend b/src/qs.bend index b605c98..7e8b338 100644 --- a/src/qs.bend +++ b/src/qs.bend @@ -4,7 +4,7 @@ # parse_qsl(keep_blank_values=True) do them. Decoded bytes are read as UTF-8 # (RFC 3629 §4), each maximal subpart of an ill-formed sequence replaced by # one U+FFFD (Unicode §3.9), as Python's errors="replace" does. Pure. -# SPEC.md HTTP-QS lists what is guaranteed. +# What is guaranteed is under laws/, such as laws/UnquoteReadsEscapes.bend. import Base import ./pct.bend as Pct import ./http.bend as Http @@ -14,7 +14,7 @@ import ./http.bend as Http def Kv() -> Data: Sigma<&2, &2, String, _ => String> -# --- percent-decoding: text to bytes (HTTP-QS-1) --- +# --- percent-decoding: text to bytes (laws/UnquoteReadsEscapes.bend) --- # one piece of a string being decoded: a byte of an ASCII run, or a # character outside ASCII, which is kept as it is and ends the run @@ -108,7 +108,8 @@ def toks(s: String, +plus: Bool, st: Esc) -> List<&2, Tok>: case SCon{+c, t}: tok.step(plus, st, c, st2 => toks(t, plus, st2)) -# --- UTF-8 decoding with replacement (HTTP-QS-2) --- +# --- UTF-8 decoding with replacement --- +# laws/Utf8DecodesWithReplacement.bend # where the UTF-8 decoding stands: between characters, or inside a sequence # with code point bits `cp` so far, needing a byte in lo..hi and `left` more @@ -285,7 +286,8 @@ def utf.dec(ts: List<&2, Tok>, st: Utf) -> String: case t <> rest: utf.step(st, t, st2 => utf.dec(rest, st2)) -# --- the decoders (HTTP-QS-1, 3) --- +# --- the decoders --- +# laws/UnquoteReadsEscapes.bend, laws/UnquotePlusReadsSpaces.bend # percent-decode a string, as urllib.parse.unquote def unquote(s: String) -> String: @@ -295,7 +297,8 @@ def unquote(s: String) -> String: def unquote.plus(s: String) -> String: utf.dec(toks(s, True{}, Plain{}), Idle{}) -# --- quote_plus and urlencode (HTTP-QS-4, 5) --- +# --- quote_plus and urlencode --- +# laws/QuotePlusWritesSpaces.bend, laws/UrlencodeJoinsPairs.bend # one character: a space as `+`, any other as quote_component writes it def plus.one(+cp: U32, rest: String, space: Bool) -> String: @@ -333,7 +336,8 @@ def urlencode(ps: List<&2, Kv()>) -> String: case (name, value) <> t: form.pair(name, value) ++ form.more(t) -# --- parse_query and query_get (HTTP-QS-6, 7) --- +# --- parse_query and query_get --- +# laws/ParseQuerySplitsPairs.bend, laws/QueryGetLastWins.bend # one character put back in front of what the rest of the cut found def cut.push(+c: Char, m: Maybe<&2, Kv()>) -> Maybe<&2, Kv()>: @@ -415,7 +419,7 @@ def get(ps: List<&2, Kv()>, +name: String) -> Maybe<&2, String>: case (key, value) <> t: get.pick(get(t, name), value, String.eq(key, name)) -# --- received octets as text (SPEC.md HTTP-BODY-4) --- +# --- received octets as text (laws/OctetsDecodeAsUtf8.bend) --- # octets, one character each, as the pieces utf.dec reads def octets.toks(s: String) -> List<&2, Tok>: @@ -426,7 +430,7 @@ def octets.toks(s: String) -> List<&2, Tok>: B{Char.to_u32(c)} <> octets.toks(t) # received octets as text: decoded as UTF-8, each ill-formed sequence's -# maximal subpart one U+FFFD (HTTP-QS-2) +# maximal subpart one U+FFFD (laws/Utf8DecodesWithReplacement.bend) def octets.text(o: String) -> String: utf.dec(octets.toks(o), Idle{}) diff --git a/src/redirect.bend b/src/redirect.bend index 9e0e0f3..48eee7c 100644 --- a/src/redirect.bend +++ b/src/redirect.bend @@ -1,6 +1,7 @@ # ezhttp/src/redirect: where a GET goes after a response, pure. The client's # follow loop (client.bend) sends the next GET where this module says and -# decides nothing itself. SPEC.md HTTP-REDIR lists what is guaranteed. +# decides nothing itself. What is guaranteed is under laws/, such as +# laws/RedirectsFollowLocation.bend. # # Policy: the client follows redirects for GET only, so every hop is a GET. # 301, 302, 303, 307 and 308 with a Location are followed; any other status diff --git a/src/server.bend b/src/server.bend index 7e5af98..9dd4608 100644 --- a/src/server.bend +++ b/src/server.bend @@ -5,8 +5,42 @@ # A `_with` serve hands the handler a context value and the request as text # with its body's octets, and takes a reply as text, packed octets or a # stream of octet chunks; the plain serves are those with a text handler. -# Every decision after an effect is step.bend's or http.bend's (SPEC.md -# HTTP-SRV); this module runs the effects in order (HTTP-TRUST-2). +# Every decision after an effect is step.bend's or http.bend's, proved under +# laws/ (the server's guarantees, such as laws/BadRequestAnswers400.bend); +# this module runs the effects in order (trusted below). +# +# Trusted, not proved: Base's TCP effects (listen, accept, send) and +# bend-kit-wire's `recv` on an accepted socket report a failure as `Fail` +# and never end the process themselves; `recv` answers the octets that +# arrived, one character each, and no octets once the peer has closed. They +# are foreign C in Bend's runtime (`effs/tcp_*.c`) and in bend-kit-wire +# (`effs/wire.c`), which no law reaches. +# +# Trusted, not proved: this module acts on step.bend's answers in order. It +# listens, then accepts with bend-kit-wire's `accept.deadline` one +# connection at a time, and serves each accepted connection on its own fork +# (`IO.spawn`); with a concurrency limit `max` other than 0 it takes a slot +# before forking and gives it back when the connection is closed, so at most +# `max` connections are served at once. On each connection it receives the +# request until the octets `step.need` names have arrived (the head, its +# blank line and its Content-Length), the peer closes, or 65536 receives +# have been made, parses it, calls the handler only for a `Call`, with the +# serve's context and the request `step.text` and `step.octets` give, writes +# the reply `step.bare` shapes (a text reply as the UTF-8 of `respond`, a +# packed octets reply as `respond.head` and its octets, a streamed one as +# `respond.stream`, then each chunk the channel gives as `chunk.frame` +# writes it until the channel is closed, then `chunk.end`) with +# bend-kit-wire's `send.words`, and closes the socket; a lost connection's +# line is written to stderr and the socket closed. `http.serve`, +# `http.serve_with`, `http.serve_io` and `http.serve_io_with` stop accepting +# after `limit` connections, a failed accept counting as one and an accept +# whose deadline passed not counting, and return once every connection they +# forked is closed; `http.serve_forever` and `http.serve_forever_with` do +# not stop. It is IO sequencing over Base's and bend-kit-wire's effects and +# the runtime's `IO.spawn` and channels: the checker cannot run an IO +# program and no law can state what happens after an effect, or which fork +# runs when. The serve_forever defs are also `@unsafe` recursion, outside +# the termination check. import Base import bend-kit-wire@0.4.6.1/wire.bend as W import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes @@ -17,14 +51,15 @@ import ./wirespec.bend as Spec # what a `_with` handler gets: the request as text (step.text) and its # body's octets as they arrived (step.octets) -type Req is Type: # noqa: L001 it holds Bytes; step.text and step.octets fill it (HTTP-SRV-9) +type Req is Type: # noqa: L001 it holds Bytes; step.text and step.octets fill it Req{method: String, target: String, headers: List<&2, Http.Header>, body: String, bytes: Bytes.Bytes} # what a `_with` handler answers: a text body (written as UTF-8), packed # octets written unchanged, or a stream of octet chunks from a channel, -# written chunked until the channel is closed (SPEC.md HTTP-SRV-6, 7) -type Answer is Type: # noqa: L001 it holds Bytes and a channel; HTTP-SRV-6 to 8 cover its writing +# written chunked until the channel is closed +# (laws/ReplyHeadFramesBody.bend, laws/StreamedRepliesAreChunked.bend) +type Answer is Type: # noqa: L001 it holds Bytes and a channel; the reply laws cover its writing TextReply{status: U32, headers: List<&2, Http.Header>, body: String} OctetsReply{status: U32, headers: List<&2, Http.Header>, bytes: Bytes.Bytes} StreamReply{status: U32, headers: List<&2, Http.Header>, @@ -199,14 +234,16 @@ def server.then(r: Socket & Step.Got, more: Socket -> IO(Unit)) case Step.Kept{_u}: more(sock) -# a text reply shaped for the request's method (HTTP-SRV-3), then close +# a text reply shaped for the request's method +# (laws/HeadAndNoContentHaveNoBody.bend), then close def server.answer(sock: Socket, method: String, reply: Http.Reply) -> IO(Unit): IO.bind(Socket & Step.Got, Unit, server.send.text(sock, Http.respond.of(Http.reply.for(method, reply))), server.close) -# an octets reply once its length is known: its head, then its octets -# unless it is bare (HTTP-SRV-6, 8) +# an octets reply once its length is known: its head, then its octets unless +# it is bare (laws/ReplyHeadFramesBody.bend, +# laws/BareRepliesWriteHeadOnly.bend) def server.octets.go(sock: Socket, +status: U32, headers: List<&2, Http.Header>, r: Bytes.Bytes & U32, bare: Bool) -> IO(Unit): (b, +n) = r @@ -273,7 +310,9 @@ def server.stream.body(sock: Socket, +ch: Chan(Bytes.Bytes), bare: Bool) case False{}: server.stream.go(server.chunks(), sock, ch) -# a handler's answer, shaped for the request's method (HTTP-SRV-3, 6 to 8) +# a handler's answer, shaped for the request's method +# (laws/HeadAndNoContentHaveNoBody.bend, laws/ReplyHeadFramesBody.bend, +# laws/StreamedRepliesAreChunked.bend, laws/BareRepliesWriteHeadOnly.bend) def server.reply(sock: Socket, +method: String, a: Answer) -> IO(Unit): match a: case TextReply{status, headers, body}: @@ -297,7 +336,9 @@ def server.req(req: Http.Request, +octets: String) -> Req: case Http.Request{method, target, headers, body}: Req{method, target, headers, body, Bytes.from_string(octets)} -# answer now, or call the handler: step.bend decides which (HTTP-SRV-1, 2, 9) +# answer now, or call the handler: step.bend decides which +# (laws/BadRequestAnswers400.bend, laws/HandlerGetsRequestText.bend, +# laws/WithServesGetOctetsAndContext.bend) def server.step(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C, sock: Socket, +method: String, +octets: String, step: Step.Step) -> IO(Unit): match step: @@ -425,7 +466,7 @@ def server.slots(+max: U32) -> IO(Chan(Unit)): # listen and serve up to `limit` connections, each on its own fork, at most # `max` at once (0: no limit), with a context value for the handler; returns -# once every fork is done (SPEC.md HTTP-TRUST-2) +# once every fork is done (trusted above) def server.serve_io_with(~C: Data, ~handle: C -> Req -> IO(Answer), +ctx: C, host: String, port: U32, limit: U32, +max: U32) -> IO(Unit): do IO: diff --git a/src/step.bend b/src/step.bend index fe608f6..f5cf803 100644 --- a/src/step.bend +++ b/src/step.bend @@ -17,8 +17,8 @@ type Got<-A: Type> is Type: Lost{line: String} # a request received as octets, as text: its method, target, header names -# and values and body each decoded as UTF-8 (SPEC.md HTTP-SRV-2); a bad one -# keeps its reason +# and values and body each decoded as UTF-8 +# (laws/HandlerGetsRequestText.bend); a bad one keeps its reason def step.text(req: Http.Request) -> Http.Request: match req: case Http.Bad{why}: @@ -61,7 +61,9 @@ def step.sent(res: Result<&1, &1, (U32 & String) & List<&2, U32>, Unit>) case Done{value}: Kept{value} -# --- the replies and the handler's request (SPEC.md HTTP-SRV-8, 9) --- +# --- the replies and the handler's request --- +# laws/BareRepliesWriteHeadOnly.bend, +# laws/WithServesGetOctetsAndContext.bend # whether a reply is written with its head only: a reply to HEAD, or a 204 # or 304 (RFC 9110 §9.3.2, §15.3.5, §15.4.5) @@ -77,7 +79,8 @@ def step.octets(req: Http.Request) -> String: body # what an accept with a deadline brought: a connection, nothing before the -# deadline, or a lost accept with its line (SPEC.md HTTP-SRV-4) +# deadline, or a lost accept with its line +# (laws/FailedEffectsLoseConnection.bend) type Accept<-A: Type> is Type: Took{value: A} Idle{} @@ -100,7 +103,7 @@ def step.accepted(-A: Type, res: Result<&1, &1, U32 & String, A>) case Done{value}: Took{value} -# --- when a request has all arrived (SPEC.md HTTP-TRUST-2) --- +# --- when a request has all arrived (trusted in src/server.bend) --- # a Content-Length that reads, or none def step.clen(m: Maybe<&2, Nat>) -> Nat: diff --git a/src/unescape.bend b/src/unescape.bend index a43e2a6..a5652af 100644 --- a/src/unescape.bend +++ b/src/unescape.bend @@ -4,7 +4,8 @@ # where the parser would report an error). Numeric references are decimal # (`&`) or hexadecimal (`&`); named ones come from the HTML # Standard's table (src/entities.bend), the legacy names also without their -# `;`. Pure. SPEC.md HTTP-REF lists what is guaranteed. +# `;`. Pure. What is guaranteed is under laws/, such as +# laws/UnescapeFindsReferences.bend. import Base import ./entities.bend as Ents @@ -23,7 +24,7 @@ type Scan is Data: def fffd() -> String: SCon{Chr{65533}, ""} -# --- numeric references (HTTP-REF-2) --- +# --- numeric references (laws/NumericReferencesDecode.bend) --- # the value a number is held at once it is past U+10FFFF: any larger one # reads the same (U+FFFD) @@ -124,7 +125,7 @@ def code.zero(+n: U32, zero: Bool) -> String: def code(+n: U32) -> String: code.zero(n, U32.is_eq(n, 0)) -# --- named references (HTTP-REF-3) --- +# --- named references (laws/NamedReferencesLongestMatch.bend) --- # a name's length, or 0 for none yet def best.len(m: Maybe<&2, Ents.Ref>) -> Nat: @@ -186,7 +187,7 @@ def named.of(m: Maybe<&2, Ents.Ref>, s: String) -> String: def named(+s: String) -> String: named.of(best(Ents.table(), s, None{}), s) -# --- the scan (HTTP-REF-1) --- +# --- the scan (laws/UnescapeFindsReferences.bend) --- # a character that may be in a name: anything but TAB, LF, FF, space, `<`, # `&`, `#` and `;` diff --git a/src/wire.bend b/src/wire.bend index 0145022..01d615f 100644 --- a/src/wire.bend +++ b/src/wire.bend @@ -10,8 +10,42 @@ # (wire.ms.of), the octets sent (wire.payload), what a failed call is reported # as (wire.fail) and when the response is whole (wire.read.count, else # wire.read.next on a copy of the receive as text), so a server that keeps the -# connection open after a complete response is not waited on (SPEC.md -# HTTP-CLI-1, 6, 10, 13; HTTP-TRUST-3, 5). +# connection open after a complete response is not waited on +# (laws/DeadlineIsTimeout.bend, laws/ReadsJudgedByCount.bend, +# laws/OctetBodiesSentUnchanged.bend, laws/FailuresReportedByPhase.bend; +# the sequencing is trusted in src/client.bend). +# +# Trusted, not proved: bend-kit-wire@0.4.6.1 (`Wire.connect`, +# `Wire.tls.connect`, `Wire.send.words.deadline`, `Wire.tls.send.words`, +# `Wire.recv.words`, `Wire.tls.recv.words`, `Wire.tls.close`), +# bend-kit-dns@0.6.0.2 (`Dns.resolve.literal.all`, `Dns.lookup.all`) and +# bend-kit-bytes@0.3.2.0 (`Bytes.from_string`, `Bytes.to_string`, +# `Bytes.length`, `Bytes.append`, `Bytes.concat`, `Bytes.find`, +# `Bytes.find.from`, `Bytes.slice`, `Bytes.new`) do what their comments say: +# `connect` connects to a numeric address within its deadline; +# `tls.connect` checks the server's certificate chain and host name within +# its deadline; `send.words.deadline` sends all `len` octets within its +# deadline, and `tls.send.words` all of them with no deadline; `recv.words` +# and `tls.recv.words` answer at least one octet that arrived within the +# deadline, as many as are waiting up to `max`, and 0 octets once the server +# has closed (over TLS, once it has sent close_notify); a deadline of 0 is +# none; `from_string` packs one octet per character (its low 8 bits) and +# `to_string` gives one character per octet; `length` answers the number of +# octets, `append` and `concat` put octets together in order, `find` +# answers where a run of octets first occurs (none when it does not) and +# `find.from` where it first occurs at or after an offset, `slice` gives the +# buffer back with a copy of up to `n` octets from an offset, and `new(0)` +# is no octets; `lookup.all` asks the OS resolver and answers every address. +# Name resolution is not bounded by the deadline. They are another +# project's code (github.com/paymog/bend-kit, Apache-2.0), pinned by hash in +# ez.toml and upgraded deliberately: foreign C and JS (`effs/wire.c`, +# `wire.js`, `effs/dns.c`, `dns.js`) over sockets, getaddrinfo and OpenSSL, +# and Bend over packed `Array` words that no law here reaches. +# bend-kit-wire ships no LAWS file, so nothing proves its comments. What the +# deadline is, what is sent, what a failure is reported as and when the +# answer is complete are proved (laws/DeadlineIsTimeout.bend, +# laws/OctetBodiesSentUnchanged.bend, laws/FailuresReportedByPhase.bend, +# laws/ReadsJudgedByCount.bend); bend-kit decides none of them. import Base import bend-kit-wire@0.4.6.1/wire.bend as W import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes @@ -77,7 +111,7 @@ def wire.shake(secure: Bool, s: Socket, host: String, ms: U32) # an exchange's answer: the failure it was reported with (a status other than # 0 on its first line, then the reason, wirespec.bend wire.fail), or every # octet of the response, packed -type Answer is Type: # noqa: L001 holds packed Bytes, which no law reaches (HTTP-TRUST-5) +type Answer is Type: # noqa: L001 holds packed Bytes, which no law reaches (trusted in src/client.bend) Failed{answer: String} Received{octets: Bytes.Bytes} @@ -111,7 +145,8 @@ def wire.loop.text(+secure: Bool, s: Socket, h: Http.Hear, piece: Bytes.Bytes, -> IO(Answer): wire.loop.copied(secure, s, h, Bytes.slice(piece, 0, 4294967295), acc, more) -# a read judged by its count when that decides it (SPEC.md HTTP-CLI-6) +# a read judged by its count when that decides it +# (laws/ReadsJudgedByCount.bend) def wire.loop.judge(+secure: Bool, s: Socket, h: Http.Hear, piece: Bytes.Bytes, acc: Bytes.Bytes, counted: Maybe<&2, Spec.Read>, more: Socket -> Http.Hear -> Bytes.Bytes -> IO(Answer)) -> IO(Answer): diff --git a/src/wirespec.bend b/src/wirespec.bend index 97c3af2..d4ccf6c 100644 --- a/src/wirespec.bend +++ b/src/wirespec.bend @@ -3,10 +3,10 @@ # wire.bend (bend-kit-wire's effects) so the proof gate can import it. # wire.bend and client.bend act on what is decided here. # -# A failed exchange's answer is a non-zero status on its first line, then the -# reason (wire.fail); a successful one is the response's octets, which -# wire.bend keeps packed and packed.bend reads. SPEC.md HTTP-CLI lists what is -# guaranteed. +# A failed exchange's answer is a non-zero status on its first line, then +# the reason (wire.fail); a successful one is the response's octets, which +# wire.bend keeps packed and packed.bend reads. What is guaranteed +# is under laws/, such as laws/RequestLineAndHost.bend. import Base import ./http.bend as Http import ./url.bend as Url @@ -33,8 +33,9 @@ def wire.ms.pick(ms: U32, zero: Bool) -> U32: def wire.ms.at(+ms: U32) -> U32: wire.ms.pick(ms, U32.is_eq(ms, 0)) -# the deadline every socket call of an exchange gets: the timeout it was made -# with, or the default (SPEC.md HTTP-CLI-1, 2) +# the deadline every socket call of an exchange gets: the timeout it was +# made with, or the default (laws/DeadlineIsTimeout.bend, +# laws/TimeoutZeroIsOneMs.bend) def wire.ms.of(m: Maybe<&2, U32>) -> U32: match m: case None{}: @@ -117,7 +118,8 @@ def wire.exchange.octets(+method: String, l: Url.Loc, Http.request.head.at(method, secure, host, port, path, headers, n)} # a request with a body of this shape to a URL that came apart: text inside -# the request text as UTF-8, octets after it (SPEC.md HTTP-CLI-10) +# the request text as UTF-8, octets after it +# (laws/OctetBodiesSentUnchanged.bend) def wire.exchange.body(method: String, l: Url.Loc, headers: List<&2, Http.Header>, b: Body.Shape) -> Exchange: match b: @@ -216,7 +218,7 @@ def wire.read.got(h: Http.Hear, +text: String, closed: Bool) -> Read: wire.read.with(Http.hear.more(h, text)) # what to do after a read brought this text, judged from where the response -# stood (SPEC.md HTTP-CLI-6) +# stood (laws/ReadsJudgedByCount.bend) def wire.read.next(h: Http.Hear, +text: String) -> Read: wire.read.got(h, text, String.is_empty(text)) @@ -238,7 +240,8 @@ def wire.read.count.at(+h: Http.Hear, +m: Nat, closed: Bool) wire.read.counted(h, m, Http.hear.counts(h, m)) # what to do after a read of m octets, when their count alone decides it; -# none when their text is needed (wire.read.next) (SPEC.md HTTP-CLI-6) +# none when their text is needed (wire.read.next) +# (laws/ReadsJudgedByCount.bend) def wire.read.count(+h: Http.Hear, +m: Nat) -> Maybe<&2, Read>: wire.read.count.at(h, m, Nat.is_eq(m, 0n)) @@ -251,8 +254,8 @@ def wire.failed(+raw: String) -> String: wire.why(wire.code(raw), wire.text(raw)) # the response an exchange's answer holds, read by the request's method -# (SPEC.md HTTP-CLI-9); a failed exchange is torn with the reason it is -# reported with (wire.why) +# (laws/InterimResponsesSkipped.bend); a failed exchange is torn with the +# reason it is reported with (wire.why) def wire.heard.of(+method: String, +raw: String, ok: Bool) -> Http.Reply: match ok: case False{}: @@ -260,8 +263,9 @@ def wire.heard.of(+method: String, +raw: String, ok: Bool) -> Http.Reply: case True{}: Http.parse.to(method, wire.text(raw)) -# the reason phrase of the response an exchange's answer holds (SPEC.md -# HTTP-CLI-11); a failed exchange's is read from its reason and not used +# the reason phrase of the response an exchange's answer holds +# (laws/ReasonPhraseFromFinalHead.bend); a failed exchange's is read from +# its reason and not used def wire.phrase(raw: String) -> String: Http.phrase.to(wire.text(raw)) @@ -269,7 +273,7 @@ def wire.phrase(raw: String) -> String: def wire.heard(+method: String, +raw: String) -> Http.Reply: wire.heard.of(method, raw, wire.ok(raw)) -# --- what goes on the wire (SPEC.md HTTP-CLI-10) --- +# --- what goes on the wire (laws/OctetBodiesSentUnchanged.bend) --- # a request text's UTF-8 octets, one character each, onto the octets after it def wire.payload.text(s: String, rest: String) -> String: @@ -284,7 +288,8 @@ def wire.payload.text(s: String, rest: String) -> String: def wire.payload(req: String) -> String: wire.payload.text(req, "") -# --- what a failed socket call is reported as (SPEC.md HTTP-CLI-13, 14) --- +# --- what a failed socket call is reported as --- +# laws/FailuresReportedByPhase.bend, laws/ErrnoNamesAreLinux.bend # where in an exchange a call failed type Phase is Data: @@ -323,7 +328,7 @@ def wire.errno.or(name: String) -> String: case SCon{h, t}: SCon{h, t} -# an error number's name (SPEC.md HTTP-CLI-14) +# an error number's name (laws/ErrnoNamesAreLinux.bend) def wire.errno.name(+n: U32) -> String: wire.errno.or(Http.reason.in(wire.errnos(), n)) @@ -385,7 +390,21 @@ def wire.fail.of(p: Phase, host: String, +port: U32, +ms: U32, +n: U32, wire.fail.other(p, host, port, n, why) # the exchange's answer when a call in this phase failed with error number n -# and reason why, with deadline ms (SPEC.md HTTP-CLI-13) +# and reason why, with deadline ms (laws/FailuresReportedByPhase.bend). +# +# Trusted, not proved: bend-kit-wire and bend-kit-dns report a failed call +# as the platform's errno number and its `strerror` text, except that +# `tls.connect` reports a failed handshake as EPROTO with OpenSSL's verify +# reason (or `TLS handshake failed`), and `lookup.all` reports a name that +# does not resolve as ENOENT, a temporary failure as EAGAIN, running out of +# memory as ENOMEM and any other getaddrinfo failure as EIO; a call whose +# deadline passes fails with ETIMEDOUT and no other call does. The names +# laws/ErrnoNamesAreLinux.bend gives are Linux's numbering. It is foreign C +# and JS reading errno, getaddrinfo and OpenSSL, which no law reaches. What +# each number is named and what a failed exchange is reported as are proved +# (laws/FailuresReportedByPhase.bend, laws/ErrnoNamesAreLinux.bend), and +# that the reason reaches `Client.Err` unchanged +# (laws/TimeoutsReadAsTimedOut.bend, laws/InterimResponsesSkipped.bend). def wire.fail(p: Phase, host: String, +port: U32, +ms: U32, +n: U32, why: String) -> String: wire.fail.of(p, host, port, ms, n, why, U32.is_eq(n, wire.etimedout())) diff --git a/tools/entities.py b/tools/entities.py index c347be4..6c5df77 100644 --- a/tools/entities.py +++ b/tools/entities.py @@ -32,8 +32,17 @@ def main(path): "references\n") out.write("# (HTML §13.5, https://html.spec.whatwg.org/entities.json), " "%d names.\n" % len(rows)) - out.write("# Generated by tools/entities.py; do not edit by hand " - "(SPEC.md HTTP-TRUST-7).\n") + out.write("# Generated by tools/entities.py; do not edit by hand.\n" + "#\n" + "# Trusted, not proved: this is the HTML Standard's named character\n" + "# references table: every name in it once, without its leading `&`, with the\n" + "# one or two code points it stands for. tools/entities.py generates it from\n" + "# the standard's entities.json (https://html.spec.whatwg.org/entities.json,\n" + "# the same table as Python's `html.entities.html5`), checking that each\n" + "# entry's code points spell its characters; no law reaches that: a law can\n" + "# say how the table is read (laws/NamedReferencesLongestMatch.bend), not\n" + "# that its rows are the standard's. No test compares html_unescape against\n" + "# Python's `html.unescape`.\n") out.write("import Base\n\n") out.write("# one named character reference: its name (with its `;` when " "it has one)\n# and the one or two code points it stands for, "