From 555ab0e21d0fd3a37df4a5de9096d0148be70da6 Mon Sep 17 00:00:00 2001 From: Noah Gardner Date: Sat, 31 Oct 2026 23:28:24 -0400 Subject: [PATCH 1/3] refactor: one file per guarantee under laws/ and proofs/ SPEC.md, a 5.5k-line LAWS.bend and a 6.5k-line PROOF.bend had to be read side by side, matched up by ID tags. Each guarantee is now laws/.bend, its prose on top and its quantified laws under it, proved by proofs/.bend. LAWS.bend and PROOF.bend only import them. Definitions the laws share are under laws/defs/, lemmas several proofs use under proofs/lemmas/, each named for what it holds. Requirement IDs are gone: a file's name is its guarantee's. A law two guarantees rest on sits in the one that states it, or in a file of its own named for what it says (PhraseTableLookup, InterimStatusIs1xx, SplitAtCharacter, ...). The laws SPEC.md never inventoried (URL parsing, framing, credentials, the cookie jar, caching, CORS) each get a file named after the law, and LAWS.bend's header says they promise nothing beyond their equality. Each section's intro heads the first file of that family; the table itself is kept as docs/SPEC.md. The trust boundary moves onto the code: the server's effects and sequencing on src/server.bend, the client's sequencing on src/client.bend and client.follow, bend-kit's wire, dns and bytes on src/wire.bend, the errno reporting on wire.fail, the form body on form.body, the packed reader on src/packed.bend, and the entities table on src/entities.bend (written by tools/entities.py). This needs a bolt that reads law and proof files by what LAWS.bend and PROOF.bend import, and no longer runs `trace`; bolt.bend drops `trace`. Co-Authored-By: Claude Opus 5.5 --- LAWS.bend | 5863 +----------------- PROOF.bend | 6766 +-------------------- README.md | 57 +- bench/main.bend | 3 +- bench/parse.bend | 8 +- bolt.bend | 4 - SPEC.md => docs/SPEC.md | 0 laws/AbsoluteLocationsResolve.bend | 138 + laws/AskClenExact.bend | 11 + laws/AskClenShort.bend | 14 + laws/AskEqualContentLengths.bend | 8 + laws/AskGetAsteriskRejected.bend | 7 + laws/AskKeepsFieldNameSpelling.bend | 7 + laws/AskNeedsBlankLine.bend | 9 + laws/AskNeedsRequestLine.bend | 8 + laws/AskOptionsAsterisk.bend | 7 + laws/AskOriginFormKeepsQuery.bend | 7 + laws/AskRejectsAbsoluteForm.bend | 7 + laws/AskRejectsClAndTe.bend | 7 + laws/AskRejectsDisagreeingCl.bend | 7 + laws/AskRejectsRelativeTarget.bend | 8 + laws/AskShowBadIsMarked.bend | 7 + laws/B64Empty.bend | 6 + laws/B64F.bend | 6 + laws/B64Fo.bend | 6 + laws/B64Foo.bend | 6 + laws/B64GoEnd.bend | 8 + laws/B64UserPass.bend | 6 + laws/BadRequestAnswers400.bend | 31 + laws/BareRepliesWriteHeadOnly.bend | 27 + laws/BasicUserPass.bend | 8 + laws/BearerIsAuthorization.bend | 11 + laws/CacheDirectiveCase.bend | 9 + laws/CacheDirectives.bend | 7 + laws/CacheIgnoresSMaxage.bend | 7 + laws/CacheMaxAgeIgnoresJunk.bend | 7 + laws/CacheMaxAgePresent.bend | 6 + laws/CacheNoCacheAlone.bend | 7 + laws/CacheNoteExpires.bend | 8 + laws/CachePrivateAlone.bend | 7 + laws/ChunkDataUnchanged.bend | 27 + laws/ChunksEmptyFuel.bend | 6 + laws/ClenExact.bend | 11 + laws/ContentLengthFramesBody.bend | 31 + laws/CookieAgeSets.bend | 21 + laws/CookieDefaultPathDir.bend | 6 + laws/CookieDefaultPathOneSlash.bend | 6 + laws/CookieDefaultPathRoot.bend | 6 + laws/CookieDefaultPathTrailing.bend | 6 + laws/CookieDomainIpExact.bend | 6 + laws/CookieDomainIpSuffix.bend | 6 + laws/CookieDomainLowercased.bend | 8 + laws/CookieDomainNotSuffix.bend | 7 + laws/CookieDomainSuffix.bend | 7 + laws/CookieHeaderEchoesStore.bend | 109 + laws/CookieInsecureOnTcp.bend | 6 + laws/CookieIpv4Literal.bend | 6 + laws/CookieIpv4Name.bend | 6 + laws/CookieLineAttributes.bend | 12 + laws/CookieMaxAgeIgnoresJunk.bend | 7 + laws/CookieMaxAgeOne.bend | 7 + laws/CookieMaxAgeOpen.bend | 6 + laws/CookieMaxAgeZero.bend | 6 + laws/CookieParseRoundTrip.bend | 9 + laws/CookiePathIdentical.bend | 6 + laws/CookiePathLongerCookie.bend | 6 + laws/CookiePathNotPrefix.bend | 6 + laws/CookiePathPrefix.bend | 6 + laws/CookiePathRoot.bend | 6 + laws/CookiePathSlashPrefix.bend | 6 + laws/CookieReceiveHostOnly.bend | 8 + laws/CookieRejectsEmptyName.bend | 6 + laws/CookieRequestJoins.bend | 9 + laws/CookieSamesiteNone.bend | 8 + laws/CookieSamesiteStrict.bend | 8 + laws/CookieSamesiteUnknown.bend | 7 + laws/CookieSecureNeedsTls.bend | 6 + laws/CookieSecureOnTls.bend | 6 + laws/CookieSendHostOnly.bend | 8 + laws/CookieSendOtherHost.bend | 8 + laws/CookieSendUnsetDomain.bend | 7 + laws/CookieSessionAlive.bend | 6 + laws/CorsCredentialsExplicitOrigin.bend | 7 + laws/CorsCredentialsReflectOrigin.bend | 9 + laws/CorsExplicitOrigin.bend | 7 + laws/CorsGetIsNotPreflight.bend | 8 + laws/CorsOnPreflight.bend | 21 + laws/CorsOriginHeader.bend | 8 + laws/CorsPreflightAllowed.bend | 17 + laws/CorsPreflightBad.bend | 11 + laws/CorsPreflightHeaderCase.bend | 16 + laws/CorsPreflightHeaderDenied.bend | 10 + laws/CorsPreflightMethodDenied.bend | 10 + laws/CorsPreflightOriginDenied.bend | 10 + laws/CorsPreflightStar.bend | 14 + laws/CorsRefusedOrigin.bend | 7 + laws/CorsRequestHeadersHeader.bend | 8 + laws/CorsRequestMethodHeader.bend | 8 + laws/CorsSimpleReflects.bend | 15 + laws/CorsSimpleRefused.bend | 9 + laws/CorsSimpleStar.bend | 10 + laws/CorsStarWithoutCredentials.bend | 6 + laws/CorsVaryEmpty.bend | 6 + laws/CorsVarySpecific.bend | 6 + laws/CorsVaryStar.bend | 6 + laws/CutAtFirstEquals.bend | 19 + laws/DeadlineIsTimeout.bend | 18 + laws/DechunkEmpty.bend | 6 + laws/DechunkIgnoresExtension.bend | 6 + laws/DechunkOnlyLast.bend | 6 + laws/DechunkStopsAtZero.bend | 6 + laws/DechunkTwoChunks.bend | 6 + laws/DechunkWiki.bend | 6 + laws/DeleteReqNamesPath.bend | 11 + laws/DfltHttps.bend | 6 + laws/DfltPlain.bend | 8 + laws/DigitsReadDecimal.bend | 6 + laws/DigitsReadRejectsJunk.bend | 6 + laws/DotSegmentsRemoved.bend | 75 + laws/ErrnoNamesAreLinux.bend | 52 + laws/ExchangeCarriesMethod.bend | 38 + laws/FailedEffectsLoseConnection.bend | 62 + laws/FailuresReportedByPhase.bend | 147 + laws/FirstIsFirstWord.bend | 10 + laws/FormBodyLayout.bend | 67 + laws/FormBoundaryRules.bend | 160 + laws/FormNamesEscaped.bend | 44 + laws/FormPartHeaders.bend | 57 + laws/FormRefusesBadRequests.bend | 70 + laws/FreshMaxAgeLimit.bend | 6 + laws/FreshMaxAgeOne.bend | 8 + laws/FreshMaxAgeOpen.bend | 6 + laws/FreshMaxAgeOverridesExpires.bend | 7 + laws/FreshMaxAgeZero.bend | 6 + laws/FreshNoCache.bend | 7 + laws/FreshNoCacheAny.bend | 14 + laws/FreshNoStore.bend | 7 + laws/FreshNoStoreAny.bend | 14 + laws/FreshPublicOnly.bend | 6 + laws/GetOmitsContentLength.bend | 13 + laws/GetReplyKeepsBody.bend | 7 + laws/GetReqNamesPath.bend | 10 + laws/HandlerGetsRequestText.bend | 28 + laws/HeadAndNoContentHaveNoBody.bend | 44 + laws/HeadHitIgnoresCase.bend | 9 + laws/HeadLineRejectsContinuation.bend | 6 + laws/HeadLineRejectsObsFold.bend | 6 + laws/HeadLineTrims.bend | 6 + laws/HeadPlannedBeforeBody.bend | 128 + laws/HeadReplyDropsBody.bend | 7 + laws/HeadReqNamesPath.bend | 10 + laws/HeadersFindFirst.bend | 7 + laws/HeadersFindIgnoresCase.bend | 6 + laws/HeadersFindMissing.bend | 6 + laws/HexlenAlpha.bend | 6 + laws/HexlenExtensionAndCase.bend | 6 + laws/HexlenIgnoresExtension.bend | 11 + laws/HtmlEscapeCharByChar.bend | 28 + laws/HtmlEscapeIsSafe.bend | 90 + laws/HtmlEscapesFiveChars.bend | 29 + laws/HtmlKeepsOtherChars.bend | 14 + laws/InterimResponsesSkipped.bend | 142 + laws/InterimStatusIs1xx.bend | 16 + laws/JoinJoined.bend | 8 + laws/JsonParseRejects.bend | 6 + laws/JsonParseString.bend | 6 + laws/JsonStringIsTextBody.bend | 9 + laws/LocationResolvedBySort.bend | 22 + laws/LocationSplitsAtQuery.bend | 23 + laws/MethodIdempotentDelete.bend | 6 + laws/MethodIdempotentGet.bend | 6 + laws/MethodIdempotentHead.bend | 6 + laws/MethodIdempotentOptions.bend | 6 + laws/MethodIdempotentPatch.bend | 6 + laws/MethodIdempotentPost.bend | 6 + laws/MethodIdempotentPut.bend | 6 + laws/MethodSafeDelete.bend | 6 + laws/MethodSafeGet.bend | 6 + laws/MethodSafeGetLower.bend | 6 + laws/MethodSafeHead.bend | 6 + laws/MethodSafeOptions.bend | 6 + laws/MethodSafePost.bend | 6 + laws/MethodSafePut.bend | 6 + laws/MethodTokensAreCaseSensitive.bend | 5 + laws/NamedReferencesLongestMatch.bend | 116 + laws/NumericReferencesDecode.bend | 86 + laws/OctetBodiesSentUnchanged.bend | 137 + laws/OctetsDecodeAsUtf8.bend | 44 + laws/OneOctetPerChar.bend | 40 + laws/OnlyGetFollows.bend | 19 + laws/OptionsReqNamesPath.bend | 10 + laws/OtherStatusesStopChain.bend | 24 + laws/ParseChunkedBody.bend | 8 + laws/ParseChunkedIgnoresCase.bend | 8 + laws/ParseClenTakesPrefix.bend | 7 + laws/ParseEqualContentLengths.bend | 8 + laws/ParseNeedsBlankLine.bend | 9 + laws/ParseNeedsSlashes.bend | 11 + laws/ParseNeedsStatus.bend | 12 + laws/ParseQuerySplitsPairs.bend | 54 + laws/ParseRejectsClAndTe.bend | 7 + laws/ParseRejectsDisagreeingCl.bend | 7 + laws/ParseRejectsNonHexChunk.bend | 7 + laws/ParseSchemeFirst.bend | 11 + laws/PathAbsolute.bend | 9 + laws/PathFormKeepsSlash.bend | 23 + laws/PhraseTableLookup.bend | 30 + laws/PostReqNamesPath.bend | 11 + laws/PostWritesContentLength.bend | 15 + laws/PutReqNamesPath.bend | 11 + laws/QueryGetLastWins.bend | 40 + laws/QuoteEscapesUtf8Bytes.bend | 79 + laws/QuoteKeepsUnreserved.bend | 35 + laws/QuoteOutputIsFramed.bend | 63 + laws/QuotePlusWritesSpaces.bend | 26 + laws/ReadStateMatchesWholeText.bend | 48 + laws/ReadsJudgedByCount.bend | 76 + laws/Reason200.bend | 6 + laws/Reason201.bend | 6 + laws/Reason204.bend | 6 + laws/Reason400.bend | 6 + laws/Reason404.bend | 6 + laws/Reason405.bend | 6 + laws/Reason500.bend | 6 + laws/ReasonPhraseFromFinalHead.bend | 100 + laws/ReasonPhrasesFollowRfc.bend | 55 + laws/ReasonUnknownEmpty.bend | 6 + laws/RedirectsFollowLocation.bend | 57 + laws/RelativeLocationsMerge.bend | 105 + laws/ReplyHeadFramesBody.bend | 42 + laws/RequestHeaderFieldsOrdered.bend | 203 + laws/RequestLineAndHost.bend | 112 + laws/RequestNamesMethod.bend | 12 + laws/Respond200KeepsBody.bend | 7 + laws/Respond204DropsBody.bend | 7 + laws/Respond304DropsBody.bend | 7 + laws/RespondOpensWithVersion.bend | 10 + laws/ResponseCompleteByFraming.bend | 231 + laws/SecondIsNextWord.bend | 12 + laws/SetCookieFieldsSelected.bend | 32 + laws/SetCookieReadsPair.bend | 82 + laws/ShowReplyOpensWithStatus.bend | 10 + laws/ShowTornIsMarked.bend | 7 + laws/SplitAtCharacter.bend | 21 + laws/StreamedRepliesAreChunked.bend | 70 + laws/TimeoutZeroIsOneMs.bend | 17 + laws/TimeoutsReadAsTimedOut.bend | 30 + laws/TokenOkNewline.bend | 6 + laws/TokenOkPlain.bend | 8 + laws/TokenOkSpace.bend | 6 + laws/TokenOkTab.bend | 6 + laws/UnescapeFindsReferences.bend | 277 + laws/UnescapeUndoesEscape.bend | 13 + laws/UnframedIsRest.bend | 11 + laws/UnquotePlusReadsSpaces.bend | 20 + laws/UnquoteReadsEscapes.bend | 179 + laws/UrlBadPortUsesDefault.bend | 7 + laws/UrlEmptyPath.bend | 9 + laws/UrlFragmentHidesQuery.bend | 7 + laws/UrlHttpDefaultPort.bend | 9 + laws/UrlHttpsDefaultPort.bend | 7 + laws/UrlHttpsExplicitPort.bend | 7 + laws/UrlIpv4Host.bend | 7 + laws/UrlIpv4Port.bend | 7 + laws/UrlQueryDropsFragment.bend | 7 + laws/UrlQueryKeepsQuestion.bend | 7 + laws/UrlQueryOnEmptyPath.bend | 7 + laws/UrlSchemeLowercased.bend | 7 + laws/UrlShowBadIsMarked.bend | 7 + laws/UrlShowOpensWithScheme.bend | 11 + laws/UrlTrailingSlash.bend | 7 + laws/UrlUserinfoLastAt.bend | 7 + laws/UrlUserinfoStripped.bend | 7 + laws/UrlencodeJoinsPairs.bend | 41 + laws/Utf8DecodesWithReplacement.bend | 252 + laws/Utf8Split.bend | 9 + laws/WithServesGetOctetsAndContext.bend | 17 + laws/defs/CharacterAbsent.bend | 10 + laws/defs/PercentFrame.bend | 46 + main.bend | 28 +- proofs/AbsoluteLocationsResolve.bend | 79 + proofs/AskClenExact.bend | 18 + proofs/AskClenShort.bend | 10 + proofs/AskEqualContentLengths.bend | 6 + proofs/AskGetAsteriskRejected.bend | 6 + proofs/AskKeepsFieldNameSpelling.bend | 6 + proofs/AskNeedsBlankLine.bend | 10 + proofs/AskNeedsRequestLine.bend | 6 + proofs/AskOptionsAsterisk.bend | 6 + proofs/AskOriginFormKeepsQuery.bend | 6 + proofs/AskRejectsAbsoluteForm.bend | 6 + proofs/AskRejectsClAndTe.bend | 6 + proofs/AskRejectsDisagreeingCl.bend | 6 + proofs/AskRejectsRelativeTarget.bend | 6 + proofs/AskShowBadIsMarked.bend | 7 + proofs/B64Empty.bend | 6 + proofs/B64F.bend | 6 + proofs/B64Fo.bend | 6 + proofs/B64Foo.bend | 6 + proofs/B64GoEnd.bend | 7 + proofs/B64UserPass.bend | 6 + proofs/BadRequestAnswers400.bend | 11 + proofs/BareRepliesWriteHeadOnly.bend | 15 + proofs/BasicUserPass.bend | 6 + proofs/BearerIsAuthorization.bend | 6 + proofs/CacheDirectiveCase.bend | 6 + proofs/CacheDirectives.bend | 6 + proofs/CacheIgnoresSMaxage.bend | 6 + proofs/CacheMaxAgeIgnoresJunk.bend | 6 + proofs/CacheMaxAgePresent.bend | 6 + proofs/CacheNoCacheAlone.bend | 6 + proofs/CacheNoteExpires.bend | 6 + proofs/CachePrivateAlone.bend | 6 + proofs/ChunkDataUnchanged.bend | 51 + proofs/ChunksEmptyFuel.bend | 6 + proofs/ClenExact.bend | 17 + proofs/ContentLengthFramesBody.bend | 43 + proofs/CookieAgeSets.bend | 19 + proofs/CookieDefaultPathDir.bend | 6 + proofs/CookieDefaultPathOneSlash.bend | 6 + proofs/CookieDefaultPathRoot.bend | 6 + proofs/CookieDefaultPathTrailing.bend | 6 + proofs/CookieDomainIpExact.bend | 6 + proofs/CookieDomainIpSuffix.bend | 6 + proofs/CookieDomainLowercased.bend | 6 + proofs/CookieDomainNotSuffix.bend | 6 + proofs/CookieDomainSuffix.bend | 6 + proofs/CookieHeaderEchoesStore.bend | 50 + proofs/CookieInsecureOnTcp.bend | 6 + proofs/CookieIpv4Literal.bend | 6 + proofs/CookieIpv4Name.bend | 6 + proofs/CookieLineAttributes.bend | 6 + proofs/CookieMaxAgeIgnoresJunk.bend | 6 + proofs/CookieMaxAgeOne.bend | 6 + proofs/CookieMaxAgeOpen.bend | 6 + proofs/CookieMaxAgeZero.bend | 6 + proofs/CookieParseRoundTrip.bend | 6 + proofs/CookiePathIdentical.bend | 6 + proofs/CookiePathLongerCookie.bend | 6 + proofs/CookiePathNotPrefix.bend | 6 + proofs/CookiePathPrefix.bend | 6 + proofs/CookiePathRoot.bend | 6 + proofs/CookiePathSlashPrefix.bend | 6 + proofs/CookieReceiveHostOnly.bend | 6 + proofs/CookieRejectsEmptyName.bend | 6 + proofs/CookieRequestJoins.bend | 6 + proofs/CookieSamesiteNone.bend | 6 + proofs/CookieSamesiteStrict.bend | 6 + proofs/CookieSamesiteUnknown.bend | 6 + proofs/CookieSecureNeedsTls.bend | 6 + proofs/CookieSecureOnTls.bend | 6 + proofs/CookieSendHostOnly.bend | 6 + proofs/CookieSendOtherHost.bend | 6 + proofs/CookieSendUnsetDomain.bend | 6 + proofs/CookieSessionAlive.bend | 6 + proofs/CorsCredentialsExplicitOrigin.bend | 6 + proofs/CorsCredentialsReflectOrigin.bend | 6 + proofs/CorsExplicitOrigin.bend | 6 + proofs/CorsGetIsNotPreflight.bend | 6 + proofs/CorsOnPreflight.bend | 6 + proofs/CorsOriginHeader.bend | 6 + proofs/CorsPreflightAllowed.bend | 6 + proofs/CorsPreflightBad.bend | 7 + proofs/CorsPreflightHeaderCase.bend | 6 + proofs/CorsPreflightHeaderDenied.bend | 6 + proofs/CorsPreflightMethodDenied.bend | 6 + proofs/CorsPreflightOriginDenied.bend | 6 + proofs/CorsPreflightStar.bend | 6 + proofs/CorsRefusedOrigin.bend | 6 + proofs/CorsRequestHeadersHeader.bend | 6 + proofs/CorsRequestMethodHeader.bend | 6 + proofs/CorsSimpleReflects.bend | 6 + proofs/CorsSimpleRefused.bend | 6 + proofs/CorsSimpleStar.bend | 6 + proofs/CorsStarWithoutCredentials.bend | 6 + proofs/CorsVaryEmpty.bend | 6 + proofs/CorsVarySpecific.bend | 6 + proofs/CorsVaryStar.bend | 6 + proofs/CutAtFirstEquals.bend | 41 + proofs/DeadlineIsTimeout.bend | 10 + proofs/DechunkEmpty.bend | 6 + proofs/DechunkIgnoresExtension.bend | 6 + proofs/DechunkOnlyLast.bend | 6 + proofs/DechunkStopsAtZero.bend | 6 + proofs/DechunkTwoChunks.bend | 6 + proofs/DechunkWiki.bend | 6 + proofs/DeleteReqNamesPath.bend | 12 + proofs/DfltHttps.bend | 6 + proofs/DfltPlain.bend | 7 + proofs/DigitsReadDecimal.bend | 6 + proofs/DigitsReadRejectsJunk.bend | 6 + proofs/DotSegmentsRemoved.bend | 58 + proofs/ErrnoNamesAreLinux.bend | 23 + proofs/ExchangeCarriesMethod.bend | 37 + proofs/FailedEffectsLoseConnection.bend | 28 + proofs/FailuresReportedByPhase.bend | 96 + proofs/FirstIsFirstWord.bend | 11 + proofs/FormBodyLayout.bend | 24 + proofs/FormBoundaryRules.bend | 78 + proofs/FormNamesEscaped.bend | 26 + proofs/FormPartHeaders.bend | 18 + proofs/FormRefusesBadRequests.bend | 41 + proofs/FreshMaxAgeLimit.bend | 6 + proofs/FreshMaxAgeOne.bend | 6 + proofs/FreshMaxAgeOpen.bend | 6 + proofs/FreshMaxAgeOverridesExpires.bend | 6 + proofs/FreshMaxAgeZero.bend | 6 + proofs/FreshNoCache.bend | 6 + proofs/FreshNoCacheAny.bend | 18 + proofs/FreshNoStore.bend | 6 + proofs/FreshNoStoreAny.bend | 14 + proofs/FreshPublicOnly.bend | 6 + proofs/GetOmitsContentLength.bend | 6 + proofs/GetReplyKeepsBody.bend | 6 + proofs/GetReqNamesPath.bend | 10 + proofs/HandlerGetsRequestText.bend | 9 + proofs/HeadAndNoContentHaveNoBody.bend | 29 + proofs/HeadHitIgnoresCase.bend | 6 + proofs/HeadLineRejectsContinuation.bend | 6 + proofs/HeadLineRejectsObsFold.bend | 6 + proofs/HeadLineTrims.bend | 6 + proofs/HeadPlannedBeforeBody.bend | 123 + proofs/HeadReplyDropsBody.bend | 6 + proofs/HeadReqNamesPath.bend | 10 + proofs/HeadersFindFirst.bend | 6 + proofs/HeadersFindIgnoresCase.bend | 6 + proofs/HeadersFindMissing.bend | 6 + proofs/HexlenAlpha.bend | 6 + proofs/HexlenExtensionAndCase.bend | 6 + proofs/HexlenIgnoresExtension.bend | 21 + proofs/HtmlEscapeCharByChar.bend | 14 + proofs/HtmlEscapeIsSafe.bend | 135 + proofs/HtmlEscapesFiveChars.bend | 18 + proofs/HtmlKeepsOtherChars.bend | 12 + proofs/InterimResponsesSkipped.bend | 72 + proofs/InterimStatusIs1xx.bend | 11 + proofs/JoinJoined.bend | 51 + proofs/JsonParseRejects.bend | 6 + proofs/JsonParseString.bend | 6 + proofs/JsonStringIsTextBody.bend | 6 + proofs/LocationResolvedBySort.bend | 13 + proofs/LocationSplitsAtQuery.bend | 15 + proofs/MethodIdempotentDelete.bend | 6 + proofs/MethodIdempotentGet.bend | 6 + proofs/MethodIdempotentHead.bend | 6 + proofs/MethodIdempotentOptions.bend | 6 + proofs/MethodIdempotentPatch.bend | 6 + proofs/MethodIdempotentPost.bend | 6 + proofs/MethodIdempotentPut.bend | 6 + proofs/MethodSafeDelete.bend | 6 + proofs/MethodSafeGet.bend | 6 + proofs/MethodSafeGetLower.bend | 6 + proofs/MethodSafeHead.bend | 6 + proofs/MethodSafeOptions.bend | 6 + proofs/MethodSafePost.bend | 6 + proofs/MethodSafePut.bend | 6 + proofs/MethodTokensAreCaseSensitive.bend | 6 + proofs/NamedReferencesLongestMatch.bend | 51 + proofs/NumericReferencesDecode.bend | 54 + proofs/OctetBodiesSentUnchanged.bend | 63 + proofs/OctetsDecodeAsUtf8.bend | 22 + proofs/OneOctetPerChar.bend | 7 + proofs/OnlyGetFollows.bend | 18 + proofs/OptionsReqNamesPath.bend | 10 + proofs/OtherStatusesStopChain.bend | 16 + proofs/ParseChunkedBody.bend | 6 + proofs/ParseChunkedIgnoresCase.bend | 6 + proofs/ParseClenTakesPrefix.bend | 6 + proofs/ParseEqualContentLengths.bend | 6 + proofs/ParseNeedsBlankLine.bend | 10 + proofs/ParseNeedsSlashes.bend | 10 + proofs/ParseNeedsStatus.bend | 10 + proofs/ParseQuerySplitsPairs.bend | 28 + proofs/ParseRejectsClAndTe.bend | 6 + proofs/ParseRejectsDisagreeingCl.bend | 6 + proofs/ParseRejectsNonHexChunk.bend | 6 + proofs/ParseSchemeFirst.bend | 9 + proofs/PathAbsolute.bend | 11 + proofs/PathFormKeepsSlash.bend | 16 + proofs/PhraseTableLookup.bend | 17 + proofs/PostReqNamesPath.bend | 12 + proofs/PostWritesContentLength.bend | 6 + proofs/PutReqNamesPath.bend | 12 + proofs/QueryGetLastWins.bend | 24 + proofs/QuoteEscapesUtf8Bytes.bend | 62 + proofs/QuoteKeepsUnreserved.bend | 17 + proofs/QuoteOutputIsFramed.bend | 94 + proofs/QuotePlusWritesSpaces.bend | 17 + proofs/ReadStateMatchesWholeText.bend | 6 + proofs/ReadsJudgedByCount.bend | 116 + proofs/Reason200.bend | 6 + proofs/Reason201.bend | 6 + proofs/Reason204.bend | 6 + proofs/Reason400.bend | 6 + proofs/Reason404.bend | 6 + proofs/Reason405.bend | 6 + proofs/Reason500.bend | 6 + proofs/ReasonPhraseFromFinalHead.bend | 227 + proofs/ReasonPhrasesFollowRfc.bend | 11 + proofs/ReasonUnknownEmpty.bend | 6 + proofs/RedirectsFollowLocation.bend | 37 + proofs/RelativeLocationsMerge.bend | 55 + proofs/ReplyHeadFramesBody.bend | 12 + proofs/RequestHeaderFieldsOrdered.bend | 664 ++ proofs/RequestLineAndHost.bend | 31 + proofs/RequestNamesMethod.bend | 11 + proofs/Respond200KeepsBody.bend | 6 + proofs/Respond204DropsBody.bend | 6 + proofs/Respond304DropsBody.bend | 6 + proofs/RespondOpensWithVersion.bend | 12 + proofs/ResponseCompleteByFraming.bend | 204 + proofs/SecondIsNextWord.bend | 11 + proofs/SetCookieFieldsSelected.bend | 18 + proofs/SetCookieReadsPair.bend | 43 + proofs/ShowReplyOpensWithStatus.bend | 7 + proofs/ShowTornIsMarked.bend | 7 + proofs/SplitAtCharacter.bend | 6 + proofs/StreamedRepliesAreChunked.bend | 43 + proofs/TimeoutZeroIsOneMs.bend | 12 + proofs/TimeoutsReadAsTimedOut.bend | 20 + proofs/TokenOkNewline.bend | 6 + proofs/TokenOkPlain.bend | 6 + proofs/TokenOkSpace.bend | 6 + proofs/TokenOkTab.bend | 6 + proofs/UnescapeFindsReferences.bend | 180 + proofs/UnescapeUndoesEscape.bend | 182 + proofs/UnframedIsRest.bend | 6 + proofs/UnquotePlusReadsSpaces.bend | 10 + proofs/UnquoteReadsEscapes.bend | 115 + proofs/UrlBadPortUsesDefault.bend | 6 + proofs/UrlEmptyPath.bend | 6 + proofs/UrlFragmentHidesQuery.bend | 6 + proofs/UrlHttpDefaultPort.bend | 6 + proofs/UrlHttpsDefaultPort.bend | 6 + proofs/UrlHttpsExplicitPort.bend | 6 + proofs/UrlIpv4Host.bend | 6 + proofs/UrlIpv4Port.bend | 6 + proofs/UrlQueryDropsFragment.bend | 6 + proofs/UrlQueryKeepsQuestion.bend | 6 + proofs/UrlQueryOnEmptyPath.bend | 6 + proofs/UrlSchemeLowercased.bend | 6 + proofs/UrlShowBadIsMarked.bend | 7 + proofs/UrlShowOpensWithScheme.bend | 7 + proofs/UrlTrailingSlash.bend | 6 + proofs/UrlUserinfoLastAt.bend | 6 + proofs/UrlUserinfoStripped.bend | 6 + proofs/UrlencodeJoinsPairs.bend | 21 + proofs/Utf8DecodesWithReplacement.bend | 175 + proofs/Utf8Split.bend | 32 + proofs/WithServesGetOctetsAndContext.bend | 6 + proofs/lemmas/BaseEqualities.bend | 161 + proofs/lemmas/DropNothing.bend | 9 + proofs/lemmas/HeaderFieldSlots.bend | 364 ++ proofs/lemmas/NatOrder.bend | 97 + proofs/lemmas/OctetLength.bend | 12 + proofs/lemmas/OctetTakeDrop.bend | 54 + proofs/lemmas/WalksOverAppendedText.bend | 1430 +++++ src/body.bend | 2 +- src/client.bend | 58 +- src/echo.bend | 3 +- src/entities.bend | 11 +- src/form.bend | 25 +- src/html.bend | 2 +- src/http.bend | 23 +- src/packed.bend | 52 +- src/pct.bend | 3 +- src/qs.bend | 20 +- src/redirect.bend | 3 +- src/server.bend | 63 +- src/step.bend | 13 +- src/unescape.bend | 9 +- src/wire.bend | 43 +- src/wirespec.bend | 53 +- tools/entities.py | 12 +- 574 files changed, 14667 insertions(+), 12183 deletions(-) rename SPEC.md => docs/SPEC.md (100%) create mode 100644 laws/AbsoluteLocationsResolve.bend create mode 100644 laws/AskClenExact.bend create mode 100644 laws/AskClenShort.bend create mode 100644 laws/AskEqualContentLengths.bend create mode 100644 laws/AskGetAsteriskRejected.bend create mode 100644 laws/AskKeepsFieldNameSpelling.bend create mode 100644 laws/AskNeedsBlankLine.bend create mode 100644 laws/AskNeedsRequestLine.bend create mode 100644 laws/AskOptionsAsterisk.bend create mode 100644 laws/AskOriginFormKeepsQuery.bend create mode 100644 laws/AskRejectsAbsoluteForm.bend create mode 100644 laws/AskRejectsClAndTe.bend create mode 100644 laws/AskRejectsDisagreeingCl.bend create mode 100644 laws/AskRejectsRelativeTarget.bend create mode 100644 laws/AskShowBadIsMarked.bend create mode 100644 laws/B64Empty.bend create mode 100644 laws/B64F.bend create mode 100644 laws/B64Fo.bend create mode 100644 laws/B64Foo.bend create mode 100644 laws/B64GoEnd.bend create mode 100644 laws/B64UserPass.bend create mode 100644 laws/BadRequestAnswers400.bend create mode 100644 laws/BareRepliesWriteHeadOnly.bend create mode 100644 laws/BasicUserPass.bend create mode 100644 laws/BearerIsAuthorization.bend create mode 100644 laws/CacheDirectiveCase.bend create mode 100644 laws/CacheDirectives.bend create mode 100644 laws/CacheIgnoresSMaxage.bend create mode 100644 laws/CacheMaxAgeIgnoresJunk.bend create mode 100644 laws/CacheMaxAgePresent.bend create mode 100644 laws/CacheNoCacheAlone.bend create mode 100644 laws/CacheNoteExpires.bend create mode 100644 laws/CachePrivateAlone.bend create mode 100644 laws/ChunkDataUnchanged.bend create mode 100644 laws/ChunksEmptyFuel.bend create mode 100644 laws/ClenExact.bend create mode 100644 laws/ContentLengthFramesBody.bend create mode 100644 laws/CookieAgeSets.bend create mode 100644 laws/CookieDefaultPathDir.bend create mode 100644 laws/CookieDefaultPathOneSlash.bend create mode 100644 laws/CookieDefaultPathRoot.bend create mode 100644 laws/CookieDefaultPathTrailing.bend create mode 100644 laws/CookieDomainIpExact.bend create mode 100644 laws/CookieDomainIpSuffix.bend create mode 100644 laws/CookieDomainLowercased.bend create mode 100644 laws/CookieDomainNotSuffix.bend create mode 100644 laws/CookieDomainSuffix.bend create mode 100644 laws/CookieHeaderEchoesStore.bend create mode 100644 laws/CookieInsecureOnTcp.bend create mode 100644 laws/CookieIpv4Literal.bend create mode 100644 laws/CookieIpv4Name.bend create mode 100644 laws/CookieLineAttributes.bend create mode 100644 laws/CookieMaxAgeIgnoresJunk.bend create mode 100644 laws/CookieMaxAgeOne.bend create mode 100644 laws/CookieMaxAgeOpen.bend create mode 100644 laws/CookieMaxAgeZero.bend create mode 100644 laws/CookieParseRoundTrip.bend create mode 100644 laws/CookiePathIdentical.bend create mode 100644 laws/CookiePathLongerCookie.bend create mode 100644 laws/CookiePathNotPrefix.bend create mode 100644 laws/CookiePathPrefix.bend create mode 100644 laws/CookiePathRoot.bend create mode 100644 laws/CookiePathSlashPrefix.bend create mode 100644 laws/CookieReceiveHostOnly.bend create mode 100644 laws/CookieRejectsEmptyName.bend create mode 100644 laws/CookieRequestJoins.bend create mode 100644 laws/CookieSamesiteNone.bend create mode 100644 laws/CookieSamesiteStrict.bend create mode 100644 laws/CookieSamesiteUnknown.bend create mode 100644 laws/CookieSecureNeedsTls.bend create mode 100644 laws/CookieSecureOnTls.bend create mode 100644 laws/CookieSendHostOnly.bend create mode 100644 laws/CookieSendOtherHost.bend create mode 100644 laws/CookieSendUnsetDomain.bend create mode 100644 laws/CookieSessionAlive.bend create mode 100644 laws/CorsCredentialsExplicitOrigin.bend create mode 100644 laws/CorsCredentialsReflectOrigin.bend create mode 100644 laws/CorsExplicitOrigin.bend create mode 100644 laws/CorsGetIsNotPreflight.bend create mode 100644 laws/CorsOnPreflight.bend create mode 100644 laws/CorsOriginHeader.bend create mode 100644 laws/CorsPreflightAllowed.bend create mode 100644 laws/CorsPreflightBad.bend create mode 100644 laws/CorsPreflightHeaderCase.bend create mode 100644 laws/CorsPreflightHeaderDenied.bend create mode 100644 laws/CorsPreflightMethodDenied.bend create mode 100644 laws/CorsPreflightOriginDenied.bend create mode 100644 laws/CorsPreflightStar.bend create mode 100644 laws/CorsRefusedOrigin.bend create mode 100644 laws/CorsRequestHeadersHeader.bend create mode 100644 laws/CorsRequestMethodHeader.bend create mode 100644 laws/CorsSimpleReflects.bend create mode 100644 laws/CorsSimpleRefused.bend create mode 100644 laws/CorsSimpleStar.bend create mode 100644 laws/CorsStarWithoutCredentials.bend create mode 100644 laws/CorsVaryEmpty.bend create mode 100644 laws/CorsVarySpecific.bend create mode 100644 laws/CorsVaryStar.bend create mode 100644 laws/CutAtFirstEquals.bend create mode 100644 laws/DeadlineIsTimeout.bend create mode 100644 laws/DechunkEmpty.bend create mode 100644 laws/DechunkIgnoresExtension.bend create mode 100644 laws/DechunkOnlyLast.bend create mode 100644 laws/DechunkStopsAtZero.bend create mode 100644 laws/DechunkTwoChunks.bend create mode 100644 laws/DechunkWiki.bend create mode 100644 laws/DeleteReqNamesPath.bend create mode 100644 laws/DfltHttps.bend create mode 100644 laws/DfltPlain.bend create mode 100644 laws/DigitsReadDecimal.bend create mode 100644 laws/DigitsReadRejectsJunk.bend create mode 100644 laws/DotSegmentsRemoved.bend create mode 100644 laws/ErrnoNamesAreLinux.bend create mode 100644 laws/ExchangeCarriesMethod.bend create mode 100644 laws/FailedEffectsLoseConnection.bend create mode 100644 laws/FailuresReportedByPhase.bend create mode 100644 laws/FirstIsFirstWord.bend create mode 100644 laws/FormBodyLayout.bend create mode 100644 laws/FormBoundaryRules.bend create mode 100644 laws/FormNamesEscaped.bend create mode 100644 laws/FormPartHeaders.bend create mode 100644 laws/FormRefusesBadRequests.bend create mode 100644 laws/FreshMaxAgeLimit.bend create mode 100644 laws/FreshMaxAgeOne.bend create mode 100644 laws/FreshMaxAgeOpen.bend create mode 100644 laws/FreshMaxAgeOverridesExpires.bend create mode 100644 laws/FreshMaxAgeZero.bend create mode 100644 laws/FreshNoCache.bend create mode 100644 laws/FreshNoCacheAny.bend create mode 100644 laws/FreshNoStore.bend create mode 100644 laws/FreshNoStoreAny.bend create mode 100644 laws/FreshPublicOnly.bend create mode 100644 laws/GetOmitsContentLength.bend create mode 100644 laws/GetReplyKeepsBody.bend create mode 100644 laws/GetReqNamesPath.bend create mode 100644 laws/HandlerGetsRequestText.bend create mode 100644 laws/HeadAndNoContentHaveNoBody.bend create mode 100644 laws/HeadHitIgnoresCase.bend create mode 100644 laws/HeadLineRejectsContinuation.bend create mode 100644 laws/HeadLineRejectsObsFold.bend create mode 100644 laws/HeadLineTrims.bend create mode 100644 laws/HeadPlannedBeforeBody.bend create mode 100644 laws/HeadReplyDropsBody.bend create mode 100644 laws/HeadReqNamesPath.bend create mode 100644 laws/HeadersFindFirst.bend create mode 100644 laws/HeadersFindIgnoresCase.bend create mode 100644 laws/HeadersFindMissing.bend create mode 100644 laws/HexlenAlpha.bend create mode 100644 laws/HexlenExtensionAndCase.bend create mode 100644 laws/HexlenIgnoresExtension.bend create mode 100644 laws/HtmlEscapeCharByChar.bend create mode 100644 laws/HtmlEscapeIsSafe.bend create mode 100644 laws/HtmlEscapesFiveChars.bend create mode 100644 laws/HtmlKeepsOtherChars.bend create mode 100644 laws/InterimResponsesSkipped.bend create mode 100644 laws/InterimStatusIs1xx.bend create mode 100644 laws/JoinJoined.bend create mode 100644 laws/JsonParseRejects.bend create mode 100644 laws/JsonParseString.bend create mode 100644 laws/JsonStringIsTextBody.bend create mode 100644 laws/LocationResolvedBySort.bend create mode 100644 laws/LocationSplitsAtQuery.bend create mode 100644 laws/MethodIdempotentDelete.bend create mode 100644 laws/MethodIdempotentGet.bend create mode 100644 laws/MethodIdempotentHead.bend create mode 100644 laws/MethodIdempotentOptions.bend create mode 100644 laws/MethodIdempotentPatch.bend create mode 100644 laws/MethodIdempotentPost.bend create mode 100644 laws/MethodIdempotentPut.bend create mode 100644 laws/MethodSafeDelete.bend create mode 100644 laws/MethodSafeGet.bend create mode 100644 laws/MethodSafeGetLower.bend create mode 100644 laws/MethodSafeHead.bend create mode 100644 laws/MethodSafeOptions.bend create mode 100644 laws/MethodSafePost.bend create mode 100644 laws/MethodSafePut.bend create mode 100644 laws/MethodTokensAreCaseSensitive.bend create mode 100644 laws/NamedReferencesLongestMatch.bend create mode 100644 laws/NumericReferencesDecode.bend create mode 100644 laws/OctetBodiesSentUnchanged.bend create mode 100644 laws/OctetsDecodeAsUtf8.bend create mode 100644 laws/OneOctetPerChar.bend create mode 100644 laws/OnlyGetFollows.bend create mode 100644 laws/OptionsReqNamesPath.bend create mode 100644 laws/OtherStatusesStopChain.bend create mode 100644 laws/ParseChunkedBody.bend create mode 100644 laws/ParseChunkedIgnoresCase.bend create mode 100644 laws/ParseClenTakesPrefix.bend create mode 100644 laws/ParseEqualContentLengths.bend create mode 100644 laws/ParseNeedsBlankLine.bend create mode 100644 laws/ParseNeedsSlashes.bend create mode 100644 laws/ParseNeedsStatus.bend create mode 100644 laws/ParseQuerySplitsPairs.bend create mode 100644 laws/ParseRejectsClAndTe.bend create mode 100644 laws/ParseRejectsDisagreeingCl.bend create mode 100644 laws/ParseRejectsNonHexChunk.bend create mode 100644 laws/ParseSchemeFirst.bend create mode 100644 laws/PathAbsolute.bend create mode 100644 laws/PathFormKeepsSlash.bend create mode 100644 laws/PhraseTableLookup.bend create mode 100644 laws/PostReqNamesPath.bend create mode 100644 laws/PostWritesContentLength.bend create mode 100644 laws/PutReqNamesPath.bend create mode 100644 laws/QueryGetLastWins.bend create mode 100644 laws/QuoteEscapesUtf8Bytes.bend create mode 100644 laws/QuoteKeepsUnreserved.bend create mode 100644 laws/QuoteOutputIsFramed.bend create mode 100644 laws/QuotePlusWritesSpaces.bend create mode 100644 laws/ReadStateMatchesWholeText.bend create mode 100644 laws/ReadsJudgedByCount.bend create mode 100644 laws/Reason200.bend create mode 100644 laws/Reason201.bend create mode 100644 laws/Reason204.bend create mode 100644 laws/Reason400.bend create mode 100644 laws/Reason404.bend create mode 100644 laws/Reason405.bend create mode 100644 laws/Reason500.bend create mode 100644 laws/ReasonPhraseFromFinalHead.bend create mode 100644 laws/ReasonPhrasesFollowRfc.bend create mode 100644 laws/ReasonUnknownEmpty.bend create mode 100644 laws/RedirectsFollowLocation.bend create mode 100644 laws/RelativeLocationsMerge.bend create mode 100644 laws/ReplyHeadFramesBody.bend create mode 100644 laws/RequestHeaderFieldsOrdered.bend create mode 100644 laws/RequestLineAndHost.bend create mode 100644 laws/RequestNamesMethod.bend create mode 100644 laws/Respond200KeepsBody.bend create mode 100644 laws/Respond204DropsBody.bend create mode 100644 laws/Respond304DropsBody.bend create mode 100644 laws/RespondOpensWithVersion.bend create mode 100644 laws/ResponseCompleteByFraming.bend create mode 100644 laws/SecondIsNextWord.bend create mode 100644 laws/SetCookieFieldsSelected.bend create mode 100644 laws/SetCookieReadsPair.bend create mode 100644 laws/ShowReplyOpensWithStatus.bend create mode 100644 laws/ShowTornIsMarked.bend create mode 100644 laws/SplitAtCharacter.bend create mode 100644 laws/StreamedRepliesAreChunked.bend create mode 100644 laws/TimeoutZeroIsOneMs.bend create mode 100644 laws/TimeoutsReadAsTimedOut.bend create mode 100644 laws/TokenOkNewline.bend create mode 100644 laws/TokenOkPlain.bend create mode 100644 laws/TokenOkSpace.bend create mode 100644 laws/TokenOkTab.bend create mode 100644 laws/UnescapeFindsReferences.bend create mode 100644 laws/UnescapeUndoesEscape.bend create mode 100644 laws/UnframedIsRest.bend create mode 100644 laws/UnquotePlusReadsSpaces.bend create mode 100644 laws/UnquoteReadsEscapes.bend create mode 100644 laws/UrlBadPortUsesDefault.bend create mode 100644 laws/UrlEmptyPath.bend create mode 100644 laws/UrlFragmentHidesQuery.bend create mode 100644 laws/UrlHttpDefaultPort.bend create mode 100644 laws/UrlHttpsDefaultPort.bend create mode 100644 laws/UrlHttpsExplicitPort.bend create mode 100644 laws/UrlIpv4Host.bend create mode 100644 laws/UrlIpv4Port.bend create mode 100644 laws/UrlQueryDropsFragment.bend create mode 100644 laws/UrlQueryKeepsQuestion.bend create mode 100644 laws/UrlQueryOnEmptyPath.bend create mode 100644 laws/UrlSchemeLowercased.bend create mode 100644 laws/UrlShowBadIsMarked.bend create mode 100644 laws/UrlShowOpensWithScheme.bend create mode 100644 laws/UrlTrailingSlash.bend create mode 100644 laws/UrlUserinfoLastAt.bend create mode 100644 laws/UrlUserinfoStripped.bend create mode 100644 laws/UrlencodeJoinsPairs.bend create mode 100644 laws/Utf8DecodesWithReplacement.bend create mode 100644 laws/Utf8Split.bend create mode 100644 laws/WithServesGetOctetsAndContext.bend create mode 100644 laws/defs/CharacterAbsent.bend create mode 100644 laws/defs/PercentFrame.bend create mode 100644 proofs/AbsoluteLocationsResolve.bend create mode 100644 proofs/AskClenExact.bend create mode 100644 proofs/AskClenShort.bend create mode 100644 proofs/AskEqualContentLengths.bend create mode 100644 proofs/AskGetAsteriskRejected.bend create mode 100644 proofs/AskKeepsFieldNameSpelling.bend create mode 100644 proofs/AskNeedsBlankLine.bend create mode 100644 proofs/AskNeedsRequestLine.bend create mode 100644 proofs/AskOptionsAsterisk.bend create mode 100644 proofs/AskOriginFormKeepsQuery.bend create mode 100644 proofs/AskRejectsAbsoluteForm.bend create mode 100644 proofs/AskRejectsClAndTe.bend create mode 100644 proofs/AskRejectsDisagreeingCl.bend create mode 100644 proofs/AskRejectsRelativeTarget.bend create mode 100644 proofs/AskShowBadIsMarked.bend create mode 100644 proofs/B64Empty.bend create mode 100644 proofs/B64F.bend create mode 100644 proofs/B64Fo.bend create mode 100644 proofs/B64Foo.bend create mode 100644 proofs/B64GoEnd.bend create mode 100644 proofs/B64UserPass.bend create mode 100644 proofs/BadRequestAnswers400.bend create mode 100644 proofs/BareRepliesWriteHeadOnly.bend create mode 100644 proofs/BasicUserPass.bend create mode 100644 proofs/BearerIsAuthorization.bend create mode 100644 proofs/CacheDirectiveCase.bend create mode 100644 proofs/CacheDirectives.bend create mode 100644 proofs/CacheIgnoresSMaxage.bend create mode 100644 proofs/CacheMaxAgeIgnoresJunk.bend create mode 100644 proofs/CacheMaxAgePresent.bend create mode 100644 proofs/CacheNoCacheAlone.bend create mode 100644 proofs/CacheNoteExpires.bend create mode 100644 proofs/CachePrivateAlone.bend create mode 100644 proofs/ChunkDataUnchanged.bend create mode 100644 proofs/ChunksEmptyFuel.bend create mode 100644 proofs/ClenExact.bend create mode 100644 proofs/ContentLengthFramesBody.bend create mode 100644 proofs/CookieAgeSets.bend create mode 100644 proofs/CookieDefaultPathDir.bend create mode 100644 proofs/CookieDefaultPathOneSlash.bend create mode 100644 proofs/CookieDefaultPathRoot.bend create mode 100644 proofs/CookieDefaultPathTrailing.bend create mode 100644 proofs/CookieDomainIpExact.bend create mode 100644 proofs/CookieDomainIpSuffix.bend create mode 100644 proofs/CookieDomainLowercased.bend create mode 100644 proofs/CookieDomainNotSuffix.bend create mode 100644 proofs/CookieDomainSuffix.bend create mode 100644 proofs/CookieHeaderEchoesStore.bend create mode 100644 proofs/CookieInsecureOnTcp.bend create mode 100644 proofs/CookieIpv4Literal.bend create mode 100644 proofs/CookieIpv4Name.bend create mode 100644 proofs/CookieLineAttributes.bend create mode 100644 proofs/CookieMaxAgeIgnoresJunk.bend create mode 100644 proofs/CookieMaxAgeOne.bend create mode 100644 proofs/CookieMaxAgeOpen.bend create mode 100644 proofs/CookieMaxAgeZero.bend create mode 100644 proofs/CookieParseRoundTrip.bend create mode 100644 proofs/CookiePathIdentical.bend create mode 100644 proofs/CookiePathLongerCookie.bend create mode 100644 proofs/CookiePathNotPrefix.bend create mode 100644 proofs/CookiePathPrefix.bend create mode 100644 proofs/CookiePathRoot.bend create mode 100644 proofs/CookiePathSlashPrefix.bend create mode 100644 proofs/CookieReceiveHostOnly.bend create mode 100644 proofs/CookieRejectsEmptyName.bend create mode 100644 proofs/CookieRequestJoins.bend create mode 100644 proofs/CookieSamesiteNone.bend create mode 100644 proofs/CookieSamesiteStrict.bend create mode 100644 proofs/CookieSamesiteUnknown.bend create mode 100644 proofs/CookieSecureNeedsTls.bend create mode 100644 proofs/CookieSecureOnTls.bend create mode 100644 proofs/CookieSendHostOnly.bend create mode 100644 proofs/CookieSendOtherHost.bend create mode 100644 proofs/CookieSendUnsetDomain.bend create mode 100644 proofs/CookieSessionAlive.bend create mode 100644 proofs/CorsCredentialsExplicitOrigin.bend create mode 100644 proofs/CorsCredentialsReflectOrigin.bend create mode 100644 proofs/CorsExplicitOrigin.bend create mode 100644 proofs/CorsGetIsNotPreflight.bend create mode 100644 proofs/CorsOnPreflight.bend create mode 100644 proofs/CorsOriginHeader.bend create mode 100644 proofs/CorsPreflightAllowed.bend create mode 100644 proofs/CorsPreflightBad.bend create mode 100644 proofs/CorsPreflightHeaderCase.bend create mode 100644 proofs/CorsPreflightHeaderDenied.bend create mode 100644 proofs/CorsPreflightMethodDenied.bend create mode 100644 proofs/CorsPreflightOriginDenied.bend create mode 100644 proofs/CorsPreflightStar.bend create mode 100644 proofs/CorsRefusedOrigin.bend create mode 100644 proofs/CorsRequestHeadersHeader.bend create mode 100644 proofs/CorsRequestMethodHeader.bend create mode 100644 proofs/CorsSimpleReflects.bend create mode 100644 proofs/CorsSimpleRefused.bend create mode 100644 proofs/CorsSimpleStar.bend create mode 100644 proofs/CorsStarWithoutCredentials.bend create mode 100644 proofs/CorsVaryEmpty.bend create mode 100644 proofs/CorsVarySpecific.bend create mode 100644 proofs/CorsVaryStar.bend create mode 100644 proofs/CutAtFirstEquals.bend create mode 100644 proofs/DeadlineIsTimeout.bend create mode 100644 proofs/DechunkEmpty.bend create mode 100644 proofs/DechunkIgnoresExtension.bend create mode 100644 proofs/DechunkOnlyLast.bend create mode 100644 proofs/DechunkStopsAtZero.bend create mode 100644 proofs/DechunkTwoChunks.bend create mode 100644 proofs/DechunkWiki.bend create mode 100644 proofs/DeleteReqNamesPath.bend create mode 100644 proofs/DfltHttps.bend create mode 100644 proofs/DfltPlain.bend create mode 100644 proofs/DigitsReadDecimal.bend create mode 100644 proofs/DigitsReadRejectsJunk.bend create mode 100644 proofs/DotSegmentsRemoved.bend create mode 100644 proofs/ErrnoNamesAreLinux.bend create mode 100644 proofs/ExchangeCarriesMethod.bend create mode 100644 proofs/FailedEffectsLoseConnection.bend create mode 100644 proofs/FailuresReportedByPhase.bend create mode 100644 proofs/FirstIsFirstWord.bend create mode 100644 proofs/FormBodyLayout.bend create mode 100644 proofs/FormBoundaryRules.bend create mode 100644 proofs/FormNamesEscaped.bend create mode 100644 proofs/FormPartHeaders.bend create mode 100644 proofs/FormRefusesBadRequests.bend create mode 100644 proofs/FreshMaxAgeLimit.bend create mode 100644 proofs/FreshMaxAgeOne.bend create mode 100644 proofs/FreshMaxAgeOpen.bend create mode 100644 proofs/FreshMaxAgeOverridesExpires.bend create mode 100644 proofs/FreshMaxAgeZero.bend create mode 100644 proofs/FreshNoCache.bend create mode 100644 proofs/FreshNoCacheAny.bend create mode 100644 proofs/FreshNoStore.bend create mode 100644 proofs/FreshNoStoreAny.bend create mode 100644 proofs/FreshPublicOnly.bend create mode 100644 proofs/GetOmitsContentLength.bend create mode 100644 proofs/GetReplyKeepsBody.bend create mode 100644 proofs/GetReqNamesPath.bend create mode 100644 proofs/HandlerGetsRequestText.bend create mode 100644 proofs/HeadAndNoContentHaveNoBody.bend create mode 100644 proofs/HeadHitIgnoresCase.bend create mode 100644 proofs/HeadLineRejectsContinuation.bend create mode 100644 proofs/HeadLineRejectsObsFold.bend create mode 100644 proofs/HeadLineTrims.bend create mode 100644 proofs/HeadPlannedBeforeBody.bend create mode 100644 proofs/HeadReplyDropsBody.bend create mode 100644 proofs/HeadReqNamesPath.bend create mode 100644 proofs/HeadersFindFirst.bend create mode 100644 proofs/HeadersFindIgnoresCase.bend create mode 100644 proofs/HeadersFindMissing.bend create mode 100644 proofs/HexlenAlpha.bend create mode 100644 proofs/HexlenExtensionAndCase.bend create mode 100644 proofs/HexlenIgnoresExtension.bend create mode 100644 proofs/HtmlEscapeCharByChar.bend create mode 100644 proofs/HtmlEscapeIsSafe.bend create mode 100644 proofs/HtmlEscapesFiveChars.bend create mode 100644 proofs/HtmlKeepsOtherChars.bend create mode 100644 proofs/InterimResponsesSkipped.bend create mode 100644 proofs/InterimStatusIs1xx.bend create mode 100644 proofs/JoinJoined.bend create mode 100644 proofs/JsonParseRejects.bend create mode 100644 proofs/JsonParseString.bend create mode 100644 proofs/JsonStringIsTextBody.bend create mode 100644 proofs/LocationResolvedBySort.bend create mode 100644 proofs/LocationSplitsAtQuery.bend create mode 100644 proofs/MethodIdempotentDelete.bend create mode 100644 proofs/MethodIdempotentGet.bend create mode 100644 proofs/MethodIdempotentHead.bend create mode 100644 proofs/MethodIdempotentOptions.bend create mode 100644 proofs/MethodIdempotentPatch.bend create mode 100644 proofs/MethodIdempotentPost.bend create mode 100644 proofs/MethodIdempotentPut.bend create mode 100644 proofs/MethodSafeDelete.bend create mode 100644 proofs/MethodSafeGet.bend create mode 100644 proofs/MethodSafeGetLower.bend create mode 100644 proofs/MethodSafeHead.bend create mode 100644 proofs/MethodSafeOptions.bend create mode 100644 proofs/MethodSafePost.bend create mode 100644 proofs/MethodSafePut.bend create mode 100644 proofs/MethodTokensAreCaseSensitive.bend create mode 100644 proofs/NamedReferencesLongestMatch.bend create mode 100644 proofs/NumericReferencesDecode.bend create mode 100644 proofs/OctetBodiesSentUnchanged.bend create mode 100644 proofs/OctetsDecodeAsUtf8.bend create mode 100644 proofs/OneOctetPerChar.bend create mode 100644 proofs/OnlyGetFollows.bend create mode 100644 proofs/OptionsReqNamesPath.bend create mode 100644 proofs/OtherStatusesStopChain.bend create mode 100644 proofs/ParseChunkedBody.bend create mode 100644 proofs/ParseChunkedIgnoresCase.bend create mode 100644 proofs/ParseClenTakesPrefix.bend create mode 100644 proofs/ParseEqualContentLengths.bend create mode 100644 proofs/ParseNeedsBlankLine.bend create mode 100644 proofs/ParseNeedsSlashes.bend create mode 100644 proofs/ParseNeedsStatus.bend create mode 100644 proofs/ParseQuerySplitsPairs.bend create mode 100644 proofs/ParseRejectsClAndTe.bend create mode 100644 proofs/ParseRejectsDisagreeingCl.bend create mode 100644 proofs/ParseRejectsNonHexChunk.bend create mode 100644 proofs/ParseSchemeFirst.bend create mode 100644 proofs/PathAbsolute.bend create mode 100644 proofs/PathFormKeepsSlash.bend create mode 100644 proofs/PhraseTableLookup.bend create mode 100644 proofs/PostReqNamesPath.bend create mode 100644 proofs/PostWritesContentLength.bend create mode 100644 proofs/PutReqNamesPath.bend create mode 100644 proofs/QueryGetLastWins.bend create mode 100644 proofs/QuoteEscapesUtf8Bytes.bend create mode 100644 proofs/QuoteKeepsUnreserved.bend create mode 100644 proofs/QuoteOutputIsFramed.bend create mode 100644 proofs/QuotePlusWritesSpaces.bend create mode 100644 proofs/ReadStateMatchesWholeText.bend create mode 100644 proofs/ReadsJudgedByCount.bend create mode 100644 proofs/Reason200.bend create mode 100644 proofs/Reason201.bend create mode 100644 proofs/Reason204.bend create mode 100644 proofs/Reason400.bend create mode 100644 proofs/Reason404.bend create mode 100644 proofs/Reason405.bend create mode 100644 proofs/Reason500.bend create mode 100644 proofs/ReasonPhraseFromFinalHead.bend create mode 100644 proofs/ReasonPhrasesFollowRfc.bend create mode 100644 proofs/ReasonUnknownEmpty.bend create mode 100644 proofs/RedirectsFollowLocation.bend create mode 100644 proofs/RelativeLocationsMerge.bend create mode 100644 proofs/ReplyHeadFramesBody.bend create mode 100644 proofs/RequestHeaderFieldsOrdered.bend create mode 100644 proofs/RequestLineAndHost.bend create mode 100644 proofs/RequestNamesMethod.bend create mode 100644 proofs/Respond200KeepsBody.bend create mode 100644 proofs/Respond204DropsBody.bend create mode 100644 proofs/Respond304DropsBody.bend create mode 100644 proofs/RespondOpensWithVersion.bend create mode 100644 proofs/ResponseCompleteByFraming.bend create mode 100644 proofs/SecondIsNextWord.bend create mode 100644 proofs/SetCookieFieldsSelected.bend create mode 100644 proofs/SetCookieReadsPair.bend create mode 100644 proofs/ShowReplyOpensWithStatus.bend create mode 100644 proofs/ShowTornIsMarked.bend create mode 100644 proofs/SplitAtCharacter.bend create mode 100644 proofs/StreamedRepliesAreChunked.bend create mode 100644 proofs/TimeoutZeroIsOneMs.bend create mode 100644 proofs/TimeoutsReadAsTimedOut.bend create mode 100644 proofs/TokenOkNewline.bend create mode 100644 proofs/TokenOkPlain.bend create mode 100644 proofs/TokenOkSpace.bend create mode 100644 proofs/TokenOkTab.bend create mode 100644 proofs/UnescapeFindsReferences.bend create mode 100644 proofs/UnescapeUndoesEscape.bend create mode 100644 proofs/UnframedIsRest.bend create mode 100644 proofs/UnquotePlusReadsSpaces.bend create mode 100644 proofs/UnquoteReadsEscapes.bend create mode 100644 proofs/UrlBadPortUsesDefault.bend create mode 100644 proofs/UrlEmptyPath.bend create mode 100644 proofs/UrlFragmentHidesQuery.bend create mode 100644 proofs/UrlHttpDefaultPort.bend create mode 100644 proofs/UrlHttpsDefaultPort.bend create mode 100644 proofs/UrlHttpsExplicitPort.bend create mode 100644 proofs/UrlIpv4Host.bend create mode 100644 proofs/UrlIpv4Port.bend create mode 100644 proofs/UrlQueryDropsFragment.bend create mode 100644 proofs/UrlQueryKeepsQuestion.bend create mode 100644 proofs/UrlQueryOnEmptyPath.bend create mode 100644 proofs/UrlSchemeLowercased.bend create mode 100644 proofs/UrlShowBadIsMarked.bend create mode 100644 proofs/UrlShowOpensWithScheme.bend create mode 100644 proofs/UrlTrailingSlash.bend create mode 100644 proofs/UrlUserinfoLastAt.bend create mode 100644 proofs/UrlUserinfoStripped.bend create mode 100644 proofs/UrlencodeJoinsPairs.bend create mode 100644 proofs/Utf8DecodesWithReplacement.bend create mode 100644 proofs/Utf8Split.bend create mode 100644 proofs/WithServesGetOctetsAndContext.bend create mode 100644 proofs/lemmas/BaseEqualities.bend create mode 100644 proofs/lemmas/DropNothing.bend create mode 100644 proofs/lemmas/HeaderFieldSlots.bend create mode 100644 proofs/lemmas/NatOrder.bend create mode 100644 proofs/lemmas/OctetLength.bend create mode 100644 proofs/lemmas/OctetTakeDrop.bend create mode 100644 proofs/lemmas/WalksOverAppendedText.bend diff --git a/LAWS.bend b/LAWS.bend index 30acb7f..894e47c 100644 --- a/LAWS.bend +++ b/LAWS.bend @@ -1,5560 +1,305 @@ -# 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 named after its one law, with no prose +# (laws/PathAbsolute.bend, laws/CacheDirectives.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/AbsoluteLocationsResolve.bend as AbsoluteLocationsResolve +import ./laws/AskClenExact.bend as AskClenExact +import ./laws/AskClenShort.bend as AskClenShort +import ./laws/AskEqualContentLengths.bend as AskEqualContentLengths +import ./laws/AskGetAsteriskRejected.bend as AskGetAsteriskRejected +import ./laws/AskKeepsFieldNameSpelling.bend as AskKeepsFieldNameSpelling +import ./laws/AskNeedsBlankLine.bend as AskNeedsBlankLine +import ./laws/AskNeedsRequestLine.bend as AskNeedsRequestLine +import ./laws/AskOptionsAsterisk.bend as AskOptionsAsterisk +import ./laws/AskOriginFormKeepsQuery.bend as AskOriginFormKeepsQuery +import ./laws/AskRejectsAbsoluteForm.bend as AskRejectsAbsoluteForm +import ./laws/AskRejectsClAndTe.bend as AskRejectsClAndTe +import ./laws/AskRejectsDisagreeingCl.bend as AskRejectsDisagreeingCl +import ./laws/AskRejectsRelativeTarget.bend as AskRejectsRelativeTarget +import ./laws/AskShowBadIsMarked.bend as AskShowBadIsMarked +import ./laws/B64Empty.bend as B64Empty +import ./laws/B64F.bend as B64F +import ./laws/B64Fo.bend as B64Fo +import ./laws/B64Foo.bend as B64Foo +import ./laws/B64GoEnd.bend as B64GoEnd +import ./laws/B64UserPass.bend as B64UserPass +import ./laws/BadRequestAnswers400.bend as BadRequestAnswers400 +import ./laws/BareRepliesWriteHeadOnly.bend as BareRepliesWriteHeadOnly +import ./laws/BasicUserPass.bend as BasicUserPass +import ./laws/BearerIsAuthorization.bend as BearerIsAuthorization +import ./laws/CacheDirectiveCase.bend as CacheDirectiveCase +import ./laws/CacheDirectives.bend as CacheDirectives +import ./laws/CacheIgnoresSMaxage.bend as CacheIgnoresSMaxage +import ./laws/CacheMaxAgeIgnoresJunk.bend as CacheMaxAgeIgnoresJunk +import ./laws/CacheMaxAgePresent.bend as CacheMaxAgePresent +import ./laws/CacheNoCacheAlone.bend as CacheNoCacheAlone +import ./laws/CacheNoteExpires.bend as CacheNoteExpires +import ./laws/CachePrivateAlone.bend as CachePrivateAlone +import ./laws/ChunkDataUnchanged.bend as ChunkDataUnchanged +import ./laws/ChunksEmptyFuel.bend as ChunksEmptyFuel +import ./laws/ClenExact.bend as ClenExact +import ./laws/ContentLengthFramesBody.bend as ContentLengthFramesBody +import ./laws/CookieAgeSets.bend as CookieAgeSets +import ./laws/CookieDefaultPathDir.bend as CookieDefaultPathDir +import ./laws/CookieDefaultPathOneSlash.bend as CookieDefaultPathOneSlash +import ./laws/CookieDefaultPathRoot.bend as CookieDefaultPathRoot +import ./laws/CookieDefaultPathTrailing.bend as CookieDefaultPathTrailing +import ./laws/CookieDomainIpExact.bend as CookieDomainIpExact +import ./laws/CookieDomainIpSuffix.bend as CookieDomainIpSuffix +import ./laws/CookieDomainLowercased.bend as CookieDomainLowercased +import ./laws/CookieDomainNotSuffix.bend as CookieDomainNotSuffix +import ./laws/CookieDomainSuffix.bend as CookieDomainSuffix +import ./laws/CookieHeaderEchoesStore.bend as CookieHeaderEchoesStore +import ./laws/CookieInsecureOnTcp.bend as CookieInsecureOnTcp +import ./laws/CookieIpv4Literal.bend as CookieIpv4Literal +import ./laws/CookieIpv4Name.bend as CookieIpv4Name +import ./laws/CookieLineAttributes.bend as CookieLineAttributes +import ./laws/CookieMaxAgeIgnoresJunk.bend as CookieMaxAgeIgnoresJunk +import ./laws/CookieMaxAgeOne.bend as CookieMaxAgeOne +import ./laws/CookieMaxAgeOpen.bend as CookieMaxAgeOpen +import ./laws/CookieMaxAgeZero.bend as CookieMaxAgeZero +import ./laws/CookieParseRoundTrip.bend as CookieParseRoundTrip +import ./laws/CookiePathIdentical.bend as CookiePathIdentical +import ./laws/CookiePathLongerCookie.bend as CookiePathLongerCookie +import ./laws/CookiePathNotPrefix.bend as CookiePathNotPrefix +import ./laws/CookiePathPrefix.bend as CookiePathPrefix +import ./laws/CookiePathRoot.bend as CookiePathRoot +import ./laws/CookiePathSlashPrefix.bend as CookiePathSlashPrefix +import ./laws/CookieReceiveHostOnly.bend as CookieReceiveHostOnly +import ./laws/CookieRejectsEmptyName.bend as CookieRejectsEmptyName +import ./laws/CookieRequestJoins.bend as CookieRequestJoins +import ./laws/CookieSamesiteNone.bend as CookieSamesiteNone +import ./laws/CookieSamesiteStrict.bend as CookieSamesiteStrict +import ./laws/CookieSamesiteUnknown.bend as CookieSamesiteUnknown +import ./laws/CookieSecureNeedsTls.bend as CookieSecureNeedsTls +import ./laws/CookieSecureOnTls.bend as CookieSecureOnTls +import ./laws/CookieSendHostOnly.bend as CookieSendHostOnly +import ./laws/CookieSendOtherHost.bend as CookieSendOtherHost +import ./laws/CookieSendUnsetDomain.bend as CookieSendUnsetDomain +import ./laws/CookieSessionAlive.bend as CookieSessionAlive +import ./laws/CorsCredentialsExplicitOrigin.bend as CorsCredentialsExplicitOrigin +import ./laws/CorsCredentialsReflectOrigin.bend as CorsCredentialsReflectOrigin +import ./laws/CorsExplicitOrigin.bend as CorsExplicitOrigin +import ./laws/CorsGetIsNotPreflight.bend as CorsGetIsNotPreflight +import ./laws/CorsOnPreflight.bend as CorsOnPreflight +import ./laws/CorsOriginHeader.bend as CorsOriginHeader +import ./laws/CorsPreflightAllowed.bend as CorsPreflightAllowed +import ./laws/CorsPreflightBad.bend as CorsPreflightBad +import ./laws/CorsPreflightHeaderCase.bend as CorsPreflightHeaderCase +import ./laws/CorsPreflightHeaderDenied.bend as CorsPreflightHeaderDenied +import ./laws/CorsPreflightMethodDenied.bend as CorsPreflightMethodDenied +import ./laws/CorsPreflightOriginDenied.bend as CorsPreflightOriginDenied +import ./laws/CorsPreflightStar.bend as CorsPreflightStar +import ./laws/CorsRefusedOrigin.bend as CorsRefusedOrigin +import ./laws/CorsRequestHeadersHeader.bend as CorsRequestHeadersHeader +import ./laws/CorsRequestMethodHeader.bend as CorsRequestMethodHeader +import ./laws/CorsSimpleReflects.bend as CorsSimpleReflects +import ./laws/CorsSimpleRefused.bend as CorsSimpleRefused +import ./laws/CorsSimpleStar.bend as CorsSimpleStar +import ./laws/CorsStarWithoutCredentials.bend as CorsStarWithoutCredentials +import ./laws/CorsVaryEmpty.bend as CorsVaryEmpty +import ./laws/CorsVarySpecific.bend as CorsVarySpecific +import ./laws/CorsVaryStar.bend as CorsVaryStar +import ./laws/CutAtFirstEquals.bend as CutAtFirstEquals +import ./laws/DeadlineIsTimeout.bend as DeadlineIsTimeout +import ./laws/DechunkEmpty.bend as DechunkEmpty +import ./laws/DechunkIgnoresExtension.bend as DechunkIgnoresExtension +import ./laws/DechunkOnlyLast.bend as DechunkOnlyLast +import ./laws/DechunkStopsAtZero.bend as DechunkStopsAtZero +import ./laws/DechunkTwoChunks.bend as DechunkTwoChunks +import ./laws/DechunkWiki.bend as DechunkWiki +import ./laws/DeleteReqNamesPath.bend as DeleteReqNamesPath +import ./laws/DfltHttps.bend as DfltHttps +import ./laws/DfltPlain.bend as DfltPlain +import ./laws/DigitsReadDecimal.bend as DigitsReadDecimal +import ./laws/DigitsReadRejectsJunk.bend as DigitsReadRejectsJunk +import ./laws/DotSegmentsRemoved.bend as DotSegmentsRemoved +import ./laws/ErrnoNamesAreLinux.bend as ErrnoNamesAreLinux +import ./laws/ExchangeCarriesMethod.bend as ExchangeCarriesMethod +import ./laws/FailedEffectsLoseConnection.bend as FailedEffectsLoseConnection +import ./laws/FailuresReportedByPhase.bend as FailuresReportedByPhase +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/FreshMaxAgeLimit.bend as FreshMaxAgeLimit +import ./laws/FreshMaxAgeOne.bend as FreshMaxAgeOne +import ./laws/FreshMaxAgeOpen.bend as FreshMaxAgeOpen +import ./laws/FreshMaxAgeOverridesExpires.bend as FreshMaxAgeOverridesExpires +import ./laws/FreshMaxAgeZero.bend as FreshMaxAgeZero +import ./laws/FreshNoCache.bend as FreshNoCache +import ./laws/FreshNoCacheAny.bend as FreshNoCacheAny +import ./laws/FreshNoStore.bend as FreshNoStore +import ./laws/FreshNoStoreAny.bend as FreshNoStoreAny +import ./laws/FreshPublicOnly.bend as FreshPublicOnly +import ./laws/GetOmitsContentLength.bend as GetOmitsContentLength +import ./laws/GetReplyKeepsBody.bend as GetReplyKeepsBody +import ./laws/GetReqNamesPath.bend as GetReqNamesPath +import ./laws/HandlerGetsRequestText.bend as HandlerGetsRequestText +import ./laws/HeadAndNoContentHaveNoBody.bend as HeadAndNoContentHaveNoBody +import ./laws/HeadHitIgnoresCase.bend as HeadHitIgnoresCase +import ./laws/HeadLineRejectsContinuation.bend as HeadLineRejectsContinuation +import ./laws/HeadLineRejectsObsFold.bend as HeadLineRejectsObsFold +import ./laws/HeadLineTrims.bend as HeadLineTrims +import ./laws/HeadPlannedBeforeBody.bend as HeadPlannedBeforeBody +import ./laws/HeadReplyDropsBody.bend as HeadReplyDropsBody +import ./laws/HeadReqNamesPath.bend as HeadReqNamesPath +import ./laws/HeadersFindFirst.bend as HeadersFindFirst +import ./laws/HeadersFindIgnoresCase.bend as HeadersFindIgnoresCase +import ./laws/HeadersFindMissing.bend as HeadersFindMissing +import ./laws/HexlenAlpha.bend as HexlenAlpha +import ./laws/HexlenExtensionAndCase.bend as HexlenExtensionAndCase +import ./laws/HexlenIgnoresExtension.bend as HexlenIgnoresExtension +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/InterimResponsesSkipped.bend as InterimResponsesSkipped +import ./laws/InterimStatusIs1xx.bend as InterimStatusIs1xx +import ./laws/JoinJoined.bend as JoinJoined +import ./laws/JsonParseRejects.bend as JsonParseRejects +import ./laws/JsonParseString.bend as JsonParseString +import ./laws/JsonStringIsTextBody.bend as JsonStringIsTextBody +import ./laws/LocationResolvedBySort.bend as LocationResolvedBySort +import ./laws/LocationSplitsAtQuery.bend as LocationSplitsAtQuery +import ./laws/MethodIdempotentDelete.bend as MethodIdempotentDelete +import ./laws/MethodIdempotentGet.bend as MethodIdempotentGet +import ./laws/MethodIdempotentHead.bend as MethodIdempotentHead +import ./laws/MethodIdempotentOptions.bend as MethodIdempotentOptions +import ./laws/MethodIdempotentPatch.bend as MethodIdempotentPatch +import ./laws/MethodIdempotentPost.bend as MethodIdempotentPost +import ./laws/MethodIdempotentPut.bend as MethodIdempotentPut +import ./laws/MethodSafeDelete.bend as MethodSafeDelete +import ./laws/MethodSafeGet.bend as MethodSafeGet +import ./laws/MethodSafeGetLower.bend as MethodSafeGetLower +import ./laws/MethodSafeHead.bend as MethodSafeHead +import ./laws/MethodSafeOptions.bend as MethodSafeOptions +import ./laws/MethodSafePost.bend as MethodSafePost +import ./laws/MethodSafePut.bend as MethodSafePut +import ./laws/MethodTokensAreCaseSensitive.bend as MethodTokensAreCaseSensitive +import ./laws/NamedReferencesLongestMatch.bend as NamedReferencesLongestMatch +import ./laws/NumericReferencesDecode.bend as NumericReferencesDecode +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/OptionsReqNamesPath.bend as OptionsReqNamesPath +import ./laws/OtherStatusesStopChain.bend as OtherStatusesStopChain +import ./laws/ParseChunkedBody.bend as ParseChunkedBody +import ./laws/ParseChunkedIgnoresCase.bend as ParseChunkedIgnoresCase +import ./laws/ParseClenTakesPrefix.bend as ParseClenTakesPrefix +import ./laws/ParseEqualContentLengths.bend as ParseEqualContentLengths +import ./laws/ParseNeedsBlankLine.bend as ParseNeedsBlankLine +import ./laws/ParseNeedsSlashes.bend as ParseNeedsSlashes +import ./laws/ParseNeedsStatus.bend as ParseNeedsStatus +import ./laws/ParseQuerySplitsPairs.bend as ParseQuerySplitsPairs +import ./laws/ParseRejectsClAndTe.bend as ParseRejectsClAndTe +import ./laws/ParseRejectsDisagreeingCl.bend as ParseRejectsDisagreeingCl +import ./laws/ParseRejectsNonHexChunk.bend as ParseRejectsNonHexChunk +import ./laws/ParseSchemeFirst.bend as ParseSchemeFirst +import ./laws/PathAbsolute.bend as PathAbsolute +import ./laws/PathFormKeepsSlash.bend as PathFormKeepsSlash +import ./laws/PhraseTableLookup.bend as PhraseTableLookup +import ./laws/PostReqNamesPath.bend as PostReqNamesPath +import ./laws/PostWritesContentLength.bend as PostWritesContentLength +import ./laws/PutReqNamesPath.bend as PutReqNamesPath +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/Reason200.bend as Reason200 +import ./laws/Reason201.bend as Reason201 +import ./laws/Reason204.bend as Reason204 +import ./laws/Reason400.bend as Reason400 +import ./laws/Reason404.bend as Reason404 +import ./laws/Reason405.bend as Reason405 +import ./laws/Reason500.bend as Reason500 +import ./laws/ReasonPhraseFromFinalHead.bend as ReasonPhraseFromFinalHead +import ./laws/ReasonPhrasesFollowRfc.bend as ReasonPhrasesFollowRfc +import ./laws/ReasonUnknownEmpty.bend as ReasonUnknownEmpty +import ./laws/RedirectsFollowLocation.bend as RedirectsFollowLocation +import ./laws/RelativeLocationsMerge.bend as RelativeLocationsMerge +import ./laws/ReplyHeadFramesBody.bend as ReplyHeadFramesBody +import ./laws/RequestHeaderFieldsOrdered.bend as RequestHeaderFieldsOrdered +import ./laws/RequestLineAndHost.bend as RequestLineAndHost +import ./laws/RequestNamesMethod.bend as RequestNamesMethod +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/SecondIsNextWord.bend as SecondIsNextWord +import ./laws/SetCookieFieldsSelected.bend as SetCookieFieldsSelected +import ./laws/SetCookieReadsPair.bend as SetCookieReadsPair +import ./laws/ShowReplyOpensWithStatus.bend as ShowReplyOpensWithStatus +import ./laws/ShowTornIsMarked.bend as ShowTornIsMarked +import ./laws/SplitAtCharacter.bend as SplitAtCharacter +import ./laws/StreamedRepliesAreChunked.bend as StreamedRepliesAreChunked +import ./laws/TimeoutZeroIsOneMs.bend as TimeoutZeroIsOneMs +import ./laws/TimeoutsReadAsTimedOut.bend as TimeoutsReadAsTimedOut +import ./laws/TokenOkNewline.bend as TokenOkNewline +import ./laws/TokenOkPlain.bend as TokenOkPlain +import ./laws/TokenOkSpace.bend as TokenOkSpace +import ./laws/TokenOkTab.bend as TokenOkTab +import ./laws/UnescapeFindsReferences.bend as UnescapeFindsReferences +import ./laws/UnescapeUndoesEscape.bend as UnescapeUndoesEscape +import ./laws/UnframedIsRest.bend as UnframedIsRest +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/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/Utf8Split.bend as Utf8Split +import ./laws/WithServesGetOctetsAndContext.bend as WithServesGetOctetsAndContext diff --git a/PROOF.bend b/PROOF.bend index bcb31e6..af0b32d 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/AbsoluteLocationsResolve.bend as PAbsoluteLocationsResolve +import ./proofs/AskClenExact.bend as PAskClenExact +import ./proofs/AskClenShort.bend as PAskClenShort +import ./proofs/AskEqualContentLengths.bend as PAskEqualContentLengths +import ./proofs/AskGetAsteriskRejected.bend as PAskGetAsteriskRejected +import ./proofs/AskKeepsFieldNameSpelling.bend as PAskKeepsFieldNameSpelling +import ./proofs/AskNeedsBlankLine.bend as PAskNeedsBlankLine +import ./proofs/AskNeedsRequestLine.bend as PAskNeedsRequestLine +import ./proofs/AskOptionsAsterisk.bend as PAskOptionsAsterisk +import ./proofs/AskOriginFormKeepsQuery.bend as PAskOriginFormKeepsQuery +import ./proofs/AskRejectsAbsoluteForm.bend as PAskRejectsAbsoluteForm +import ./proofs/AskRejectsClAndTe.bend as PAskRejectsClAndTe +import ./proofs/AskRejectsDisagreeingCl.bend as PAskRejectsDisagreeingCl +import ./proofs/AskRejectsRelativeTarget.bend as PAskRejectsRelativeTarget +import ./proofs/AskShowBadIsMarked.bend as PAskShowBadIsMarked +import ./proofs/B64Empty.bend as PB64Empty +import ./proofs/B64F.bend as PB64F +import ./proofs/B64Fo.bend as PB64Fo +import ./proofs/B64Foo.bend as PB64Foo +import ./proofs/B64GoEnd.bend as PB64GoEnd +import ./proofs/B64UserPass.bend as PB64UserPass +import ./proofs/BadRequestAnswers400.bend as PBadRequestAnswers400 +import ./proofs/BareRepliesWriteHeadOnly.bend as PBareRepliesWriteHeadOnly +import ./proofs/BasicUserPass.bend as PBasicUserPass +import ./proofs/BearerIsAuthorization.bend as PBearerIsAuthorization +import ./proofs/CacheDirectiveCase.bend as PCacheDirectiveCase +import ./proofs/CacheDirectives.bend as PCacheDirectives +import ./proofs/CacheIgnoresSMaxage.bend as PCacheIgnoresSMaxage +import ./proofs/CacheMaxAgeIgnoresJunk.bend as PCacheMaxAgeIgnoresJunk +import ./proofs/CacheMaxAgePresent.bend as PCacheMaxAgePresent +import ./proofs/CacheNoCacheAlone.bend as PCacheNoCacheAlone +import ./proofs/CacheNoteExpires.bend as PCacheNoteExpires +import ./proofs/CachePrivateAlone.bend as PCachePrivateAlone +import ./proofs/ChunkDataUnchanged.bend as PChunkDataUnchanged +import ./proofs/ChunksEmptyFuel.bend as PChunksEmptyFuel +import ./proofs/ClenExact.bend as PClenExact +import ./proofs/ContentLengthFramesBody.bend as PContentLengthFramesBody +import ./proofs/CookieAgeSets.bend as PCookieAgeSets +import ./proofs/CookieDefaultPathDir.bend as PCookieDefaultPathDir +import ./proofs/CookieDefaultPathOneSlash.bend as PCookieDefaultPathOneSlash +import ./proofs/CookieDefaultPathRoot.bend as PCookieDefaultPathRoot +import ./proofs/CookieDefaultPathTrailing.bend as PCookieDefaultPathTrailing +import ./proofs/CookieDomainIpExact.bend as PCookieDomainIpExact +import ./proofs/CookieDomainIpSuffix.bend as PCookieDomainIpSuffix +import ./proofs/CookieDomainLowercased.bend as PCookieDomainLowercased +import ./proofs/CookieDomainNotSuffix.bend as PCookieDomainNotSuffix +import ./proofs/CookieDomainSuffix.bend as PCookieDomainSuffix +import ./proofs/CookieHeaderEchoesStore.bend as PCookieHeaderEchoesStore +import ./proofs/CookieInsecureOnTcp.bend as PCookieInsecureOnTcp +import ./proofs/CookieIpv4Literal.bend as PCookieIpv4Literal +import ./proofs/CookieIpv4Name.bend as PCookieIpv4Name +import ./proofs/CookieLineAttributes.bend as PCookieLineAttributes +import ./proofs/CookieMaxAgeIgnoresJunk.bend as PCookieMaxAgeIgnoresJunk +import ./proofs/CookieMaxAgeOne.bend as PCookieMaxAgeOne +import ./proofs/CookieMaxAgeOpen.bend as PCookieMaxAgeOpen +import ./proofs/CookieMaxAgeZero.bend as PCookieMaxAgeZero +import ./proofs/CookieParseRoundTrip.bend as PCookieParseRoundTrip +import ./proofs/CookiePathIdentical.bend as PCookiePathIdentical +import ./proofs/CookiePathLongerCookie.bend as PCookiePathLongerCookie +import ./proofs/CookiePathNotPrefix.bend as PCookiePathNotPrefix +import ./proofs/CookiePathPrefix.bend as PCookiePathPrefix +import ./proofs/CookiePathRoot.bend as PCookiePathRoot +import ./proofs/CookiePathSlashPrefix.bend as PCookiePathSlashPrefix +import ./proofs/CookieReceiveHostOnly.bend as PCookieReceiveHostOnly +import ./proofs/CookieRejectsEmptyName.bend as PCookieRejectsEmptyName +import ./proofs/CookieRequestJoins.bend as PCookieRequestJoins +import ./proofs/CookieSamesiteNone.bend as PCookieSamesiteNone +import ./proofs/CookieSamesiteStrict.bend as PCookieSamesiteStrict +import ./proofs/CookieSamesiteUnknown.bend as PCookieSamesiteUnknown +import ./proofs/CookieSecureNeedsTls.bend as PCookieSecureNeedsTls +import ./proofs/CookieSecureOnTls.bend as PCookieSecureOnTls +import ./proofs/CookieSendHostOnly.bend as PCookieSendHostOnly +import ./proofs/CookieSendOtherHost.bend as PCookieSendOtherHost +import ./proofs/CookieSendUnsetDomain.bend as PCookieSendUnsetDomain +import ./proofs/CookieSessionAlive.bend as PCookieSessionAlive +import ./proofs/CorsCredentialsExplicitOrigin.bend as PCorsCredentialsExplicitOrigin +import ./proofs/CorsCredentialsReflectOrigin.bend as PCorsCredentialsReflectOrigin +import ./proofs/CorsExplicitOrigin.bend as PCorsExplicitOrigin +import ./proofs/CorsGetIsNotPreflight.bend as PCorsGetIsNotPreflight +import ./proofs/CorsOnPreflight.bend as PCorsOnPreflight +import ./proofs/CorsOriginHeader.bend as PCorsOriginHeader +import ./proofs/CorsPreflightAllowed.bend as PCorsPreflightAllowed +import ./proofs/CorsPreflightBad.bend as PCorsPreflightBad +import ./proofs/CorsPreflightHeaderCase.bend as PCorsPreflightHeaderCase +import ./proofs/CorsPreflightHeaderDenied.bend as PCorsPreflightHeaderDenied +import ./proofs/CorsPreflightMethodDenied.bend as PCorsPreflightMethodDenied +import ./proofs/CorsPreflightOriginDenied.bend as PCorsPreflightOriginDenied +import ./proofs/CorsPreflightStar.bend as PCorsPreflightStar +import ./proofs/CorsRefusedOrigin.bend as PCorsRefusedOrigin +import ./proofs/CorsRequestHeadersHeader.bend as PCorsRequestHeadersHeader +import ./proofs/CorsRequestMethodHeader.bend as PCorsRequestMethodHeader +import ./proofs/CorsSimpleReflects.bend as PCorsSimpleReflects +import ./proofs/CorsSimpleRefused.bend as PCorsSimpleRefused +import ./proofs/CorsSimpleStar.bend as PCorsSimpleStar +import ./proofs/CorsStarWithoutCredentials.bend as PCorsStarWithoutCredentials +import ./proofs/CorsVaryEmpty.bend as PCorsVaryEmpty +import ./proofs/CorsVarySpecific.bend as PCorsVarySpecific +import ./proofs/CorsVaryStar.bend as PCorsVaryStar +import ./proofs/CutAtFirstEquals.bend as PCutAtFirstEquals +import ./proofs/DeadlineIsTimeout.bend as PDeadlineIsTimeout +import ./proofs/DechunkEmpty.bend as PDechunkEmpty +import ./proofs/DechunkIgnoresExtension.bend as PDechunkIgnoresExtension +import ./proofs/DechunkOnlyLast.bend as PDechunkOnlyLast +import ./proofs/DechunkStopsAtZero.bend as PDechunkStopsAtZero +import ./proofs/DechunkTwoChunks.bend as PDechunkTwoChunks +import ./proofs/DechunkWiki.bend as PDechunkWiki +import ./proofs/DeleteReqNamesPath.bend as PDeleteReqNamesPath +import ./proofs/DfltHttps.bend as PDfltHttps +import ./proofs/DfltPlain.bend as PDfltPlain +import ./proofs/DigitsReadDecimal.bend as PDigitsReadDecimal +import ./proofs/DigitsReadRejectsJunk.bend as PDigitsReadRejectsJunk +import ./proofs/DotSegmentsRemoved.bend as PDotSegmentsRemoved +import ./proofs/ErrnoNamesAreLinux.bend as PErrnoNamesAreLinux +import ./proofs/ExchangeCarriesMethod.bend as PExchangeCarriesMethod +import ./proofs/FailedEffectsLoseConnection.bend as PFailedEffectsLoseConnection +import ./proofs/FailuresReportedByPhase.bend as PFailuresReportedByPhase +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/FreshMaxAgeLimit.bend as PFreshMaxAgeLimit +import ./proofs/FreshMaxAgeOne.bend as PFreshMaxAgeOne +import ./proofs/FreshMaxAgeOpen.bend as PFreshMaxAgeOpen +import ./proofs/FreshMaxAgeOverridesExpires.bend as PFreshMaxAgeOverridesExpires +import ./proofs/FreshMaxAgeZero.bend as PFreshMaxAgeZero +import ./proofs/FreshNoCache.bend as PFreshNoCache +import ./proofs/FreshNoCacheAny.bend as PFreshNoCacheAny +import ./proofs/FreshNoStore.bend as PFreshNoStore +import ./proofs/FreshNoStoreAny.bend as PFreshNoStoreAny +import ./proofs/FreshPublicOnly.bend as PFreshPublicOnly +import ./proofs/GetOmitsContentLength.bend as PGetOmitsContentLength +import ./proofs/GetReplyKeepsBody.bend as PGetReplyKeepsBody +import ./proofs/GetReqNamesPath.bend as PGetReqNamesPath +import ./proofs/HandlerGetsRequestText.bend as PHandlerGetsRequestText +import ./proofs/HeadAndNoContentHaveNoBody.bend as PHeadAndNoContentHaveNoBody +import ./proofs/HeadHitIgnoresCase.bend as PHeadHitIgnoresCase +import ./proofs/HeadLineRejectsContinuation.bend as PHeadLineRejectsContinuation +import ./proofs/HeadLineRejectsObsFold.bend as PHeadLineRejectsObsFold +import ./proofs/HeadLineTrims.bend as PHeadLineTrims +import ./proofs/HeadPlannedBeforeBody.bend as PHeadPlannedBeforeBody +import ./proofs/HeadReplyDropsBody.bend as PHeadReplyDropsBody +import ./proofs/HeadReqNamesPath.bend as PHeadReqNamesPath +import ./proofs/HeadersFindFirst.bend as PHeadersFindFirst +import ./proofs/HeadersFindIgnoresCase.bend as PHeadersFindIgnoresCase +import ./proofs/HeadersFindMissing.bend as PHeadersFindMissing +import ./proofs/HexlenAlpha.bend as PHexlenAlpha +import ./proofs/HexlenExtensionAndCase.bend as PHexlenExtensionAndCase +import ./proofs/HexlenIgnoresExtension.bend as PHexlenIgnoresExtension +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/InterimResponsesSkipped.bend as PInterimResponsesSkipped +import ./proofs/InterimStatusIs1xx.bend as PInterimStatusIs1xx +import ./proofs/JoinJoined.bend as PJoinJoined +import ./proofs/JsonParseRejects.bend as PJsonParseRejects +import ./proofs/JsonParseString.bend as PJsonParseString +import ./proofs/JsonStringIsTextBody.bend as PJsonStringIsTextBody +import ./proofs/LocationResolvedBySort.bend as PLocationResolvedBySort +import ./proofs/LocationSplitsAtQuery.bend as PLocationSplitsAtQuery +import ./proofs/MethodIdempotentDelete.bend as PMethodIdempotentDelete +import ./proofs/MethodIdempotentGet.bend as PMethodIdempotentGet +import ./proofs/MethodIdempotentHead.bend as PMethodIdempotentHead +import ./proofs/MethodIdempotentOptions.bend as PMethodIdempotentOptions +import ./proofs/MethodIdempotentPatch.bend as PMethodIdempotentPatch +import ./proofs/MethodIdempotentPost.bend as PMethodIdempotentPost +import ./proofs/MethodIdempotentPut.bend as PMethodIdempotentPut +import ./proofs/MethodSafeDelete.bend as PMethodSafeDelete +import ./proofs/MethodSafeGet.bend as PMethodSafeGet +import ./proofs/MethodSafeGetLower.bend as PMethodSafeGetLower +import ./proofs/MethodSafeHead.bend as PMethodSafeHead +import ./proofs/MethodSafeOptions.bend as PMethodSafeOptions +import ./proofs/MethodSafePost.bend as PMethodSafePost +import ./proofs/MethodSafePut.bend as PMethodSafePut +import ./proofs/MethodTokensAreCaseSensitive.bend as PMethodTokensAreCaseSensitive +import ./proofs/NamedReferencesLongestMatch.bend as PNamedReferencesLongestMatch +import ./proofs/NumericReferencesDecode.bend as PNumericReferencesDecode +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/OptionsReqNamesPath.bend as POptionsReqNamesPath +import ./proofs/OtherStatusesStopChain.bend as POtherStatusesStopChain +import ./proofs/ParseChunkedBody.bend as PParseChunkedBody +import ./proofs/ParseChunkedIgnoresCase.bend as PParseChunkedIgnoresCase +import ./proofs/ParseClenTakesPrefix.bend as PParseClenTakesPrefix +import ./proofs/ParseEqualContentLengths.bend as PParseEqualContentLengths +import ./proofs/ParseNeedsBlankLine.bend as PParseNeedsBlankLine +import ./proofs/ParseNeedsSlashes.bend as PParseNeedsSlashes +import ./proofs/ParseNeedsStatus.bend as PParseNeedsStatus +import ./proofs/ParseQuerySplitsPairs.bend as PParseQuerySplitsPairs +import ./proofs/ParseRejectsClAndTe.bend as PParseRejectsClAndTe +import ./proofs/ParseRejectsDisagreeingCl.bend as PParseRejectsDisagreeingCl +import ./proofs/ParseRejectsNonHexChunk.bend as PParseRejectsNonHexChunk +import ./proofs/ParseSchemeFirst.bend as PParseSchemeFirst +import ./proofs/PathAbsolute.bend as PPathAbsolute +import ./proofs/PathFormKeepsSlash.bend as PPathFormKeepsSlash +import ./proofs/PhraseTableLookup.bend as PPhraseTableLookup +import ./proofs/PostReqNamesPath.bend as PPostReqNamesPath +import ./proofs/PostWritesContentLength.bend as PPostWritesContentLength +import ./proofs/PutReqNamesPath.bend as PPutReqNamesPath +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/Reason200.bend as PReason200 +import ./proofs/Reason201.bend as PReason201 +import ./proofs/Reason204.bend as PReason204 +import ./proofs/Reason400.bend as PReason400 +import ./proofs/Reason404.bend as PReason404 +import ./proofs/Reason405.bend as PReason405 +import ./proofs/Reason500.bend as PReason500 +import ./proofs/ReasonPhraseFromFinalHead.bend as PReasonPhraseFromFinalHead +import ./proofs/ReasonPhrasesFollowRfc.bend as PReasonPhrasesFollowRfc +import ./proofs/ReasonUnknownEmpty.bend as PReasonUnknownEmpty +import ./proofs/RedirectsFollowLocation.bend as PRedirectsFollowLocation +import ./proofs/RelativeLocationsMerge.bend as PRelativeLocationsMerge +import ./proofs/ReplyHeadFramesBody.bend as PReplyHeadFramesBody +import ./proofs/RequestHeaderFieldsOrdered.bend as PRequestHeaderFieldsOrdered +import ./proofs/RequestLineAndHost.bend as PRequestLineAndHost +import ./proofs/RequestNamesMethod.bend as PRequestNamesMethod +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/SecondIsNextWord.bend as PSecondIsNextWord +import ./proofs/SetCookieFieldsSelected.bend as PSetCookieFieldsSelected +import ./proofs/SetCookieReadsPair.bend as PSetCookieReadsPair +import ./proofs/ShowReplyOpensWithStatus.bend as PShowReplyOpensWithStatus +import ./proofs/ShowTornIsMarked.bend as PShowTornIsMarked +import ./proofs/SplitAtCharacter.bend as PSplitAtCharacter +import ./proofs/StreamedRepliesAreChunked.bend as PStreamedRepliesAreChunked +import ./proofs/TimeoutZeroIsOneMs.bend as PTimeoutZeroIsOneMs +import ./proofs/TimeoutsReadAsTimedOut.bend as PTimeoutsReadAsTimedOut +import ./proofs/TokenOkNewline.bend as PTokenOkNewline +import ./proofs/TokenOkPlain.bend as PTokenOkPlain +import ./proofs/TokenOkSpace.bend as PTokenOkSpace +import ./proofs/TokenOkTab.bend as PTokenOkTab +import ./proofs/UnescapeFindsReferences.bend as PUnescapeFindsReferences +import ./proofs/UnescapeUndoesEscape.bend as PUnescapeUndoesEscape +import ./proofs/UnframedIsRest.bend as PUnframedIsRest +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/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/Utf8Split.bend as PUtf8Split +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..807b53d 100644 --- a/bench/main.bend +++ b/bench/main.bend @@ -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()), 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/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/AskClenExact.bend b/laws/AskClenExact.bend new file mode 100644 index 0000000..0c24653 --- /dev/null +++ b/laws/AskClenExact.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/AskClenShort.bend b/laws/AskClenShort.bend new file mode 100644 index 0000000..b7bd01c --- /dev/null +++ b/laws/AskClenShort.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/AskEqualContentLengths.bend b/laws/AskEqualContentLengths.bend new file mode 100644 index 0000000..870d220 --- /dev/null +++ b/laws/AskEqualContentLengths.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/AskGetAsteriskRejected.bend b/laws/AskGetAsteriskRejected.bend new file mode 100644 index 0000000..daee653 --- /dev/null +++ b/laws/AskGetAsteriskRejected.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/AskKeepsFieldNameSpelling.bend b/laws/AskKeepsFieldNameSpelling.bend new file mode 100644 index 0000000..fd88e37 --- /dev/null +++ b/laws/AskKeepsFieldNameSpelling.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/AskNeedsBlankLine.bend b/laws/AskNeedsBlankLine.bend new file mode 100644 index 0000000..57df137 --- /dev/null +++ b/laws/AskNeedsBlankLine.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/AskNeedsRequestLine.bend b/laws/AskNeedsRequestLine.bend new file mode 100644 index 0000000..e7a501b --- /dev/null +++ b/laws/AskNeedsRequestLine.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/AskOptionsAsterisk.bend b/laws/AskOptionsAsterisk.bend new file mode 100644 index 0000000..877e081 --- /dev/null +++ b/laws/AskOptionsAsterisk.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/AskOriginFormKeepsQuery.bend b/laws/AskOriginFormKeepsQuery.bend new file mode 100644 index 0000000..83da1bc --- /dev/null +++ b/laws/AskOriginFormKeepsQuery.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/AskRejectsAbsoluteForm.bend b/laws/AskRejectsAbsoluteForm.bend new file mode 100644 index 0000000..6d546d5 --- /dev/null +++ b/laws/AskRejectsAbsoluteForm.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/AskRejectsClAndTe.bend b/laws/AskRejectsClAndTe.bend new file mode 100644 index 0000000..5b05cc6 --- /dev/null +++ b/laws/AskRejectsClAndTe.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/AskRejectsDisagreeingCl.bend b/laws/AskRejectsDisagreeingCl.bend new file mode 100644 index 0000000..b5bd35f --- /dev/null +++ b/laws/AskRejectsDisagreeingCl.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/AskRejectsRelativeTarget.bend b/laws/AskRejectsRelativeTarget.bend new file mode 100644 index 0000000..1650683 --- /dev/null +++ b/laws/AskRejectsRelativeTarget.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/AskShowBadIsMarked.bend b/laws/AskShowBadIsMarked.bend new file mode 100644 index 0000000..df0628e --- /dev/null +++ b/laws/AskShowBadIsMarked.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/B64Empty.bend b/laws/B64Empty.bend new file mode 100644 index 0000000..d50b2ce --- /dev/null +++ b/laws/B64Empty.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/B64F.bend b/laws/B64F.bend new file mode 100644 index 0000000..8db8b18 --- /dev/null +++ b/laws/B64F.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/B64Fo.bend b/laws/B64Fo.bend new file mode 100644 index 0000000..bee0e8e --- /dev/null +++ b/laws/B64Fo.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/B64Foo.bend b/laws/B64Foo.bend new file mode 100644 index 0000000..2ee4e09 --- /dev/null +++ b/laws/B64Foo.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/B64GoEnd.bend b/laws/B64GoEnd.bend new file mode 100644 index 0000000..fb3127c --- /dev/null +++ b/laws/B64GoEnd.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/B64UserPass.bend b/laws/B64UserPass.bend new file mode 100644 index 0000000..24256a8 --- /dev/null +++ b/laws/B64UserPass.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/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/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/BasicUserPass.bend b/laws/BasicUserPass.bend new file mode 100644 index 0000000..e523327 --- /dev/null +++ b/laws/BasicUserPass.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/CacheDirectiveCase.bend b/laws/CacheDirectiveCase.bend new file mode 100644 index 0000000..a82e194 --- /dev/null +++ b/laws/CacheDirectiveCase.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/CacheDirectives.bend b/laws/CacheDirectives.bend new file mode 100644 index 0000000..c598b83 --- /dev/null +++ b/laws/CacheDirectives.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/CacheIgnoresSMaxage.bend b/laws/CacheIgnoresSMaxage.bend new file mode 100644 index 0000000..7c0e05e --- /dev/null +++ b/laws/CacheIgnoresSMaxage.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/CacheMaxAgePresent.bend b/laws/CacheMaxAgePresent.bend new file mode 100644 index 0000000..2d4c8ac --- /dev/null +++ b/laws/CacheMaxAgePresent.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/CacheNoCacheAlone.bend b/laws/CacheNoCacheAlone.bend new file mode 100644 index 0000000..656c50c --- /dev/null +++ b/laws/CacheNoCacheAlone.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/CacheNoteExpires.bend b/laws/CacheNoteExpires.bend new file mode 100644 index 0000000..e31a7ca --- /dev/null +++ b/laws/CacheNoteExpires.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/CachePrivateAlone.bend b/laws/CachePrivateAlone.bend new file mode 100644 index 0000000..555c413 --- /dev/null +++ b/laws/CachePrivateAlone.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/ChunksEmptyFuel.bend b/laws/ChunksEmptyFuel.bend new file mode 100644 index 0000000..5a97cae --- /dev/null +++ b/laws/ChunksEmptyFuel.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/ClenExact.bend b/laws/ClenExact.bend new file mode 100644 index 0000000..a47a3d9 --- /dev/null +++ b/laws/ClenExact.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/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/CookieAgeSets.bend b/laws/CookieAgeSets.bend new file mode 100644 index 0000000..e7f0237 --- /dev/null +++ b/laws/CookieAgeSets.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/CookieDefaultPathDir.bend b/laws/CookieDefaultPathDir.bend new file mode 100644 index 0000000..d6c621c --- /dev/null +++ b/laws/CookieDefaultPathDir.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/CookieDefaultPathOneSlash.bend b/laws/CookieDefaultPathOneSlash.bend new file mode 100644 index 0000000..641915a --- /dev/null +++ b/laws/CookieDefaultPathOneSlash.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/CookieDefaultPathRoot.bend b/laws/CookieDefaultPathRoot.bend new file mode 100644 index 0000000..8a952b7 --- /dev/null +++ b/laws/CookieDefaultPathRoot.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/CookieDefaultPathTrailing.bend b/laws/CookieDefaultPathTrailing.bend new file mode 100644 index 0000000..c5bacec --- /dev/null +++ b/laws/CookieDefaultPathTrailing.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/CookieDomainIpExact.bend b/laws/CookieDomainIpExact.bend new file mode 100644 index 0000000..6041894 --- /dev/null +++ b/laws/CookieDomainIpExact.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/CookieDomainIpSuffix.bend b/laws/CookieDomainIpSuffix.bend new file mode 100644 index 0000000..3ae8799 --- /dev/null +++ b/laws/CookieDomainIpSuffix.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/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/CookieDomainNotSuffix.bend b/laws/CookieDomainNotSuffix.bend new file mode 100644 index 0000000..478ee00 --- /dev/null +++ b/laws/CookieDomainNotSuffix.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/CookieDomainSuffix.bend b/laws/CookieDomainSuffix.bend new file mode 100644 index 0000000..ff0732f --- /dev/null +++ b/laws/CookieDomainSuffix.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/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/CookieInsecureOnTcp.bend b/laws/CookieInsecureOnTcp.bend new file mode 100644 index 0000000..a042202 --- /dev/null +++ b/laws/CookieInsecureOnTcp.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/CookieIpv4Literal.bend b/laws/CookieIpv4Literal.bend new file mode 100644 index 0000000..b495155 --- /dev/null +++ b/laws/CookieIpv4Literal.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/CookieIpv4Name.bend b/laws/CookieIpv4Name.bend new file mode 100644 index 0000000..06d80cd --- /dev/null +++ b/laws/CookieIpv4Name.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/CookieLineAttributes.bend b/laws/CookieLineAttributes.bend new file mode 100644 index 0000000..c92b5b7 --- /dev/null +++ b/laws/CookieLineAttributes.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/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/CookieMaxAgeOne.bend b/laws/CookieMaxAgeOne.bend new file mode 100644 index 0000000..2bc6898 --- /dev/null +++ b/laws/CookieMaxAgeOne.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/CookieMaxAgeOpen.bend b/laws/CookieMaxAgeOpen.bend new file mode 100644 index 0000000..236fb7b --- /dev/null +++ b/laws/CookieMaxAgeOpen.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/CookieMaxAgeZero.bend b/laws/CookieMaxAgeZero.bend new file mode 100644 index 0000000..7680eed --- /dev/null +++ b/laws/CookieMaxAgeZero.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/CookieParseRoundTrip.bend b/laws/CookieParseRoundTrip.bend new file mode 100644 index 0000000..eca0bb4 --- /dev/null +++ b/laws/CookieParseRoundTrip.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/CookiePathIdentical.bend b/laws/CookiePathIdentical.bend new file mode 100644 index 0000000..a5a1f6f --- /dev/null +++ b/laws/CookiePathIdentical.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/CookiePathLongerCookie.bend b/laws/CookiePathLongerCookie.bend new file mode 100644 index 0000000..02f41db --- /dev/null +++ b/laws/CookiePathLongerCookie.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/CookiePathNotPrefix.bend b/laws/CookiePathNotPrefix.bend new file mode 100644 index 0000000..ae88fd6 --- /dev/null +++ b/laws/CookiePathNotPrefix.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/CookiePathPrefix.bend b/laws/CookiePathPrefix.bend new file mode 100644 index 0000000..2c1bcb4 --- /dev/null +++ b/laws/CookiePathPrefix.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/CookiePathRoot.bend b/laws/CookiePathRoot.bend new file mode 100644 index 0000000..5e76af7 --- /dev/null +++ b/laws/CookiePathRoot.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/CookiePathSlashPrefix.bend b/laws/CookiePathSlashPrefix.bend new file mode 100644 index 0000000..db32a16 --- /dev/null +++ b/laws/CookiePathSlashPrefix.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/CookieReceiveHostOnly.bend b/laws/CookieReceiveHostOnly.bend new file mode 100644 index 0000000..d277e4c --- /dev/null +++ b/laws/CookieReceiveHostOnly.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/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/CookieRequestJoins.bend b/laws/CookieRequestJoins.bend new file mode 100644 index 0000000..6d724b3 --- /dev/null +++ b/laws/CookieRequestJoins.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/CookieSamesiteNone.bend b/laws/CookieSamesiteNone.bend new file mode 100644 index 0000000..93099c9 --- /dev/null +++ b/laws/CookieSamesiteNone.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/CookieSamesiteStrict.bend b/laws/CookieSamesiteStrict.bend new file mode 100644 index 0000000..32ae846 --- /dev/null +++ b/laws/CookieSamesiteStrict.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/CookieSamesiteUnknown.bend b/laws/CookieSamesiteUnknown.bend new file mode 100644 index 0000000..fd5e314 --- /dev/null +++ b/laws/CookieSamesiteUnknown.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/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/CookieSecureOnTls.bend b/laws/CookieSecureOnTls.bend new file mode 100644 index 0000000..f623329 --- /dev/null +++ b/laws/CookieSecureOnTls.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/CookieSendHostOnly.bend b/laws/CookieSendHostOnly.bend new file mode 100644 index 0000000..e1a16d7 --- /dev/null +++ b/laws/CookieSendHostOnly.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/CookieSendOtherHost.bend b/laws/CookieSendOtherHost.bend new file mode 100644 index 0000000..faf5551 --- /dev/null +++ b/laws/CookieSendOtherHost.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/CookieSendUnsetDomain.bend b/laws/CookieSendUnsetDomain.bend new file mode 100644 index 0000000..ef45e2f --- /dev/null +++ b/laws/CookieSendUnsetDomain.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/CookieSessionAlive.bend b/laws/CookieSessionAlive.bend new file mode 100644 index 0000000..92e7d84 --- /dev/null +++ b/laws/CookieSessionAlive.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/CorsCredentialsExplicitOrigin.bend b/laws/CorsCredentialsExplicitOrigin.bend new file mode 100644 index 0000000..76079f4 --- /dev/null +++ b/laws/CorsCredentialsExplicitOrigin.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/CorsExplicitOrigin.bend b/laws/CorsExplicitOrigin.bend new file mode 100644 index 0000000..5e0ef6b --- /dev/null +++ b/laws/CorsExplicitOrigin.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/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/CorsOnPreflight.bend b/laws/CorsOnPreflight.bend new file mode 100644 index 0000000..983f1fe --- /dev/null +++ b/laws/CorsOnPreflight.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/CorsOriginHeader.bend b/laws/CorsOriginHeader.bend new file mode 100644 index 0000000..c0451de --- /dev/null +++ b/laws/CorsOriginHeader.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/CorsPreflightAllowed.bend b/laws/CorsPreflightAllowed.bend new file mode 100644 index 0000000..50477a5 --- /dev/null +++ b/laws/CorsPreflightAllowed.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/CorsPreflightBad.bend b/laws/CorsPreflightBad.bend new file mode 100644 index 0000000..3079050 --- /dev/null +++ b/laws/CorsPreflightBad.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/CorsPreflightHeaderCase.bend b/laws/CorsPreflightHeaderCase.bend new file mode 100644 index 0000000..d138e30 --- /dev/null +++ b/laws/CorsPreflightHeaderCase.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/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/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/CorsPreflightStar.bend b/laws/CorsPreflightStar.bend new file mode 100644 index 0000000..f62d65f --- /dev/null +++ b/laws/CorsPreflightStar.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/CorsRefusedOrigin.bend b/laws/CorsRefusedOrigin.bend new file mode 100644 index 0000000..20927e6 --- /dev/null +++ b/laws/CorsRefusedOrigin.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/CorsRequestHeadersHeader.bend b/laws/CorsRequestHeadersHeader.bend new file mode 100644 index 0000000..4be7223 --- /dev/null +++ b/laws/CorsRequestHeadersHeader.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/CorsRequestMethodHeader.bend b/laws/CorsRequestMethodHeader.bend new file mode 100644 index 0000000..01de247 --- /dev/null +++ b/laws/CorsRequestMethodHeader.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/CorsSimpleReflects.bend b/laws/CorsSimpleReflects.bend new file mode 100644 index 0000000..ca9ba38 --- /dev/null +++ b/laws/CorsSimpleReflects.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/CorsSimpleRefused.bend b/laws/CorsSimpleRefused.bend new file mode 100644 index 0000000..ff319c4 --- /dev/null +++ b/laws/CorsSimpleRefused.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/CorsSimpleStar.bend b/laws/CorsSimpleStar.bend new file mode 100644 index 0000000..7775521 --- /dev/null +++ b/laws/CorsSimpleStar.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/CorsStarWithoutCredentials.bend b/laws/CorsStarWithoutCredentials.bend new file mode 100644 index 0000000..fd68b29 --- /dev/null +++ b/laws/CorsStarWithoutCredentials.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/CorsVaryEmpty.bend b/laws/CorsVaryEmpty.bend new file mode 100644 index 0000000..f64612a --- /dev/null +++ b/laws/CorsVaryEmpty.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/CorsVarySpecific.bend b/laws/CorsVarySpecific.bend new file mode 100644 index 0000000..f6c1666 --- /dev/null +++ b/laws/CorsVarySpecific.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/CorsVaryStar.bend b/laws/CorsVaryStar.bend new file mode 100644 index 0000000..dd45431 --- /dev/null +++ b/laws/CorsVaryStar.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/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/DechunkEmpty.bend b/laws/DechunkEmpty.bend new file mode 100644 index 0000000..56258b1 --- /dev/null +++ b/laws/DechunkEmpty.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/DechunkOnlyLast.bend b/laws/DechunkOnlyLast.bend new file mode 100644 index 0000000..a901f05 --- /dev/null +++ b/laws/DechunkOnlyLast.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/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/DechunkTwoChunks.bend b/laws/DechunkTwoChunks.bend new file mode 100644 index 0000000..85f2caa --- /dev/null +++ b/laws/DechunkTwoChunks.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/DechunkWiki.bend b/laws/DechunkWiki.bend new file mode 100644 index 0000000..30b639e --- /dev/null +++ b/laws/DechunkWiki.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/DeleteReqNamesPath.bend b/laws/DeleteReqNamesPath.bend new file mode 100644 index 0000000..321fce0 --- /dev/null +++ b/laws/DeleteReqNamesPath.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/DfltHttps.bend b/laws/DfltHttps.bend new file mode 100644 index 0000000..d0d46b1 --- /dev/null +++ b/laws/DfltHttps.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/DfltPlain.bend b/laws/DfltPlain.bend new file mode 100644 index 0000000..522fb8d --- /dev/null +++ b/laws/DfltPlain.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/DigitsReadDecimal.bend b/laws/DigitsReadDecimal.bend new file mode 100644 index 0000000..06a69bb --- /dev/null +++ b/laws/DigitsReadDecimal.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/DigitsReadRejectsJunk.bend b/laws/DigitsReadRejectsJunk.bend new file mode 100644 index 0000000..c002203 --- /dev/null +++ b/laws/DigitsReadRejectsJunk.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/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/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/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/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/FreshMaxAgeLimit.bend b/laws/FreshMaxAgeLimit.bend new file mode 100644 index 0000000..799b3e6 --- /dev/null +++ b/laws/FreshMaxAgeLimit.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/FreshMaxAgeOne.bend b/laws/FreshMaxAgeOne.bend new file mode 100644 index 0000000..ba2ae6a --- /dev/null +++ b/laws/FreshMaxAgeOne.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/FreshMaxAgeOpen.bend b/laws/FreshMaxAgeOpen.bend new file mode 100644 index 0000000..0035707 --- /dev/null +++ b/laws/FreshMaxAgeOpen.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/FreshMaxAgeOverridesExpires.bend b/laws/FreshMaxAgeOverridesExpires.bend new file mode 100644 index 0000000..ca60ced --- /dev/null +++ b/laws/FreshMaxAgeOverridesExpires.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/FreshMaxAgeZero.bend b/laws/FreshMaxAgeZero.bend new file mode 100644 index 0000000..d1313e8 --- /dev/null +++ b/laws/FreshMaxAgeZero.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/FreshNoCache.bend b/laws/FreshNoCache.bend new file mode 100644 index 0000000..6f19e3e --- /dev/null +++ b/laws/FreshNoCache.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/FreshNoCacheAny.bend b/laws/FreshNoCacheAny.bend new file mode 100644 index 0000000..dec3963 --- /dev/null +++ b/laws/FreshNoCacheAny.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/FreshNoStore.bend b/laws/FreshNoStore.bend new file mode 100644 index 0000000..f1e1b4c --- /dev/null +++ b/laws/FreshNoStore.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/FreshNoStoreAny.bend b/laws/FreshNoStoreAny.bend new file mode 100644 index 0000000..c607e38 --- /dev/null +++ b/laws/FreshNoStoreAny.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/FreshPublicOnly.bend b/laws/FreshPublicOnly.bend new file mode 100644 index 0000000..1373b58 --- /dev/null +++ b/laws/FreshPublicOnly.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/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/GetReplyKeepsBody.bend b/laws/GetReplyKeepsBody.bend new file mode 100644 index 0000000..454b2eb --- /dev/null +++ b/laws/GetReplyKeepsBody.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/GetReqNamesPath.bend b/laws/GetReqNamesPath.bend new file mode 100644 index 0000000..e2627d5 --- /dev/null +++ b/laws/GetReqNamesPath.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/HeadHitIgnoresCase.bend b/laws/HeadHitIgnoresCase.bend new file mode 100644 index 0000000..8687155 --- /dev/null +++ b/laws/HeadHitIgnoresCase.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/HeadLineRejectsContinuation.bend b/laws/HeadLineRejectsContinuation.bend new file mode 100644 index 0000000..137f940 --- /dev/null +++ b/laws/HeadLineRejectsContinuation.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/HeadLineRejectsObsFold.bend b/laws/HeadLineRejectsObsFold.bend new file mode 100644 index 0000000..43338b5 --- /dev/null +++ b/laws/HeadLineRejectsObsFold.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/HeadLineTrims.bend b/laws/HeadLineTrims.bend new file mode 100644 index 0000000..13fd334 --- /dev/null +++ b/laws/HeadLineTrims.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/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/HeadReqNamesPath.bend b/laws/HeadReqNamesPath.bend new file mode 100644 index 0000000..6fa1e90 --- /dev/null +++ b/laws/HeadReqNamesPath.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/HeadersFindFirst.bend b/laws/HeadersFindFirst.bend new file mode 100644 index 0000000..bb1d87c --- /dev/null +++ b/laws/HeadersFindFirst.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/HeadersFindIgnoresCase.bend b/laws/HeadersFindIgnoresCase.bend new file mode 100644 index 0000000..20943b4 --- /dev/null +++ b/laws/HeadersFindIgnoresCase.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/HeadersFindMissing.bend b/laws/HeadersFindMissing.bend new file mode 100644 index 0000000..52ba907 --- /dev/null +++ b/laws/HeadersFindMissing.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/HexlenAlpha.bend b/laws/HexlenAlpha.bend new file mode 100644 index 0000000..63ffc29 --- /dev/null +++ b/laws/HexlenAlpha.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/HexlenExtensionAndCase.bend b/laws/HexlenExtensionAndCase.bend new file mode 100644 index 0000000..406ed23 --- /dev/null +++ b/laws/HexlenExtensionAndCase.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/HexlenIgnoresExtension.bend b/laws/HexlenIgnoresExtension.bend new file mode 100644 index 0000000..af6a57b --- /dev/null +++ b/laws/HexlenIgnoresExtension.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/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/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/JoinJoined.bend b/laws/JoinJoined.bend new file mode 100644 index 0000000..b556987 --- /dev/null +++ b/laws/JoinJoined.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/JsonParseRejects.bend b/laws/JsonParseRejects.bend new file mode 100644 index 0000000..006ad7b --- /dev/null +++ b/laws/JsonParseRejects.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/JsonParseString.bend b/laws/JsonParseString.bend new file mode 100644 index 0000000..bce40ed --- /dev/null +++ b/laws/JsonParseString.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/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/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/MethodIdempotentDelete.bend b/laws/MethodIdempotentDelete.bend new file mode 100644 index 0000000..bbe10c8 --- /dev/null +++ b/laws/MethodIdempotentDelete.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/MethodIdempotentGet.bend b/laws/MethodIdempotentGet.bend new file mode 100644 index 0000000..a12e381 --- /dev/null +++ b/laws/MethodIdempotentGet.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/MethodIdempotentHead.bend b/laws/MethodIdempotentHead.bend new file mode 100644 index 0000000..c9e3718 --- /dev/null +++ b/laws/MethodIdempotentHead.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/MethodIdempotentOptions.bend b/laws/MethodIdempotentOptions.bend new file mode 100644 index 0000000..6da820a --- /dev/null +++ b/laws/MethodIdempotentOptions.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/MethodIdempotentPatch.bend b/laws/MethodIdempotentPatch.bend new file mode 100644 index 0000000..2eea20a --- /dev/null +++ b/laws/MethodIdempotentPatch.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/MethodIdempotentPost.bend b/laws/MethodIdempotentPost.bend new file mode 100644 index 0000000..b3dfa39 --- /dev/null +++ b/laws/MethodIdempotentPost.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/MethodIdempotentPut.bend b/laws/MethodIdempotentPut.bend new file mode 100644 index 0000000..0ab47fa --- /dev/null +++ b/laws/MethodIdempotentPut.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/MethodSafeDelete.bend b/laws/MethodSafeDelete.bend new file mode 100644 index 0000000..b6454a1 --- /dev/null +++ b/laws/MethodSafeDelete.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/MethodSafeGet.bend b/laws/MethodSafeGet.bend new file mode 100644 index 0000000..b71ed8a --- /dev/null +++ b/laws/MethodSafeGet.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/MethodSafeGetLower.bend b/laws/MethodSafeGetLower.bend new file mode 100644 index 0000000..186a354 --- /dev/null +++ b/laws/MethodSafeGetLower.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/MethodSafeHead.bend b/laws/MethodSafeHead.bend new file mode 100644 index 0000000..601f2d3 --- /dev/null +++ b/laws/MethodSafeHead.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/MethodSafeOptions.bend b/laws/MethodSafeOptions.bend new file mode 100644 index 0000000..6f302bf --- /dev/null +++ b/laws/MethodSafeOptions.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/MethodSafePost.bend b/laws/MethodSafePost.bend new file mode 100644 index 0000000..1533cbb --- /dev/null +++ b/laws/MethodSafePost.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/MethodSafePut.bend b/laws/MethodSafePut.bend new file mode 100644 index 0000000..a2513d6 --- /dev/null +++ b/laws/MethodSafePut.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/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/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/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/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/OptionsReqNamesPath.bend b/laws/OptionsReqNamesPath.bend new file mode 100644 index 0000000..be7ee92 --- /dev/null +++ b/laws/OptionsReqNamesPath.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/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/ParseChunkedBody.bend b/laws/ParseChunkedBody.bend new file mode 100644 index 0000000..6dbfd21 --- /dev/null +++ b/laws/ParseChunkedBody.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/ParseChunkedIgnoresCase.bend b/laws/ParseChunkedIgnoresCase.bend new file mode 100644 index 0000000..c8f5dc6 --- /dev/null +++ b/laws/ParseChunkedIgnoresCase.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/ParseClenTakesPrefix.bend b/laws/ParseClenTakesPrefix.bend new file mode 100644 index 0000000..60a8c16 --- /dev/null +++ b/laws/ParseClenTakesPrefix.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/ParseEqualContentLengths.bend b/laws/ParseEqualContentLengths.bend new file mode 100644 index 0000000..114080c --- /dev/null +++ b/laws/ParseEqualContentLengths.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/ParseNeedsBlankLine.bend b/laws/ParseNeedsBlankLine.bend new file mode 100644 index 0000000..de2c1c4 --- /dev/null +++ b/laws/ParseNeedsBlankLine.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/ParseNeedsSlashes.bend b/laws/ParseNeedsSlashes.bend new file mode 100644 index 0000000..08f35a2 --- /dev/null +++ b/laws/ParseNeedsSlashes.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/ParseNeedsStatus.bend b/laws/ParseNeedsStatus.bend new file mode 100644 index 0000000..66ec64c --- /dev/null +++ b/laws/ParseNeedsStatus.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/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/ParseRejectsClAndTe.bend b/laws/ParseRejectsClAndTe.bend new file mode 100644 index 0000000..b6cf2d7 --- /dev/null +++ b/laws/ParseRejectsClAndTe.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/ParseRejectsDisagreeingCl.bend b/laws/ParseRejectsDisagreeingCl.bend new file mode 100644 index 0000000..f785fc1 --- /dev/null +++ b/laws/ParseRejectsDisagreeingCl.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/ParseRejectsNonHexChunk.bend b/laws/ParseRejectsNonHexChunk.bend new file mode 100644 index 0000000..c03984f --- /dev/null +++ b/laws/ParseRejectsNonHexChunk.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/ParseSchemeFirst.bend b/laws/ParseSchemeFirst.bend new file mode 100644 index 0000000..e3bd3ec --- /dev/null +++ b/laws/ParseSchemeFirst.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/PathAbsolute.bend b/laws/PathAbsolute.bend new file mode 100644 index 0000000..1bfd5b8 --- /dev/null +++ b/laws/PathAbsolute.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/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/PostReqNamesPath.bend b/laws/PostReqNamesPath.bend new file mode 100644 index 0000000..f1753db --- /dev/null +++ b/laws/PostReqNamesPath.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/PutReqNamesPath.bend b/laws/PutReqNamesPath.bend new file mode 100644 index 0000000..c82057b --- /dev/null +++ b/laws/PutReqNamesPath.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/Reason200.bend b/laws/Reason200.bend new file mode 100644 index 0000000..d820360 --- /dev/null +++ b/laws/Reason200.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/Reason201.bend b/laws/Reason201.bend new file mode 100644 index 0000000..e3f027d --- /dev/null +++ b/laws/Reason201.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/Reason204.bend b/laws/Reason204.bend new file mode 100644 index 0000000..3e75ae7 --- /dev/null +++ b/laws/Reason204.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/Reason400.bend b/laws/Reason400.bend new file mode 100644 index 0000000..40bef94 --- /dev/null +++ b/laws/Reason400.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/Reason404.bend b/laws/Reason404.bend new file mode 100644 index 0000000..73e2f57 --- /dev/null +++ b/laws/Reason404.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/Reason405.bend b/laws/Reason405.bend new file mode 100644 index 0000000..5b0d35f --- /dev/null +++ b/laws/Reason405.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/Reason500.bend b/laws/Reason500.bend new file mode 100644 index 0000000..45c4f92 --- /dev/null +++ b/laws/Reason500.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/ReasonUnknownEmpty.bend b/laws/ReasonUnknownEmpty.bend new file mode 100644 index 0000000..e22f668 --- /dev/null +++ b/laws/ReasonUnknownEmpty.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/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/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/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/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/RequestNamesMethod.bend b/laws/RequestNamesMethod.bend new file mode 100644 index 0000000..d16a2a9 --- /dev/null +++ b/laws/RequestNamesMethod.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/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/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/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/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/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/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/TokenOkNewline.bend b/laws/TokenOkNewline.bend new file mode 100644 index 0000000..f865b14 --- /dev/null +++ b/laws/TokenOkNewline.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/TokenOkPlain.bend b/laws/TokenOkPlain.bend new file mode 100644 index 0000000..eaa2eda --- /dev/null +++ b/laws/TokenOkPlain.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/TokenOkSpace.bend b/laws/TokenOkSpace.bend new file mode 100644 index 0000000..f6698ff --- /dev/null +++ b/laws/TokenOkSpace.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/TokenOkTab.bend b/laws/TokenOkTab.bend new file mode 100644 index 0000000..e52ba40 --- /dev/null +++ b/laws/TokenOkTab.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/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/UnframedIsRest.bend b/laws/UnframedIsRest.bend new file mode 100644 index 0000000..81fb860 --- /dev/null +++ b/laws/UnframedIsRest.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/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/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/Utf8Split.bend b/laws/Utf8Split.bend new file mode 100644 index 0000000..dd297f4 --- /dev/null +++ b/laws/Utf8Split.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/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/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/AskClenExact.bend b/proofs/AskClenExact.bend new file mode 100644 index 0000000..f68f4a7 --- /dev/null +++ b/proofs/AskClenExact.bend @@ -0,0 +1,18 @@ +# proofs of laws/AskClenExact.bend +import Base +import ../src/http.bend as Http +import ../laws/AskClenExact.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/AskClenShort.bend b/proofs/AskClenShort.bend new file mode 100644 index 0000000..a78212f --- /dev/null +++ b/proofs/AskClenShort.bend @@ -0,0 +1,10 @@ +# proofs of laws/AskClenShort.bend +import Base +import ../src/http.bend as Http +import ../laws/AskClenShort.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/AskEqualContentLengths.bend b/proofs/AskEqualContentLengths.bend new file mode 100644 index 0000000..0311be1 --- /dev/null +++ b/proofs/AskEqualContentLengths.bend @@ -0,0 +1,6 @@ +# proofs of laws/AskEqualContentLengths.bend +import Base +import ../laws/AskEqualContentLengths.bend as Laws + +def Laws.ask_equal_content_lengths(): + {==} diff --git a/proofs/AskGetAsteriskRejected.bend b/proofs/AskGetAsteriskRejected.bend new file mode 100644 index 0000000..ccc6b2a --- /dev/null +++ b/proofs/AskGetAsteriskRejected.bend @@ -0,0 +1,6 @@ +# proofs of laws/AskGetAsteriskRejected.bend +import Base +import ../laws/AskGetAsteriskRejected.bend as Laws + +def Laws.ask_get_asterisk_rejected(): + {==} diff --git a/proofs/AskKeepsFieldNameSpelling.bend b/proofs/AskKeepsFieldNameSpelling.bend new file mode 100644 index 0000000..fa41d49 --- /dev/null +++ b/proofs/AskKeepsFieldNameSpelling.bend @@ -0,0 +1,6 @@ +# proofs of laws/AskKeepsFieldNameSpelling.bend +import Base +import ../laws/AskKeepsFieldNameSpelling.bend as Laws + +def Laws.ask_keeps_field_name_spelling(): + {==} diff --git a/proofs/AskNeedsBlankLine.bend b/proofs/AskNeedsBlankLine.bend new file mode 100644 index 0000000..ee7e13f --- /dev/null +++ b/proofs/AskNeedsBlankLine.bend @@ -0,0 +1,10 @@ +# proofs of laws/AskNeedsBlankLine.bend +import Base +import ../src/http.bend as Http +import ../laws/AskNeedsBlankLine.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/AskNeedsRequestLine.bend b/proofs/AskNeedsRequestLine.bend new file mode 100644 index 0000000..4ff8e0b --- /dev/null +++ b/proofs/AskNeedsRequestLine.bend @@ -0,0 +1,6 @@ +# proofs of laws/AskNeedsRequestLine.bend +import Base +import ../laws/AskNeedsRequestLine.bend as Laws + +def Laws.ask_needs_request_line(_body): + {==} diff --git a/proofs/AskOptionsAsterisk.bend b/proofs/AskOptionsAsterisk.bend new file mode 100644 index 0000000..2f67194 --- /dev/null +++ b/proofs/AskOptionsAsterisk.bend @@ -0,0 +1,6 @@ +# proofs of laws/AskOptionsAsterisk.bend +import Base +import ../laws/AskOptionsAsterisk.bend as Laws + +def Laws.ask_options_asterisk(): + {==} diff --git a/proofs/AskOriginFormKeepsQuery.bend b/proofs/AskOriginFormKeepsQuery.bend new file mode 100644 index 0000000..9f41efb --- /dev/null +++ b/proofs/AskOriginFormKeepsQuery.bend @@ -0,0 +1,6 @@ +# proofs of laws/AskOriginFormKeepsQuery.bend +import Base +import ../laws/AskOriginFormKeepsQuery.bend as Laws + +def Laws.ask_origin_form_keeps_query(): + {==} diff --git a/proofs/AskRejectsAbsoluteForm.bend b/proofs/AskRejectsAbsoluteForm.bend new file mode 100644 index 0000000..4c71832 --- /dev/null +++ b/proofs/AskRejectsAbsoluteForm.bend @@ -0,0 +1,6 @@ +# proofs of laws/AskRejectsAbsoluteForm.bend +import Base +import ../laws/AskRejectsAbsoluteForm.bend as Laws + +def Laws.ask_rejects_absolute_form(): + {==} diff --git a/proofs/AskRejectsClAndTe.bend b/proofs/AskRejectsClAndTe.bend new file mode 100644 index 0000000..2bbb339 --- /dev/null +++ b/proofs/AskRejectsClAndTe.bend @@ -0,0 +1,6 @@ +# proofs of laws/AskRejectsClAndTe.bend +import Base +import ../laws/AskRejectsClAndTe.bend as Laws + +def Laws.ask_rejects_cl_and_te(): + {==} diff --git a/proofs/AskRejectsDisagreeingCl.bend b/proofs/AskRejectsDisagreeingCl.bend new file mode 100644 index 0000000..b5c97ff --- /dev/null +++ b/proofs/AskRejectsDisagreeingCl.bend @@ -0,0 +1,6 @@ +# proofs of laws/AskRejectsDisagreeingCl.bend +import Base +import ../laws/AskRejectsDisagreeingCl.bend as Laws + +def Laws.ask_rejects_disagreeing_cl(): + {==} diff --git a/proofs/AskRejectsRelativeTarget.bend b/proofs/AskRejectsRelativeTarget.bend new file mode 100644 index 0000000..42b1ae9 --- /dev/null +++ b/proofs/AskRejectsRelativeTarget.bend @@ -0,0 +1,6 @@ +# proofs of laws/AskRejectsRelativeTarget.bend +import Base +import ../laws/AskRejectsRelativeTarget.bend as Laws + +def Laws.ask_rejects_relative_target(): + {==} diff --git a/proofs/AskShowBadIsMarked.bend b/proofs/AskShowBadIsMarked.bend new file mode 100644 index 0000000..a640221 --- /dev/null +++ b/proofs/AskShowBadIsMarked.bend @@ -0,0 +1,7 @@ +# proofs of laws/AskShowBadIsMarked.bend +import Base +import ../laws/AskShowBadIsMarked.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/B64Empty.bend b/proofs/B64Empty.bend new file mode 100644 index 0000000..752a884 --- /dev/null +++ b/proofs/B64Empty.bend @@ -0,0 +1,6 @@ +# proofs of laws/B64Empty.bend +import Base +import ../laws/B64Empty.bend as Laws + +def Laws.b64_empty(): + {==} diff --git a/proofs/B64F.bend b/proofs/B64F.bend new file mode 100644 index 0000000..8a1a0a2 --- /dev/null +++ b/proofs/B64F.bend @@ -0,0 +1,6 @@ +# proofs of laws/B64F.bend +import Base +import ../laws/B64F.bend as Laws + +def Laws.b64_f(): + {==} diff --git a/proofs/B64Fo.bend b/proofs/B64Fo.bend new file mode 100644 index 0000000..0aae5ad --- /dev/null +++ b/proofs/B64Fo.bend @@ -0,0 +1,6 @@ +# proofs of laws/B64Fo.bend +import Base +import ../laws/B64Fo.bend as Laws + +def Laws.b64_fo(): + {==} diff --git a/proofs/B64Foo.bend b/proofs/B64Foo.bend new file mode 100644 index 0000000..f0756c1 --- /dev/null +++ b/proofs/B64Foo.bend @@ -0,0 +1,6 @@ +# proofs of laws/B64Foo.bend +import Base +import ../laws/B64Foo.bend as Laws + +def Laws.b64_foo(): + {==} diff --git a/proofs/B64GoEnd.bend b/proofs/B64GoEnd.bend new file mode 100644 index 0000000..0b3f6a4 --- /dev/null +++ b/proofs/B64GoEnd.bend @@ -0,0 +1,7 @@ +# proofs of laws/B64GoEnd.bend +import Base +import ../laws/B64GoEnd.bend as Laws + +# b64_go_end: go matches the input first +def Laws.b64_go_end(_phase): + {==} diff --git a/proofs/B64UserPass.bend b/proofs/B64UserPass.bend new file mode 100644 index 0000000..08c3825 --- /dev/null +++ b/proofs/B64UserPass.bend @@ -0,0 +1,6 @@ +# proofs of laws/B64UserPass.bend +import Base +import ../laws/B64UserPass.bend as Laws + +def Laws.b64_user_pass(): + {==} 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/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/BasicUserPass.bend b/proofs/BasicUserPass.bend new file mode 100644 index 0000000..be16649 --- /dev/null +++ b/proofs/BasicUserPass.bend @@ -0,0 +1,6 @@ +# proofs of laws/BasicUserPass.bend +import Base +import ../laws/BasicUserPass.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/CacheDirectiveCase.bend b/proofs/CacheDirectiveCase.bend new file mode 100644 index 0000000..86be227 --- /dev/null +++ b/proofs/CacheDirectiveCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheDirectiveCase.bend +import Base +import ../laws/CacheDirectiveCase.bend as Laws + +def Laws.cache_directive_case(): + {==} diff --git a/proofs/CacheDirectives.bend b/proofs/CacheDirectives.bend new file mode 100644 index 0000000..4da7916 --- /dev/null +++ b/proofs/CacheDirectives.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheDirectives.bend +import Base +import ../laws/CacheDirectives.bend as Laws + +def Laws.cache_directives(): + {==} diff --git a/proofs/CacheIgnoresSMaxage.bend b/proofs/CacheIgnoresSMaxage.bend new file mode 100644 index 0000000..d53bb34 --- /dev/null +++ b/proofs/CacheIgnoresSMaxage.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheIgnoresSMaxage.bend +import Base +import ../laws/CacheIgnoresSMaxage.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/CacheMaxAgePresent.bend b/proofs/CacheMaxAgePresent.bend new file mode 100644 index 0000000..e3c4afa --- /dev/null +++ b/proofs/CacheMaxAgePresent.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheMaxAgePresent.bend +import Base +import ../laws/CacheMaxAgePresent.bend as Laws + +def Laws.cache_max_age_present(): + {==} diff --git a/proofs/CacheNoCacheAlone.bend b/proofs/CacheNoCacheAlone.bend new file mode 100644 index 0000000..00d20d1 --- /dev/null +++ b/proofs/CacheNoCacheAlone.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheNoCacheAlone.bend +import Base +import ../laws/CacheNoCacheAlone.bend as Laws + +def Laws.cache_no_cache_alone(): + {==} diff --git a/proofs/CacheNoteExpires.bend b/proofs/CacheNoteExpires.bend new file mode 100644 index 0000000..5e8afb7 --- /dev/null +++ b/proofs/CacheNoteExpires.bend @@ -0,0 +1,6 @@ +# proofs of laws/CacheNoteExpires.bend +import Base +import ../laws/CacheNoteExpires.bend as Laws + +def Laws.cache_note_expires(): + {==} diff --git a/proofs/CachePrivateAlone.bend b/proofs/CachePrivateAlone.bend new file mode 100644 index 0000000..b8918d7 --- /dev/null +++ b/proofs/CachePrivateAlone.bend @@ -0,0 +1,6 @@ +# proofs of laws/CachePrivateAlone.bend +import Base +import ../laws/CachePrivateAlone.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/ChunksEmptyFuel.bend b/proofs/ChunksEmptyFuel.bend new file mode 100644 index 0000000..d677921 --- /dev/null +++ b/proofs/ChunksEmptyFuel.bend @@ -0,0 +1,6 @@ +# proofs of laws/ChunksEmptyFuel.bend +import Base +import ../laws/ChunksEmptyFuel.bend as Laws + +def Laws.chunks_empty_fuel(): + {==} diff --git a/proofs/ClenExact.bend b/proofs/ClenExact.bend new file mode 100644 index 0000000..0931fe3 --- /dev/null +++ b/proofs/ClenExact.bend @@ -0,0 +1,17 @@ +# proofs of laws/ClenExact.bend +import Base +import ../src/http.bend as Http +import ../laws/ClenExact.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/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/CookieAgeSets.bend b/proofs/CookieAgeSets.bend new file mode 100644 index 0000000..2cf8e4a --- /dev/null +++ b/proofs/CookieAgeSets.bend @@ -0,0 +1,19 @@ +# proofs of laws/CookieAgeSets.bend +import Base +import ../laws/CookieAgeSets.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/CookieDefaultPathDir.bend b/proofs/CookieDefaultPathDir.bend new file mode 100644 index 0000000..cc2e5a6 --- /dev/null +++ b/proofs/CookieDefaultPathDir.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDefaultPathDir.bend +import Base +import ../laws/CookieDefaultPathDir.bend as Laws + +def Laws.cookie_default_path_dir(): + {==} diff --git a/proofs/CookieDefaultPathOneSlash.bend b/proofs/CookieDefaultPathOneSlash.bend new file mode 100644 index 0000000..14ff6a2 --- /dev/null +++ b/proofs/CookieDefaultPathOneSlash.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDefaultPathOneSlash.bend +import Base +import ../laws/CookieDefaultPathOneSlash.bend as Laws + +def Laws.cookie_default_path_one_slash(): + {==} diff --git a/proofs/CookieDefaultPathRoot.bend b/proofs/CookieDefaultPathRoot.bend new file mode 100644 index 0000000..8f586e5 --- /dev/null +++ b/proofs/CookieDefaultPathRoot.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDefaultPathRoot.bend +import Base +import ../laws/CookieDefaultPathRoot.bend as Laws + +def Laws.cookie_default_path_root(): + {==} diff --git a/proofs/CookieDefaultPathTrailing.bend b/proofs/CookieDefaultPathTrailing.bend new file mode 100644 index 0000000..e6326ac --- /dev/null +++ b/proofs/CookieDefaultPathTrailing.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDefaultPathTrailing.bend +import Base +import ../laws/CookieDefaultPathTrailing.bend as Laws + +def Laws.cookie_default_path_trailing(): + {==} diff --git a/proofs/CookieDomainIpExact.bend b/proofs/CookieDomainIpExact.bend new file mode 100644 index 0000000..b89147c --- /dev/null +++ b/proofs/CookieDomainIpExact.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDomainIpExact.bend +import Base +import ../laws/CookieDomainIpExact.bend as Laws + +def Laws.cookie_domain_ip_exact(): + {==} diff --git a/proofs/CookieDomainIpSuffix.bend b/proofs/CookieDomainIpSuffix.bend new file mode 100644 index 0000000..8600434 --- /dev/null +++ b/proofs/CookieDomainIpSuffix.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDomainIpSuffix.bend +import Base +import ../laws/CookieDomainIpSuffix.bend as Laws + +def Laws.cookie_domain_ip_suffix(): + {==} 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/CookieDomainNotSuffix.bend b/proofs/CookieDomainNotSuffix.bend new file mode 100644 index 0000000..18ea71d --- /dev/null +++ b/proofs/CookieDomainNotSuffix.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDomainNotSuffix.bend +import Base +import ../laws/CookieDomainNotSuffix.bend as Laws + +def Laws.cookie_domain_not_suffix(): + {==} diff --git a/proofs/CookieDomainSuffix.bend b/proofs/CookieDomainSuffix.bend new file mode 100644 index 0000000..83ba1d6 --- /dev/null +++ b/proofs/CookieDomainSuffix.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieDomainSuffix.bend +import Base +import ../laws/CookieDomainSuffix.bend as Laws + +def Laws.cookie_domain_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/CookieInsecureOnTcp.bend b/proofs/CookieInsecureOnTcp.bend new file mode 100644 index 0000000..78a6f61 --- /dev/null +++ b/proofs/CookieInsecureOnTcp.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieInsecureOnTcp.bend +import Base +import ../laws/CookieInsecureOnTcp.bend as Laws + +def Laws.cookie_insecure_on_tcp(): + {==} diff --git a/proofs/CookieIpv4Literal.bend b/proofs/CookieIpv4Literal.bend new file mode 100644 index 0000000..0c831a8 --- /dev/null +++ b/proofs/CookieIpv4Literal.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieIpv4Literal.bend +import Base +import ../laws/CookieIpv4Literal.bend as Laws + +def Laws.cookie_ipv4_literal(): + {==} diff --git a/proofs/CookieIpv4Name.bend b/proofs/CookieIpv4Name.bend new file mode 100644 index 0000000..28b8827 --- /dev/null +++ b/proofs/CookieIpv4Name.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieIpv4Name.bend +import Base +import ../laws/CookieIpv4Name.bend as Laws + +def Laws.cookie_ipv4_name(): + {==} diff --git a/proofs/CookieLineAttributes.bend b/proofs/CookieLineAttributes.bend new file mode 100644 index 0000000..44c960a --- /dev/null +++ b/proofs/CookieLineAttributes.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieLineAttributes.bend +import Base +import ../laws/CookieLineAttributes.bend as Laws + +def Laws.cookie_line_attributes(): + {==} 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/CookieMaxAgeOne.bend b/proofs/CookieMaxAgeOne.bend new file mode 100644 index 0000000..cbeeed6 --- /dev/null +++ b/proofs/CookieMaxAgeOne.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieMaxAgeOne.bend +import Base +import ../laws/CookieMaxAgeOne.bend as Laws + +def Laws.cookie_max_age_one(): + {==} diff --git a/proofs/CookieMaxAgeOpen.bend b/proofs/CookieMaxAgeOpen.bend new file mode 100644 index 0000000..42b4356 --- /dev/null +++ b/proofs/CookieMaxAgeOpen.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieMaxAgeOpen.bend +import Base +import ../laws/CookieMaxAgeOpen.bend as Laws + +def Laws.cookie_max_age_open(): + {==} diff --git a/proofs/CookieMaxAgeZero.bend b/proofs/CookieMaxAgeZero.bend new file mode 100644 index 0000000..429fdc5 --- /dev/null +++ b/proofs/CookieMaxAgeZero.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieMaxAgeZero.bend +import Base +import ../laws/CookieMaxAgeZero.bend as Laws + +def Laws.cookie_max_age_zero(): + {==} diff --git a/proofs/CookieParseRoundTrip.bend b/proofs/CookieParseRoundTrip.bend new file mode 100644 index 0000000..d1c19b6 --- /dev/null +++ b/proofs/CookieParseRoundTrip.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieParseRoundTrip.bend +import Base +import ../laws/CookieParseRoundTrip.bend as Laws + +def Laws.cookie_parse_round_trip(): + {==} diff --git a/proofs/CookiePathIdentical.bend b/proofs/CookiePathIdentical.bend new file mode 100644 index 0000000..b6d048b --- /dev/null +++ b/proofs/CookiePathIdentical.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookiePathIdentical.bend +import Base +import ../laws/CookiePathIdentical.bend as Laws + +def Laws.cookie_path_identical(): + {==} diff --git a/proofs/CookiePathLongerCookie.bend b/proofs/CookiePathLongerCookie.bend new file mode 100644 index 0000000..45cdb36 --- /dev/null +++ b/proofs/CookiePathLongerCookie.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookiePathLongerCookie.bend +import Base +import ../laws/CookiePathLongerCookie.bend as Laws + +def Laws.cookie_path_longer_cookie(): + {==} diff --git a/proofs/CookiePathNotPrefix.bend b/proofs/CookiePathNotPrefix.bend new file mode 100644 index 0000000..45a79b5 --- /dev/null +++ b/proofs/CookiePathNotPrefix.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookiePathNotPrefix.bend +import Base +import ../laws/CookiePathNotPrefix.bend as Laws + +def Laws.cookie_path_not_prefix(): + {==} diff --git a/proofs/CookiePathPrefix.bend b/proofs/CookiePathPrefix.bend new file mode 100644 index 0000000..0aa37d1 --- /dev/null +++ b/proofs/CookiePathPrefix.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookiePathPrefix.bend +import Base +import ../laws/CookiePathPrefix.bend as Laws + +def Laws.cookie_path_prefix(): + {==} diff --git a/proofs/CookiePathRoot.bend b/proofs/CookiePathRoot.bend new file mode 100644 index 0000000..d6a4a24 --- /dev/null +++ b/proofs/CookiePathRoot.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookiePathRoot.bend +import Base +import ../laws/CookiePathRoot.bend as Laws + +def Laws.cookie_path_root(): + {==} diff --git a/proofs/CookiePathSlashPrefix.bend b/proofs/CookiePathSlashPrefix.bend new file mode 100644 index 0000000..6805930 --- /dev/null +++ b/proofs/CookiePathSlashPrefix.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookiePathSlashPrefix.bend +import Base +import ../laws/CookiePathSlashPrefix.bend as Laws + +def Laws.cookie_path_slash_prefix(): + {==} diff --git a/proofs/CookieReceiveHostOnly.bend b/proofs/CookieReceiveHostOnly.bend new file mode 100644 index 0000000..24b6be5 --- /dev/null +++ b/proofs/CookieReceiveHostOnly.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieReceiveHostOnly.bend +import Base +import ../laws/CookieReceiveHostOnly.bend as Laws + +def Laws.cookie_receive_host_only(): + {==} 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/CookieRequestJoins.bend b/proofs/CookieRequestJoins.bend new file mode 100644 index 0000000..7aaaa37 --- /dev/null +++ b/proofs/CookieRequestJoins.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieRequestJoins.bend +import Base +import ../laws/CookieRequestJoins.bend as Laws + +def Laws.cookie_request_joins(): + {==} diff --git a/proofs/CookieSamesiteNone.bend b/proofs/CookieSamesiteNone.bend new file mode 100644 index 0000000..a63f3f0 --- /dev/null +++ b/proofs/CookieSamesiteNone.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSamesiteNone.bend +import Base +import ../laws/CookieSamesiteNone.bend as Laws + +def Laws.cookie_samesite_none(): + {==} diff --git a/proofs/CookieSamesiteStrict.bend b/proofs/CookieSamesiteStrict.bend new file mode 100644 index 0000000..a8d787d --- /dev/null +++ b/proofs/CookieSamesiteStrict.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSamesiteStrict.bend +import Base +import ../laws/CookieSamesiteStrict.bend as Laws + +def Laws.cookie_samesite_strict(): + {==} diff --git a/proofs/CookieSamesiteUnknown.bend b/proofs/CookieSamesiteUnknown.bend new file mode 100644 index 0000000..5776300 --- /dev/null +++ b/proofs/CookieSamesiteUnknown.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSamesiteUnknown.bend +import Base +import ../laws/CookieSamesiteUnknown.bend as Laws + +def Laws.cookie_samesite_unknown(): + {==} 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/CookieSecureOnTls.bend b/proofs/CookieSecureOnTls.bend new file mode 100644 index 0000000..c62e2fe --- /dev/null +++ b/proofs/CookieSecureOnTls.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSecureOnTls.bend +import Base +import ../laws/CookieSecureOnTls.bend as Laws + +def Laws.cookie_secure_on_tls(): + {==} diff --git a/proofs/CookieSendHostOnly.bend b/proofs/CookieSendHostOnly.bend new file mode 100644 index 0000000..42396ec --- /dev/null +++ b/proofs/CookieSendHostOnly.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSendHostOnly.bend +import Base +import ../laws/CookieSendHostOnly.bend as Laws + +def Laws.cookie_send_host_only(): + {==} diff --git a/proofs/CookieSendOtherHost.bend b/proofs/CookieSendOtherHost.bend new file mode 100644 index 0000000..c655aec --- /dev/null +++ b/proofs/CookieSendOtherHost.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSendOtherHost.bend +import Base +import ../laws/CookieSendOtherHost.bend as Laws + +def Laws.cookie_send_other_host(): + {==} diff --git a/proofs/CookieSendUnsetDomain.bend b/proofs/CookieSendUnsetDomain.bend new file mode 100644 index 0000000..9854e7c --- /dev/null +++ b/proofs/CookieSendUnsetDomain.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSendUnsetDomain.bend +import Base +import ../laws/CookieSendUnsetDomain.bend as Laws + +def Laws.cookie_send_unset_domain(): + {==} diff --git a/proofs/CookieSessionAlive.bend b/proofs/CookieSessionAlive.bend new file mode 100644 index 0000000..fd789ef --- /dev/null +++ b/proofs/CookieSessionAlive.bend @@ -0,0 +1,6 @@ +# proofs of laws/CookieSessionAlive.bend +import Base +import ../laws/CookieSessionAlive.bend as Laws + +def Laws.cookie_session_alive(): + {==} diff --git a/proofs/CorsCredentialsExplicitOrigin.bend b/proofs/CorsCredentialsExplicitOrigin.bend new file mode 100644 index 0000000..7438c83 --- /dev/null +++ b/proofs/CorsCredentialsExplicitOrigin.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsCredentialsExplicitOrigin.bend +import Base +import ../laws/CorsCredentialsExplicitOrigin.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/CorsExplicitOrigin.bend b/proofs/CorsExplicitOrigin.bend new file mode 100644 index 0000000..0e821cc --- /dev/null +++ b/proofs/CorsExplicitOrigin.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsExplicitOrigin.bend +import Base +import ../laws/CorsExplicitOrigin.bend as Laws + +def Laws.cors_explicit_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/CorsOnPreflight.bend b/proofs/CorsOnPreflight.bend new file mode 100644 index 0000000..6a9e548 --- /dev/null +++ b/proofs/CorsOnPreflight.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsOnPreflight.bend +import Base +import ../laws/CorsOnPreflight.bend as Laws + +def Laws.cors_on_preflight(): + {==} diff --git a/proofs/CorsOriginHeader.bend b/proofs/CorsOriginHeader.bend new file mode 100644 index 0000000..491b0b3 --- /dev/null +++ b/proofs/CorsOriginHeader.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsOriginHeader.bend +import Base +import ../laws/CorsOriginHeader.bend as Laws + +def Laws.cors_origin_header(): + {==} diff --git a/proofs/CorsPreflightAllowed.bend b/proofs/CorsPreflightAllowed.bend new file mode 100644 index 0000000..a12d96b --- /dev/null +++ b/proofs/CorsPreflightAllowed.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsPreflightAllowed.bend +import Base +import ../laws/CorsPreflightAllowed.bend as Laws + +def Laws.cors_preflight_allowed(): + {==} diff --git a/proofs/CorsPreflightBad.bend b/proofs/CorsPreflightBad.bend new file mode 100644 index 0000000..106f937 --- /dev/null +++ b/proofs/CorsPreflightBad.bend @@ -0,0 +1,7 @@ +# proofs of laws/CorsPreflightBad.bend +import Base +import ../laws/CorsPreflightBad.bend as Laws + +# cors_preflight_bad: preflight_req matches the request first +def Laws.cors_preflight_bad(_cfg, _why): + {==} diff --git a/proofs/CorsPreflightHeaderCase.bend b/proofs/CorsPreflightHeaderCase.bend new file mode 100644 index 0000000..363cb86 --- /dev/null +++ b/proofs/CorsPreflightHeaderCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsPreflightHeaderCase.bend +import Base +import ../laws/CorsPreflightHeaderCase.bend as Laws + +def Laws.cors_preflight_header_case(): + {==} 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/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/CorsPreflightStar.bend b/proofs/CorsPreflightStar.bend new file mode 100644 index 0000000..c728023 --- /dev/null +++ b/proofs/CorsPreflightStar.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsPreflightStar.bend +import Base +import ../laws/CorsPreflightStar.bend as Laws + +def Laws.cors_preflight_star(): + {==} diff --git a/proofs/CorsRefusedOrigin.bend b/proofs/CorsRefusedOrigin.bend new file mode 100644 index 0000000..ee79e17 --- /dev/null +++ b/proofs/CorsRefusedOrigin.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsRefusedOrigin.bend +import Base +import ../laws/CorsRefusedOrigin.bend as Laws + +def Laws.cors_refused_origin(): + {==} diff --git a/proofs/CorsRequestHeadersHeader.bend b/proofs/CorsRequestHeadersHeader.bend new file mode 100644 index 0000000..d89e8d5 --- /dev/null +++ b/proofs/CorsRequestHeadersHeader.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsRequestHeadersHeader.bend +import Base +import ../laws/CorsRequestHeadersHeader.bend as Laws + +def Laws.cors_request_headers_header(): + {==} diff --git a/proofs/CorsRequestMethodHeader.bend b/proofs/CorsRequestMethodHeader.bend new file mode 100644 index 0000000..609b00a --- /dev/null +++ b/proofs/CorsRequestMethodHeader.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsRequestMethodHeader.bend +import Base +import ../laws/CorsRequestMethodHeader.bend as Laws + +def Laws.cors_request_method_header(): + {==} diff --git a/proofs/CorsSimpleReflects.bend b/proofs/CorsSimpleReflects.bend new file mode 100644 index 0000000..e245662 --- /dev/null +++ b/proofs/CorsSimpleReflects.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsSimpleReflects.bend +import Base +import ../laws/CorsSimpleReflects.bend as Laws + +def Laws.cors_simple_reflects(): + {==} diff --git a/proofs/CorsSimpleRefused.bend b/proofs/CorsSimpleRefused.bend new file mode 100644 index 0000000..ebad234 --- /dev/null +++ b/proofs/CorsSimpleRefused.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsSimpleRefused.bend +import Base +import ../laws/CorsSimpleRefused.bend as Laws + +def Laws.cors_simple_refused(): + {==} diff --git a/proofs/CorsSimpleStar.bend b/proofs/CorsSimpleStar.bend new file mode 100644 index 0000000..3a9a53f --- /dev/null +++ b/proofs/CorsSimpleStar.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsSimpleStar.bend +import Base +import ../laws/CorsSimpleStar.bend as Laws + +def Laws.cors_simple_star(): + {==} diff --git a/proofs/CorsStarWithoutCredentials.bend b/proofs/CorsStarWithoutCredentials.bend new file mode 100644 index 0000000..e173fc2 --- /dev/null +++ b/proofs/CorsStarWithoutCredentials.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsStarWithoutCredentials.bend +import Base +import ../laws/CorsStarWithoutCredentials.bend as Laws + +def Laws.cors_star_without_credentials(): + {==} diff --git a/proofs/CorsVaryEmpty.bend b/proofs/CorsVaryEmpty.bend new file mode 100644 index 0000000..e0b26d9 --- /dev/null +++ b/proofs/CorsVaryEmpty.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsVaryEmpty.bend +import Base +import ../laws/CorsVaryEmpty.bend as Laws + +def Laws.cors_vary_empty(): + {==} diff --git a/proofs/CorsVarySpecific.bend b/proofs/CorsVarySpecific.bend new file mode 100644 index 0000000..17edb02 --- /dev/null +++ b/proofs/CorsVarySpecific.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsVarySpecific.bend +import Base +import ../laws/CorsVarySpecific.bend as Laws + +def Laws.cors_vary_specific(): + {==} diff --git a/proofs/CorsVaryStar.bend b/proofs/CorsVaryStar.bend new file mode 100644 index 0000000..f6b0880 --- /dev/null +++ b/proofs/CorsVaryStar.bend @@ -0,0 +1,6 @@ +# proofs of laws/CorsVaryStar.bend +import Base +import ../laws/CorsVaryStar.bend as Laws + +def Laws.cors_vary_star(): + {==} 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/DechunkEmpty.bend b/proofs/DechunkEmpty.bend new file mode 100644 index 0000000..9296144 --- /dev/null +++ b/proofs/DechunkEmpty.bend @@ -0,0 +1,6 @@ +# proofs of laws/DechunkEmpty.bend +import Base +import ../laws/DechunkEmpty.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/DechunkOnlyLast.bend b/proofs/DechunkOnlyLast.bend new file mode 100644 index 0000000..87f51fe --- /dev/null +++ b/proofs/DechunkOnlyLast.bend @@ -0,0 +1,6 @@ +# proofs of laws/DechunkOnlyLast.bend +import Base +import ../laws/DechunkOnlyLast.bend as Laws + +def Laws.dechunk_only_last(): + {==} 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/DechunkTwoChunks.bend b/proofs/DechunkTwoChunks.bend new file mode 100644 index 0000000..bb29a41 --- /dev/null +++ b/proofs/DechunkTwoChunks.bend @@ -0,0 +1,6 @@ +# proofs of laws/DechunkTwoChunks.bend +import Base +import ../laws/DechunkTwoChunks.bend as Laws + +def Laws.dechunk_two_chunks(): + {==} diff --git a/proofs/DechunkWiki.bend b/proofs/DechunkWiki.bend new file mode 100644 index 0000000..cc89769 --- /dev/null +++ b/proofs/DechunkWiki.bend @@ -0,0 +1,6 @@ +# proofs of laws/DechunkWiki.bend +import Base +import ../laws/DechunkWiki.bend as Laws + +def Laws.dechunk_wiki(): + {==} diff --git a/proofs/DeleteReqNamesPath.bend b/proofs/DeleteReqNamesPath.bend new file mode 100644 index 0000000..86ac9a2 --- /dev/null +++ b/proofs/DeleteReqNamesPath.bend @@ -0,0 +1,12 @@ +# proofs of laws/DeleteReqNamesPath.bend +import Base +import ../src/http.bend as Http +import ../laws/DeleteReqNamesPath.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/DfltHttps.bend b/proofs/DfltHttps.bend new file mode 100644 index 0000000..b57f147 --- /dev/null +++ b/proofs/DfltHttps.bend @@ -0,0 +1,6 @@ +# proofs of laws/DfltHttps.bend +import Base +import ../laws/DfltHttps.bend as Laws + +def Laws.dflt_https(): + {==} diff --git a/proofs/DfltPlain.bend b/proofs/DfltPlain.bend new file mode 100644 index 0000000..da65893 --- /dev/null +++ b/proofs/DfltPlain.bend @@ -0,0 +1,7 @@ +# proofs of laws/DfltPlain.bend +import Base +import ../laws/DfltPlain.bend as Laws + +def Laws.dflt_plain(_scheme, e): + %e : {Bool.pick(U32, _, 443, 80) == 80 : U32} + {==} diff --git a/proofs/DigitsReadDecimal.bend b/proofs/DigitsReadDecimal.bend new file mode 100644 index 0000000..6bd73c3 --- /dev/null +++ b/proofs/DigitsReadDecimal.bend @@ -0,0 +1,6 @@ +# proofs of laws/DigitsReadDecimal.bend +import Base +import ../laws/DigitsReadDecimal.bend as Laws + +def Laws.digits_read_decimal(): + {==} diff --git a/proofs/DigitsReadRejectsJunk.bend b/proofs/DigitsReadRejectsJunk.bend new file mode 100644 index 0000000..128a665 --- /dev/null +++ b/proofs/DigitsReadRejectsJunk.bend @@ -0,0 +1,6 @@ +# proofs of laws/DigitsReadRejectsJunk.bend +import Base +import ../laws/DigitsReadRejectsJunk.bend as Laws + +def Laws.digits_read_rejects_junk(): + {==} 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/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/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/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/FreshMaxAgeLimit.bend b/proofs/FreshMaxAgeLimit.bend new file mode 100644 index 0000000..9173819 --- /dev/null +++ b/proofs/FreshMaxAgeLimit.bend @@ -0,0 +1,6 @@ +# proofs of laws/FreshMaxAgeLimit.bend +import Base +import ../laws/FreshMaxAgeLimit.bend as Laws + +def Laws.fresh_max_age_limit(): + {==} diff --git a/proofs/FreshMaxAgeOne.bend b/proofs/FreshMaxAgeOne.bend new file mode 100644 index 0000000..fd5b9d8 --- /dev/null +++ b/proofs/FreshMaxAgeOne.bend @@ -0,0 +1,6 @@ +# proofs of laws/FreshMaxAgeOne.bend +import Base +import ../laws/FreshMaxAgeOne.bend as Laws + +def Laws.fresh_max_age_one(): + {==} diff --git a/proofs/FreshMaxAgeOpen.bend b/proofs/FreshMaxAgeOpen.bend new file mode 100644 index 0000000..1a7a4f5 --- /dev/null +++ b/proofs/FreshMaxAgeOpen.bend @@ -0,0 +1,6 @@ +# proofs of laws/FreshMaxAgeOpen.bend +import Base +import ../laws/FreshMaxAgeOpen.bend as Laws + +def Laws.fresh_max_age_open(): + {==} diff --git a/proofs/FreshMaxAgeOverridesExpires.bend b/proofs/FreshMaxAgeOverridesExpires.bend new file mode 100644 index 0000000..9b4ea5c --- /dev/null +++ b/proofs/FreshMaxAgeOverridesExpires.bend @@ -0,0 +1,6 @@ +# proofs of laws/FreshMaxAgeOverridesExpires.bend +import Base +import ../laws/FreshMaxAgeOverridesExpires.bend as Laws + +def Laws.fresh_max_age_overrides_expires(): + {==} diff --git a/proofs/FreshMaxAgeZero.bend b/proofs/FreshMaxAgeZero.bend new file mode 100644 index 0000000..0153bc1 --- /dev/null +++ b/proofs/FreshMaxAgeZero.bend @@ -0,0 +1,6 @@ +# proofs of laws/FreshMaxAgeZero.bend +import Base +import ../laws/FreshMaxAgeZero.bend as Laws + +def Laws.fresh_max_age_zero(): + {==} diff --git a/proofs/FreshNoCache.bend b/proofs/FreshNoCache.bend new file mode 100644 index 0000000..96cddd4 --- /dev/null +++ b/proofs/FreshNoCache.bend @@ -0,0 +1,6 @@ +# proofs of laws/FreshNoCache.bend +import Base +import ../laws/FreshNoCache.bend as Laws + +def Laws.fresh_no_cache(): + {==} diff --git a/proofs/FreshNoCacheAny.bend b/proofs/FreshNoCacheAny.bend new file mode 100644 index 0000000..bbeddcc --- /dev/null +++ b/proofs/FreshNoCacheAny.bend @@ -0,0 +1,18 @@ +# proofs of laws/FreshNoCacheAny.bend +import Base +import ../laws/FreshNoCacheAny.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/FreshNoStore.bend b/proofs/FreshNoStore.bend new file mode 100644 index 0000000..681c710 --- /dev/null +++ b/proofs/FreshNoStore.bend @@ -0,0 +1,6 @@ +# proofs of laws/FreshNoStore.bend +import Base +import ../laws/FreshNoStore.bend as Laws + +def Laws.fresh_no_store(): + {==} diff --git a/proofs/FreshNoStoreAny.bend b/proofs/FreshNoStoreAny.bend new file mode 100644 index 0000000..3265d9f --- /dev/null +++ b/proofs/FreshNoStoreAny.bend @@ -0,0 +1,14 @@ +# proofs of laws/FreshNoStoreAny.bend +import Base +import ../laws/FreshNoStoreAny.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/FreshPublicOnly.bend b/proofs/FreshPublicOnly.bend new file mode 100644 index 0000000..64a46c2 --- /dev/null +++ b/proofs/FreshPublicOnly.bend @@ -0,0 +1,6 @@ +# proofs of laws/FreshPublicOnly.bend +import Base +import ../laws/FreshPublicOnly.bend as Laws + +def Laws.fresh_public_only(): + {==} 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/GetReplyKeepsBody.bend b/proofs/GetReplyKeepsBody.bend new file mode 100644 index 0000000..ec0df74 --- /dev/null +++ b/proofs/GetReplyKeepsBody.bend @@ -0,0 +1,6 @@ +# proofs of laws/GetReplyKeepsBody.bend +import Base +import ../laws/GetReplyKeepsBody.bend as Laws + +def Laws.get_reply_keeps_body(): + {==} diff --git a/proofs/GetReqNamesPath.bend b/proofs/GetReqNamesPath.bend new file mode 100644 index 0000000..312b933 --- /dev/null +++ b/proofs/GetReqNamesPath.bend @@ -0,0 +1,10 @@ +# proofs of laws/GetReqNamesPath.bend +import Base +import ../src/http.bend as Http +import ../laws/GetReqNamesPath.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/HeadHitIgnoresCase.bend b/proofs/HeadHitIgnoresCase.bend new file mode 100644 index 0000000..ce27207 --- /dev/null +++ b/proofs/HeadHitIgnoresCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/HeadHitIgnoresCase.bend +import Base +import ../laws/HeadHitIgnoresCase.bend as Laws + +def Laws.head_hit_ignores_case(_h, _name): + {==} diff --git a/proofs/HeadLineRejectsContinuation.bend b/proofs/HeadLineRejectsContinuation.bend new file mode 100644 index 0000000..7f12685 --- /dev/null +++ b/proofs/HeadLineRejectsContinuation.bend @@ -0,0 +1,6 @@ +# proofs of laws/HeadLineRejectsContinuation.bend +import Base +import ../laws/HeadLineRejectsContinuation.bend as Laws + +def Laws.head_line_rejects_continuation(): + {==} diff --git a/proofs/HeadLineRejectsObsFold.bend b/proofs/HeadLineRejectsObsFold.bend new file mode 100644 index 0000000..eb399db --- /dev/null +++ b/proofs/HeadLineRejectsObsFold.bend @@ -0,0 +1,6 @@ +# proofs of laws/HeadLineRejectsObsFold.bend +import Base +import ../laws/HeadLineRejectsObsFold.bend as Laws + +def Laws.head_line_rejects_obs_fold(): + {==} diff --git a/proofs/HeadLineTrims.bend b/proofs/HeadLineTrims.bend new file mode 100644 index 0000000..9dd60b4 --- /dev/null +++ b/proofs/HeadLineTrims.bend @@ -0,0 +1,6 @@ +# proofs of laws/HeadLineTrims.bend +import Base +import ../laws/HeadLineTrims.bend as Laws + +def Laws.head_line_trims(): + {==} 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/HeadReqNamesPath.bend b/proofs/HeadReqNamesPath.bend new file mode 100644 index 0000000..4858fbb --- /dev/null +++ b/proofs/HeadReqNamesPath.bend @@ -0,0 +1,10 @@ +# proofs of laws/HeadReqNamesPath.bend +import Base +import ../src/http.bend as Http +import ../laws/HeadReqNamesPath.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/HeadersFindFirst.bend b/proofs/HeadersFindFirst.bend new file mode 100644 index 0000000..939ac3f --- /dev/null +++ b/proofs/HeadersFindFirst.bend @@ -0,0 +1,6 @@ +# proofs of laws/HeadersFindFirst.bend +import Base +import ../laws/HeadersFindFirst.bend as Laws + +def Laws.headers_find_first(): + {==} diff --git a/proofs/HeadersFindIgnoresCase.bend b/proofs/HeadersFindIgnoresCase.bend new file mode 100644 index 0000000..24d5277 --- /dev/null +++ b/proofs/HeadersFindIgnoresCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/HeadersFindIgnoresCase.bend +import Base +import ../laws/HeadersFindIgnoresCase.bend as Laws + +def Laws.headers_find_ignores_case(): + {==} diff --git a/proofs/HeadersFindMissing.bend b/proofs/HeadersFindMissing.bend new file mode 100644 index 0000000..02e30f6 --- /dev/null +++ b/proofs/HeadersFindMissing.bend @@ -0,0 +1,6 @@ +# proofs of laws/HeadersFindMissing.bend +import Base +import ../laws/HeadersFindMissing.bend as Laws + +def Laws.headers_find_missing(): + {==} diff --git a/proofs/HexlenAlpha.bend b/proofs/HexlenAlpha.bend new file mode 100644 index 0000000..6bc0a52 --- /dev/null +++ b/proofs/HexlenAlpha.bend @@ -0,0 +1,6 @@ +# proofs of laws/HexlenAlpha.bend +import Base +import ../laws/HexlenAlpha.bend as Laws + +def Laws.hexlen_alpha(): + {==} diff --git a/proofs/HexlenExtensionAndCase.bend b/proofs/HexlenExtensionAndCase.bend new file mode 100644 index 0000000..33aef04 --- /dev/null +++ b/proofs/HexlenExtensionAndCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/HexlenExtensionAndCase.bend +import Base +import ../laws/HexlenExtensionAndCase.bend as Laws + +def Laws.hexlen_extension_and_case(): + {==} diff --git a/proofs/HexlenIgnoresExtension.bend b/proofs/HexlenIgnoresExtension.bend new file mode 100644 index 0000000..664ac12 --- /dev/null +++ b/proofs/HexlenIgnoresExtension.bend @@ -0,0 +1,21 @@ +# proofs of laws/HexlenIgnoresExtension.bend +import Base +import ../src/http.bend as Http +import ../laws/HexlenIgnoresExtension.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/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/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/JoinJoined.bend b/proofs/JoinJoined.bend new file mode 100644 index 0000000..5c92291 --- /dev/null +++ b/proofs/JoinJoined.bend @@ -0,0 +1,51 @@ +# proofs of laws/JoinJoined.bend +import Base +import ../src/wirespec.bend as Spec +import ../laws/JoinJoined.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/JsonParseRejects.bend b/proofs/JsonParseRejects.bend new file mode 100644 index 0000000..5e0edb2 --- /dev/null +++ b/proofs/JsonParseRejects.bend @@ -0,0 +1,6 @@ +# proofs of laws/JsonParseRejects.bend +import Base +import ../laws/JsonParseRejects.bend as Laws + +def Laws.json_parse_rejects(): + {==} diff --git a/proofs/JsonParseString.bend b/proofs/JsonParseString.bend new file mode 100644 index 0000000..417cd3a --- /dev/null +++ b/proofs/JsonParseString.bend @@ -0,0 +1,6 @@ +# proofs of laws/JsonParseString.bend +import Base +import ../laws/JsonParseString.bend as Laws + +def Laws.json_parse_string(): + {==} 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/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/MethodIdempotentDelete.bend b/proofs/MethodIdempotentDelete.bend new file mode 100644 index 0000000..f6139a2 --- /dev/null +++ b/proofs/MethodIdempotentDelete.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodIdempotentDelete.bend +import Base +import ../laws/MethodIdempotentDelete.bend as Laws + +def Laws.method_idempotent_delete(): + {==} diff --git a/proofs/MethodIdempotentGet.bend b/proofs/MethodIdempotentGet.bend new file mode 100644 index 0000000..bc651cd --- /dev/null +++ b/proofs/MethodIdempotentGet.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodIdempotentGet.bend +import Base +import ../laws/MethodIdempotentGet.bend as Laws + +def Laws.method_idempotent_get(): + {==} diff --git a/proofs/MethodIdempotentHead.bend b/proofs/MethodIdempotentHead.bend new file mode 100644 index 0000000..8d08328 --- /dev/null +++ b/proofs/MethodIdempotentHead.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodIdempotentHead.bend +import Base +import ../laws/MethodIdempotentHead.bend as Laws + +def Laws.method_idempotent_head(): + {==} diff --git a/proofs/MethodIdempotentOptions.bend b/proofs/MethodIdempotentOptions.bend new file mode 100644 index 0000000..c1d6c14 --- /dev/null +++ b/proofs/MethodIdempotentOptions.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodIdempotentOptions.bend +import Base +import ../laws/MethodIdempotentOptions.bend as Laws + +def Laws.method_idempotent_options(): + {==} diff --git a/proofs/MethodIdempotentPatch.bend b/proofs/MethodIdempotentPatch.bend new file mode 100644 index 0000000..f68bb3f --- /dev/null +++ b/proofs/MethodIdempotentPatch.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodIdempotentPatch.bend +import Base +import ../laws/MethodIdempotentPatch.bend as Laws + +def Laws.method_idempotent_patch(): + {==} diff --git a/proofs/MethodIdempotentPost.bend b/proofs/MethodIdempotentPost.bend new file mode 100644 index 0000000..ff89fce --- /dev/null +++ b/proofs/MethodIdempotentPost.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodIdempotentPost.bend +import Base +import ../laws/MethodIdempotentPost.bend as Laws + +def Laws.method_idempotent_post(): + {==} diff --git a/proofs/MethodIdempotentPut.bend b/proofs/MethodIdempotentPut.bend new file mode 100644 index 0000000..92b6e15 --- /dev/null +++ b/proofs/MethodIdempotentPut.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodIdempotentPut.bend +import Base +import ../laws/MethodIdempotentPut.bend as Laws + +def Laws.method_idempotent_put(): + {==} diff --git a/proofs/MethodSafeDelete.bend b/proofs/MethodSafeDelete.bend new file mode 100644 index 0000000..85dde17 --- /dev/null +++ b/proofs/MethodSafeDelete.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodSafeDelete.bend +import Base +import ../laws/MethodSafeDelete.bend as Laws + +def Laws.method_safe_delete(): + {==} diff --git a/proofs/MethodSafeGet.bend b/proofs/MethodSafeGet.bend new file mode 100644 index 0000000..51751a9 --- /dev/null +++ b/proofs/MethodSafeGet.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodSafeGet.bend +import Base +import ../laws/MethodSafeGet.bend as Laws + +def Laws.method_safe_get(): + {==} diff --git a/proofs/MethodSafeGetLower.bend b/proofs/MethodSafeGetLower.bend new file mode 100644 index 0000000..3f9d0d4 --- /dev/null +++ b/proofs/MethodSafeGetLower.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodSafeGetLower.bend +import Base +import ../laws/MethodSafeGetLower.bend as Laws + +def Laws.method_safe_get_lower(): + {==} diff --git a/proofs/MethodSafeHead.bend b/proofs/MethodSafeHead.bend new file mode 100644 index 0000000..aebfa3c --- /dev/null +++ b/proofs/MethodSafeHead.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodSafeHead.bend +import Base +import ../laws/MethodSafeHead.bend as Laws + +def Laws.method_safe_head(): + {==} diff --git a/proofs/MethodSafeOptions.bend b/proofs/MethodSafeOptions.bend new file mode 100644 index 0000000..f8fe2f8 --- /dev/null +++ b/proofs/MethodSafeOptions.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodSafeOptions.bend +import Base +import ../laws/MethodSafeOptions.bend as Laws + +def Laws.method_safe_options(): + {==} diff --git a/proofs/MethodSafePost.bend b/proofs/MethodSafePost.bend new file mode 100644 index 0000000..d4e37c2 --- /dev/null +++ b/proofs/MethodSafePost.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodSafePost.bend +import Base +import ../laws/MethodSafePost.bend as Laws + +def Laws.method_safe_post(): + {==} diff --git a/proofs/MethodSafePut.bend b/proofs/MethodSafePut.bend new file mode 100644 index 0000000..a3369b4 --- /dev/null +++ b/proofs/MethodSafePut.bend @@ -0,0 +1,6 @@ +# proofs of laws/MethodSafePut.bend +import Base +import ../laws/MethodSafePut.bend as Laws + +def Laws.method_safe_put(): + {==} 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/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/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/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/OptionsReqNamesPath.bend b/proofs/OptionsReqNamesPath.bend new file mode 100644 index 0000000..3f16fed --- /dev/null +++ b/proofs/OptionsReqNamesPath.bend @@ -0,0 +1,10 @@ +# proofs of laws/OptionsReqNamesPath.bend +import Base +import ../src/http.bend as Http +import ../laws/OptionsReqNamesPath.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/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/ParseChunkedBody.bend b/proofs/ParseChunkedBody.bend new file mode 100644 index 0000000..c15274f --- /dev/null +++ b/proofs/ParseChunkedBody.bend @@ -0,0 +1,6 @@ +# proofs of laws/ParseChunkedBody.bend +import Base +import ../laws/ParseChunkedBody.bend as Laws + +def Laws.parse_chunked_body(): + {==} diff --git a/proofs/ParseChunkedIgnoresCase.bend b/proofs/ParseChunkedIgnoresCase.bend new file mode 100644 index 0000000..e0f9a02 --- /dev/null +++ b/proofs/ParseChunkedIgnoresCase.bend @@ -0,0 +1,6 @@ +# proofs of laws/ParseChunkedIgnoresCase.bend +import Base +import ../laws/ParseChunkedIgnoresCase.bend as Laws + +def Laws.parse_chunked_ignores_case(): + {==} diff --git a/proofs/ParseClenTakesPrefix.bend b/proofs/ParseClenTakesPrefix.bend new file mode 100644 index 0000000..da399cf --- /dev/null +++ b/proofs/ParseClenTakesPrefix.bend @@ -0,0 +1,6 @@ +# proofs of laws/ParseClenTakesPrefix.bend +import Base +import ../laws/ParseClenTakesPrefix.bend as Laws + +def Laws.parse_clen_takes_prefix(): + {==} diff --git a/proofs/ParseEqualContentLengths.bend b/proofs/ParseEqualContentLengths.bend new file mode 100644 index 0000000..2b4f815 --- /dev/null +++ b/proofs/ParseEqualContentLengths.bend @@ -0,0 +1,6 @@ +# proofs of laws/ParseEqualContentLengths.bend +import Base +import ../laws/ParseEqualContentLengths.bend as Laws + +def Laws.parse_equal_content_lengths(): + {==} diff --git a/proofs/ParseNeedsBlankLine.bend b/proofs/ParseNeedsBlankLine.bend new file mode 100644 index 0000000..e77d2cd --- /dev/null +++ b/proofs/ParseNeedsBlankLine.bend @@ -0,0 +1,10 @@ +# proofs of laws/ParseNeedsBlankLine.bend +import Base +import ../src/http.bend as Http +import ../laws/ParseNeedsBlankLine.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/ParseNeedsSlashes.bend b/proofs/ParseNeedsSlashes.bend new file mode 100644 index 0000000..a32b677 --- /dev/null +++ b/proofs/ParseNeedsSlashes.bend @@ -0,0 +1,10 @@ +# proofs of laws/ParseNeedsSlashes.bend +import Base +import ../src/url.bend as Url +import ../laws/ParseNeedsSlashes.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/ParseNeedsStatus.bend b/proofs/ParseNeedsStatus.bend new file mode 100644 index 0000000..8c2bc1b --- /dev/null +++ b/proofs/ParseNeedsStatus.bend @@ -0,0 +1,10 @@ +# proofs of laws/ParseNeedsStatus.bend +import Base +import ../src/http.bend as Http +import ../laws/ParseNeedsStatus.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/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/ParseRejectsClAndTe.bend b/proofs/ParseRejectsClAndTe.bend new file mode 100644 index 0000000..a4523f5 --- /dev/null +++ b/proofs/ParseRejectsClAndTe.bend @@ -0,0 +1,6 @@ +# proofs of laws/ParseRejectsClAndTe.bend +import Base +import ../laws/ParseRejectsClAndTe.bend as Laws + +def Laws.parse_rejects_cl_and_te(): + {==} diff --git a/proofs/ParseRejectsDisagreeingCl.bend b/proofs/ParseRejectsDisagreeingCl.bend new file mode 100644 index 0000000..363887e --- /dev/null +++ b/proofs/ParseRejectsDisagreeingCl.bend @@ -0,0 +1,6 @@ +# proofs of laws/ParseRejectsDisagreeingCl.bend +import Base +import ../laws/ParseRejectsDisagreeingCl.bend as Laws + +def Laws.parse_rejects_disagreeing_cl(): + {==} diff --git a/proofs/ParseRejectsNonHexChunk.bend b/proofs/ParseRejectsNonHexChunk.bend new file mode 100644 index 0000000..c05e393 --- /dev/null +++ b/proofs/ParseRejectsNonHexChunk.bend @@ -0,0 +1,6 @@ +# proofs of laws/ParseRejectsNonHexChunk.bend +import Base +import ../laws/ParseRejectsNonHexChunk.bend as Laws + +def Laws.parse_rejects_non_hex_chunk(): + {==} diff --git a/proofs/ParseSchemeFirst.bend b/proofs/ParseSchemeFirst.bend new file mode 100644 index 0000000..a4a8ba3 --- /dev/null +++ b/proofs/ParseSchemeFirst.bend @@ -0,0 +1,9 @@ +# proofs of laws/ParseSchemeFirst.bend +import Base +import ../src/url.bend as Url +import ../laws/ParseSchemeFirst.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/PathAbsolute.bend b/proofs/PathAbsolute.bend new file mode 100644 index 0000000..65dce93 --- /dev/null +++ b/proofs/PathAbsolute.bend @@ -0,0 +1,11 @@ +# proofs of laws/PathAbsolute.bend +import Base +import ../laws/PathAbsolute.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/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/PostReqNamesPath.bend b/proofs/PostReqNamesPath.bend new file mode 100644 index 0000000..4f4cc18 --- /dev/null +++ b/proofs/PostReqNamesPath.bend @@ -0,0 +1,12 @@ +# proofs of laws/PostReqNamesPath.bend +import Base +import ../src/http.bend as Http +import ../laws/PostReqNamesPath.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/PutReqNamesPath.bend b/proofs/PutReqNamesPath.bend new file mode 100644 index 0000000..885ccb2 --- /dev/null +++ b/proofs/PutReqNamesPath.bend @@ -0,0 +1,12 @@ +# proofs of laws/PutReqNamesPath.bend +import Base +import ../src/http.bend as Http +import ../laws/PutReqNamesPath.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/Reason200.bend b/proofs/Reason200.bend new file mode 100644 index 0000000..61fc9fa --- /dev/null +++ b/proofs/Reason200.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason200.bend +import Base +import ../laws/Reason200.bend as Laws + +def Laws.reason_200(): + {==} diff --git a/proofs/Reason201.bend b/proofs/Reason201.bend new file mode 100644 index 0000000..3e397ce --- /dev/null +++ b/proofs/Reason201.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason201.bend +import Base +import ../laws/Reason201.bend as Laws + +def Laws.reason_201(): + {==} diff --git a/proofs/Reason204.bend b/proofs/Reason204.bend new file mode 100644 index 0000000..2091555 --- /dev/null +++ b/proofs/Reason204.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason204.bend +import Base +import ../laws/Reason204.bend as Laws + +def Laws.reason_204(): + {==} diff --git a/proofs/Reason400.bend b/proofs/Reason400.bend new file mode 100644 index 0000000..908cd54 --- /dev/null +++ b/proofs/Reason400.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason400.bend +import Base +import ../laws/Reason400.bend as Laws + +def Laws.reason_400(): + {==} diff --git a/proofs/Reason404.bend b/proofs/Reason404.bend new file mode 100644 index 0000000..0e50812 --- /dev/null +++ b/proofs/Reason404.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason404.bend +import Base +import ../laws/Reason404.bend as Laws + +def Laws.reason_404(): + {==} diff --git a/proofs/Reason405.bend b/proofs/Reason405.bend new file mode 100644 index 0000000..cc2694c --- /dev/null +++ b/proofs/Reason405.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason405.bend +import Base +import ../laws/Reason405.bend as Laws + +def Laws.reason_405(): + {==} diff --git a/proofs/Reason500.bend b/proofs/Reason500.bend new file mode 100644 index 0000000..db9c54e --- /dev/null +++ b/proofs/Reason500.bend @@ -0,0 +1,6 @@ +# proofs of laws/Reason500.bend +import Base +import ../laws/Reason500.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/ReasonUnknownEmpty.bend b/proofs/ReasonUnknownEmpty.bend new file mode 100644 index 0000000..d3ec62a --- /dev/null +++ b/proofs/ReasonUnknownEmpty.bend @@ -0,0 +1,6 @@ +# proofs of laws/ReasonUnknownEmpty.bend +import Base +import ../laws/ReasonUnknownEmpty.bend as Laws + +def Laws.reason_unknown_empty(): + {==} 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/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/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/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/RequestNamesMethod.bend b/proofs/RequestNamesMethod.bend new file mode 100644 index 0000000..6e82c28 --- /dev/null +++ b/proofs/RequestNamesMethod.bend @@ -0,0 +1,11 @@ +# proofs of laws/RequestNamesMethod.bend +import Base +import ../src/http.bend as Http +import ../laws/RequestNamesMethod.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/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/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/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/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/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/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/TokenOkNewline.bend b/proofs/TokenOkNewline.bend new file mode 100644 index 0000000..f50fcb7 --- /dev/null +++ b/proofs/TokenOkNewline.bend @@ -0,0 +1,6 @@ +# proofs of laws/TokenOkNewline.bend +import Base +import ../laws/TokenOkNewline.bend as Laws + +def Laws.token_ok_newline(): + {==} diff --git a/proofs/TokenOkPlain.bend b/proofs/TokenOkPlain.bend new file mode 100644 index 0000000..9ba4f2a --- /dev/null +++ b/proofs/TokenOkPlain.bend @@ -0,0 +1,6 @@ +# proofs of laws/TokenOkPlain.bend +import Base +import ../laws/TokenOkPlain.bend as Laws + +def Laws.token_ok_plain(): + {==} diff --git a/proofs/TokenOkSpace.bend b/proofs/TokenOkSpace.bend new file mode 100644 index 0000000..81073f9 --- /dev/null +++ b/proofs/TokenOkSpace.bend @@ -0,0 +1,6 @@ +# proofs of laws/TokenOkSpace.bend +import Base +import ../laws/TokenOkSpace.bend as Laws + +def Laws.token_ok_space(): + {==} diff --git a/proofs/TokenOkTab.bend b/proofs/TokenOkTab.bend new file mode 100644 index 0000000..bac4429 --- /dev/null +++ b/proofs/TokenOkTab.bend @@ -0,0 +1,6 @@ +# proofs of laws/TokenOkTab.bend +import Base +import ../laws/TokenOkTab.bend as Laws + +def Laws.token_ok_tab(): + {==} 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/UnframedIsRest.bend b/proofs/UnframedIsRest.bend new file mode 100644 index 0000000..f2c3298 --- /dev/null +++ b/proofs/UnframedIsRest.bend @@ -0,0 +1,6 @@ +# proofs of laws/UnframedIsRest.bend +import Base +import ../laws/UnframedIsRest.bend as Laws + +def Laws.unframed_is_rest(_st, _hs, _body): + {==} 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/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/Utf8Split.bend b/proofs/Utf8Split.bend new file mode 100644 index 0000000..2077712 --- /dev/null +++ b/proofs/Utf8Split.bend @@ -0,0 +1,32 @@ +# proofs of laws/Utf8Split.bend +import Base +import ../src/http.bend as Http +import ../laws/Utf8Split.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/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..f2e57c1 100644 --- a/src/entities.bend +++ b/src/entities.bend @@ -1,6 +1,15 @@ # 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. It is data transcribed from another +# document (the same table as Python's `html.entities.html5`) by +# tools/entities.py, which no law reaches: a law can say how the table is +# read (laws/NamedReferencesLongestMatch.bend), not that its rows are the +# standard's. The differential test against `html.unescape` reads every name +# with and without its `;`. 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..b83c650 100644 --- a/tools/entities.py +++ b/tools/entities.py @@ -32,8 +32,16 @@ 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. It is data transcribed from another\n" + "# document (the same table as Python's `html.entities.html5`) by\n" + "# tools/entities.py, which no law reaches: a law can say how the table is\n" + "# read (laws/NamedReferencesLongestMatch.bend), not that its rows are the\n" + "# standard's. The differential test against `html.unescape` reads every name\n" + "# with and without its `;`.\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, " From 1c5cbacdaaa59a6f94f15d202b21e76c2695ba26 Mon Sep 17 00:00:00 2001 From: Noah Gardner Date: Sat, 31 Oct 2026 23:32:08 -0400 Subject: [PATCH 2/3] refactor: name the uninventoried law files for what they state The 203 one-law files the converter named after their law (B64F, AskClenExact, FreshNoStoreAny, ...) now say what the law states: 160 are renamed (Base64EncodesF, RequestBodyOfContentLengthKept, NoStoreNeverFresh, ...) with their proofs; the 43 whose names already read as behaviors stay. Imports and cites follow. The entities table's trust comment no longer claims a differential test against html.unescape, which does not exist: it says tools/entities.py generates the table from WHATWG's entities.json and checks each entry's code points, and that no test compares html_unescape with Python's. Co-Authored-By: Claude Opus 5.5 --- LAWS.bend | 327 +++++++++--------- PROOF.bend | 320 ++++++++--------- ...m.bend => AbsoluteFormTargetRejected.bend} | 0 ...arked.bend => BadRequestShowIsMarked.bend} | 0 ...{B64Empty.bend => Base64EmptyIsEmpty.bend} | 0 laws/{B64F.bend => Base64EncodesF.bend} | 0 laws/{B64Fo.bend => Base64EncodesFo.bend} | 0 laws/{B64Foo.bend => Base64EncodesFoo.bend} | 0 ...erPass.bend => Base64EncodesUserPass.bend} | 0 ...GoEnd.bend => Base64PadsFinalQuantum.bend} | 0 ....bend => BasicIsBase64OfUserPassword.bend} | 0 ...lain.bend => BearerTokenAcceptsPlain.bend} | 0 ...ne.bend => BearerTokenRejectsNewline.bend} | 0 ...pace.bend => BearerTokenRejectsSpace.bend} | 0 ...nOkTab.bend => BearerTokenRejectsTab.bend} | 0 ...end => CacheDirectiveNamesIgnoreCase.bend} | 0 ...bend => CacheIgnoresUnknownDirective.bend} | 0 ...es.bend => CacheReadsKnownDirectives.bend} | 0 ...eExpires.bend => CacheRecordsExpires.bend} | 0 ...gePresent.bend => CacheRecordsMaxAge.bend} | 0 ...cheAlone.bend => CacheRecordsNoCache.bend} | 0 ...ateAlone.bend => CacheRecordsPrivate.bend} | 0 ...lpha.bend => ChunkSizeHexIgnoresCase.bend} | 0 ...on.bend => ChunkSizeIgnoresExtension.bend} | 0 ...d => ChunkSizeReadsHexSkipsExtension.bend} | 0 ...dBody.bend => ChunkedBodyJoinsChunks.bend} | 0 ...sCase.bend => ChunkedNameIgnoresCase.bend} | 0 ...tyFuel.bend => ChunksStopWithoutFuel.bend} | 0 ...al.bend => ContentLengthReadsDecimal.bend} | 0 ...unk.bend => ContentLengthRejectsJunk.bend} | 0 ...fix.bend => ContentLengthTakesPrefix.bend} | 0 ...ion.bend => ContinuationLineRejected.bend} | 0 ...eOpen.bend => CookieAliveUnderMaxAge.bend} | 0 ...> CookieAttributesJoinWithSemicolons.bend} | 0 ... => CookieDefaultPathFallsBackToRoot.bend} | 0 ...bend => CookieDefaultPathIsDirectory.bend} | 0 ... => CookieDefaultPathNeedsTwoSlashes.bend} | 0 ... CookieDefaultPathOmitsTrailingSlash.bend} | 0 ...bend => CookieDomainMatchesDotSuffix.bend} | 0 ... => CookieDomainNeedsDotBeforeSuffix.bend} | 0 ...nd => CookieHostOnlyNotSentElsewhere.bend} | 0 ...nly.bend => CookieHostOnlySentToHost.bend} | 0 ...nly.bend => CookieHostOnlyStoresHost.bend} | 0 ...al.bend => CookieIdenticalPathsMatch.bend} | 0 ...end => CookieInsecureSentOnCleartext.bend} | 0 ...t.bend => CookieIpHostMatchesExactly.bend} | 0 ...ix.bend => CookieIpHostNoSuffixMatch.bend} | 0 ...Cookie.bend => CookieLongerPathFails.bend} | 0 ...ne.bend => CookieMaxAgeOneStaleAtOne.bend} | 0 ...eAgeSets.bend => CookieMaxAgeSetsAge.bend} | 0 ...ero.bend => CookieMaxAgeZeroNotAlive.bend} | 0 ...Ipv4Name.bend => CookieNameIsNotIpv4.bend} | 0 ...nd => CookieNoDomainNotSentElsewhere.bend} | 0 ...bend => CookiePathMatchesSlashPrefix.bend} | 0 ...nd => CookiePathNeedsSegmentBoundary.bend} | 0 ....bend => CookieRecognizesIpv4Literal.bend} | 0 ...oins.bend => CookieRequestJoinsPairs.bend} | 0 ....bend => CookieRootPathMatchesLonger.bend} | 0 ...rict.bend => CookieSameSiteFoldsCase.bend} | 0 ....bend => CookieSameSiteNoneCanonical.bend} | 0 ...eOnTls.bend => CookieSecureSentOnTls.bend} | 0 ...live.bend => CookieSessionStaysAlive.bend} | 0 ...bend => CookieSlashEndedPathIsPrefix.bend} | 0 ...bend => CookieUnknownSameSiteDropped.bend} | 0 ....bend => CorsAllowListReflectsOrigin.bend} | 0 ...ed.bend => CorsAllowedPreflightIs200.bend} | 0 ...ghtBad.bend => CorsBadPreflightIs400.bend} | 0 ...=> CorsCredentialsKeepExplicitOrigin.bend} | 0 ...rsVaryStar.bend => CorsNoVaryForStar.bend} | 0 ...yEmpty.bend => CorsNoVaryWhenRefused.bend} | 0 ... => CorsOptionsWithMethodIsPreflight.bend} | 0 ...tar.bend => CorsPreflightAnswersStar.bend} | 0 ...nd => CorsPreflightHeadersIgnoreCase.bend} | 0 ...Header.bend => CorsReadsOriginHeader.bend} | 0 ...ader.bend => CorsReadsRequestHeaders.bend} | 0 ...eader.bend => CorsReadsRequestMethod.bend} | 0 ...fused.bend => CorsRefusedLeavesReply.bend} | 0 ...gin.bend => CorsRefusesMissingOrigin.bend} | 0 ...leStar.bend => CorsSimpleAnswersStar.bend} | 0 ...Reflects.bend => CorsSimpleKeepsBody.bend} | 0 ...redentials.bend => CorsStarStaysStar.bend} | 0 ...fic.bend => CorsVaryOnSpecificOrigin.bend} | 0 ...unkEmpty.bend => DechunkEmptyIsEmpty.bend} | 0 ...Chunks.bend => DechunkJoinsTwoChunks.bend} | 0 ...bend => DechunkLastChunkAloneIsEmpty.bend} | 0 ...echunkWiki.bend => DechunkRfcExample.bend} | 0 ...entDelete.bend => DeleteIsIdempotent.bend} | 0 ...thodSafeDelete.bend => DeleteNotSafe.bend} | 0 ...qNamesPath.bend => DeleteRequestLine.bend} | 0 ...hs.bend => EqualContentLengthsAreOne.bend} | 0 ...t.bend => ExactContentLengthBodyKept.bend} | 0 ...dFirst.bend => FieldLookupFindsFirst.bend} | 0 ...sCase.bend => FieldLookupIgnoresCase.bend} | 0 ...resCase.bend => FieldNamesIgnoreCase.bend} | 0 ...ms.bend => FieldValueTrimsWhitespace.bend} | 0 ...hMaxAgeOpen.bend => FreshUnderMaxAge.bend} | 0 ...ed.bend => GetAsteriskTargetRejected.bend} | 0 ...dempotentGet.bend => GetIsIdempotent.bend} | 0 laws/{MethodSafeGet.bend => GetIsSafe.bend} | 0 ...tReqNamesPath.bend => GetRequestLine.bend} | 0 ...mpotentHead.bend => HeadIsIdempotent.bend} | 0 laws/{MethodSafeHead.bend => HeadIsSafe.bend} | 0 ...ReqNamesPath.bend => HeadRequestLine.bend} | 0 ...DfltHttps.bend => HttpsDefaultsTo443.bend} | 0 ...inJoined.bend => JoinedPiecesAreText.bend} | 0 ...seRejects.bend => JsonRejectsNonJson.bend} | 0 ...eString.bend => JsonStringParsesBack.bend} | 0 ...GetLower.bend => LowercaseGetNotSafe.bend} | 0 ...eOne.bend => MaxAgeOneFreshThenStale.bend} | 0 ...pires.bend => MaxAgeOverridesExpires.bend} | 0 ...MaxAgeZero.bend => MaxAgeZeroIsStale.bend} | 0 ...dMissing.bend => MissingFieldIsEmpty.bend} | 0 ...NoCacheAny.bend => NoCacheNeverFresh.bend} | 0 ...FreshNoCache.bend => NoCacheNotFresh.bend} | 0 ...hPublicOnly.bend => NoMaxAgeNotFresh.bend} | 0 ...NoStoreAny.bend => NoStoreNeverFresh.bend} | 0 ...FreshNoStore.bend => NoStoreNotFresh.bend} | 0 ...psBody.bend => NonHeadReplyKeepsBody.bend} | 0 ...HexChunk.bend => NonHexChunkSizeTorn.bend} | 0 ...jectsObsFold.bend => ObsFoldRejected.bend} | 0 ...k.bend => OptionsAsteriskNamesServer.bend} | 0 ...tOptions.bend => OptionsIsIdempotent.bend} | 0 ...hodSafeOptions.bend => OptionsIsSafe.bend} | 0 ...NamesPath.bend => OptionsRequestLine.bend} | 0 ...psQuery.bend => OriginFormKeepsQuery.bend} | 0 ...lain.bend => OtherSchemesDefaultTo80.bend} | 0 ...tentPatch.bend => PatchNotIdempotent.bend} | 0 ...potentPost.bend => PostNotIdempotent.bend} | 0 .../{MethodSafePost.bend => PostNotSafe.bend} | 0 ...ReqNamesPath.bend => PostRequestLine.bend} | 0 ...dempotentPut.bend => PutIsIdempotent.bend} | 0 laws/{MethodSafePut.bend => PutNotSafe.bend} | 0 ...tReqNamesPath.bend => PutRequestLine.bend} | 0 laws/{Reason200.bend => Reason200IsOk.bend} | 0 ...Reason201.bend => Reason201IsCreated.bend} | 0 ...ason204.bend => Reason204IsNoContent.bend} | 0 ...son400.bend => Reason400IsBadRequest.bend} | 0 ...eason404.bend => Reason404IsNotFound.bend} | 0 ....bend => Reason405IsMethodNotAllowed.bend} | 0 ...nd => Reason500IsInternalServerError.bend} | 0 ...arget.bend => RelativeTargetRejected.bend} | 0 ...nd => RequestBodyOfContentLengthKept.bend} | 0 ...estDisagreeingContentLengthsRejected.bend} | 0 ...questEqualContentLengthsFrameOneBody.bend} | 0 ...end => RequestKeepsFieldNameSpelling.bend} | 0 ...d.bend => RequestLineOpensWithMethod.bend} | 0 ...nkLine.bend => RequestNeedsBlankLine.bend} | 0 ...Line.bend => RequestNeedsRequestLine.bend} | 0 ...d => RequestWithBothFramingsRejected.bend} | 0 ...esponseDisagreeingContentLengthsTorn.bend} | 0 ...kLine.bend => ResponseNeedsBlankLine.bend} | 0 ...dsStatus.bend => ResponseNeedsStatus.bend} | 0 ...bend => ResponseWithBothFramingsTorn.bend} | 0 ...First.bend => SchemeBeforeFirstColon.bend} | 0 ...oundTrip.bend => SetCookieParsesBack.bend} | 0 ...nShort.bend => ShortRequestBodyIsBad.bend} | 0 ...eshMaxAgeLimit.bend => StaleAtMaxAge.bend} | 0 ...lit.bend => TakeDropSplitAtOneOffset.bend} | 0 ...medIsRest.bend => UnframedBodyIsRest.bend} | 0 ....bend => UnknownStatusHasEmptyReason.bend} | 0 ...NeedsSlashes.bend => UrlNeedsSlashes.bend} | 0 ...thAbsolute.bend => UrlPathIsAbsolute.bend} | 0 proofs/AbsoluteFormTargetRejected.bend | 6 + proofs/AskEqualContentLengths.bend | 6 - proofs/AskGetAsteriskRejected.bend | 6 - proofs/AskKeepsFieldNameSpelling.bend | 6 - proofs/AskNeedsRequestLine.bend | 6 - proofs/AskOptionsAsterisk.bend | 6 - proofs/AskOriginFormKeepsQuery.bend | 6 - proofs/AskRejectsAbsoluteForm.bend | 6 - proofs/AskRejectsClAndTe.bend | 6 - proofs/AskRejectsDisagreeingCl.bend | 6 - proofs/AskRejectsRelativeTarget.bend | 6 - proofs/B64Empty.bend | 6 - proofs/B64F.bend | 6 - proofs/B64Fo.bend | 6 - proofs/B64Foo.bend | 6 - proofs/B64GoEnd.bend | 7 - proofs/B64UserPass.bend | 6 - ...arked.bend => BadRequestShowIsMarked.bend} | 4 +- proofs/Base64EmptyIsEmpty.bend | 6 + proofs/Base64EncodesF.bend | 6 + proofs/Base64EncodesFo.bend | 6 + proofs/Base64EncodesFoo.bend | 6 + proofs/Base64EncodesUserPass.bend | 6 + proofs/Base64PadsFinalQuantum.bend | 7 + proofs/BasicIsBase64OfUserPassword.bend | 6 + proofs/BasicUserPass.bend | 6 - proofs/BearerTokenAcceptsPlain.bend | 6 + proofs/BearerTokenRejectsNewline.bend | 6 + proofs/BearerTokenRejectsSpace.bend | 6 + proofs/BearerTokenRejectsTab.bend | 6 + proofs/CacheDirectiveCase.bend | 6 - proofs/CacheDirectiveNamesIgnoreCase.bend | 6 + proofs/CacheDirectives.bend | 6 - proofs/CacheIgnoresSMaxage.bend | 6 - proofs/CacheIgnoresUnknownDirective.bend | 6 + proofs/CacheMaxAgePresent.bend | 6 - proofs/CacheNoCacheAlone.bend | 6 - proofs/CacheNoteExpires.bend | 6 - proofs/CachePrivateAlone.bend | 6 - proofs/CacheReadsKnownDirectives.bend | 6 + proofs/CacheRecordsExpires.bend | 6 + proofs/CacheRecordsMaxAge.bend | 6 + proofs/CacheRecordsNoCache.bend | 6 + proofs/CacheRecordsPrivate.bend | 6 + proofs/ChunkSizeHexIgnoresCase.bend | 6 + ...on.bend => ChunkSizeIgnoresExtension.bend} | 4 +- proofs/ChunkSizeReadsHexSkipsExtension.bend | 6 + proofs/ChunkedBodyJoinsChunks.bend | 6 + proofs/ChunkedNameIgnoresCase.bend | 6 + proofs/ChunksEmptyFuel.bend | 6 - proofs/ChunksStopWithoutFuel.bend | 6 + proofs/ContentLengthReadsDecimal.bend | 6 + proofs/ContentLengthRejectsJunk.bend | 6 + proofs/ContentLengthTakesPrefix.bend | 6 + proofs/ContinuationLineRejected.bend | 6 + proofs/CookieAliveUnderMaxAge.bend | 6 + .../CookieAttributesJoinWithSemicolons.bend | 6 + proofs/CookieDefaultPathDir.bend | 6 - proofs/CookieDefaultPathFallsBackToRoot.bend | 6 + proofs/CookieDefaultPathIsDirectory.bend | 6 + proofs/CookieDefaultPathNeedsTwoSlashes.bend | 6 + .../CookieDefaultPathOmitsTrailingSlash.bend | 6 + proofs/CookieDefaultPathOneSlash.bend | 6 - proofs/CookieDefaultPathRoot.bend | 6 - proofs/CookieDefaultPathTrailing.bend | 6 - proofs/CookieDomainIpExact.bend | 6 - proofs/CookieDomainIpSuffix.bend | 6 - proofs/CookieDomainMatchesDotSuffix.bend | 6 + proofs/CookieDomainNeedsDotBeforeSuffix.bend | 6 + proofs/CookieDomainNotSuffix.bend | 6 - proofs/CookieDomainSuffix.bend | 6 - proofs/CookieHostOnlyNotSentElsewhere.bend | 6 + proofs/CookieHostOnlySentToHost.bend | 6 + proofs/CookieHostOnlyStoresHost.bend | 6 + proofs/CookieIdenticalPathsMatch.bend | 6 + proofs/CookieInsecureOnTcp.bend | 6 - proofs/CookieInsecureSentOnCleartext.bend | 6 + proofs/CookieIpHostMatchesExactly.bend | 6 + proofs/CookieIpHostNoSuffixMatch.bend | 6 + proofs/CookieIpv4Literal.bend | 6 - proofs/CookieIpv4Name.bend | 6 - proofs/CookieLineAttributes.bend | 6 - proofs/CookieLongerPathFails.bend | 6 + proofs/CookieMaxAgeOne.bend | 6 - proofs/CookieMaxAgeOneStaleAtOne.bend | 6 + proofs/CookieMaxAgeOpen.bend | 6 - ...eAgeSets.bend => CookieMaxAgeSetsAge.bend} | 4 +- proofs/CookieMaxAgeZero.bend | 6 - proofs/CookieMaxAgeZeroNotAlive.bend | 6 + proofs/CookieNameIsNotIpv4.bend | 6 + proofs/CookieNoDomainNotSentElsewhere.bend | 6 + proofs/CookieParseRoundTrip.bend | 6 - proofs/CookiePathIdentical.bend | 6 - proofs/CookiePathLongerCookie.bend | 6 - proofs/CookiePathMatchesSlashPrefix.bend | 6 + proofs/CookiePathNeedsSegmentBoundary.bend | 6 + proofs/CookiePathNotPrefix.bend | 6 - proofs/CookiePathPrefix.bend | 6 - proofs/CookiePathRoot.bend | 6 - proofs/CookiePathSlashPrefix.bend | 6 - proofs/CookieReceiveHostOnly.bend | 6 - proofs/CookieRecognizesIpv4Literal.bend | 6 + proofs/CookieRequestJoins.bend | 6 - proofs/CookieRequestJoinsPairs.bend | 6 + proofs/CookieRootPathMatchesLonger.bend | 6 + proofs/CookieSameSiteFoldsCase.bend | 6 + proofs/CookieSameSiteNoneCanonical.bend | 6 + proofs/CookieSamesiteNone.bend | 6 - proofs/CookieSamesiteStrict.bend | 6 - proofs/CookieSamesiteUnknown.bend | 6 - proofs/CookieSecureOnTls.bend | 6 - proofs/CookieSecureSentOnTls.bend | 6 + proofs/CookieSendHostOnly.bend | 6 - proofs/CookieSendOtherHost.bend | 6 - proofs/CookieSendUnsetDomain.bend | 6 - proofs/CookieSessionAlive.bend | 6 - proofs/CookieSessionStaysAlive.bend | 6 + proofs/CookieSlashEndedPathIsPrefix.bend | 6 + proofs/CookieUnknownSameSiteDropped.bend | 6 + proofs/CorsAllowListReflectsOrigin.bend | 6 + proofs/CorsAllowedPreflightIs200.bend | 6 + ...ghtBad.bend => CorsBadPreflightIs400.bend} | 4 +- proofs/CorsCredentialsExplicitOrigin.bend | 6 - proofs/CorsCredentialsKeepExplicitOrigin.bend | 6 + proofs/CorsExplicitOrigin.bend | 6 - proofs/CorsNoVaryForStar.bend | 6 + proofs/CorsNoVaryWhenRefused.bend | 6 + proofs/CorsOnPreflight.bend | 6 - proofs/CorsOptionsWithMethodIsPreflight.bend | 6 + proofs/CorsOriginHeader.bend | 6 - proofs/CorsPreflightAllowed.bend | 6 - proofs/CorsPreflightAnswersStar.bend | 6 + proofs/CorsPreflightHeaderCase.bend | 6 - proofs/CorsPreflightHeadersIgnoreCase.bend | 6 + proofs/CorsPreflightStar.bend | 6 - proofs/CorsReadsOriginHeader.bend | 6 + proofs/CorsReadsRequestHeaders.bend | 6 + proofs/CorsReadsRequestMethod.bend | 6 + proofs/CorsRefusedLeavesReply.bend | 6 + proofs/CorsRefusedOrigin.bend | 6 - proofs/CorsRefusesMissingOrigin.bend | 6 + proofs/CorsRequestHeadersHeader.bend | 6 - proofs/CorsRequestMethodHeader.bend | 6 - proofs/CorsSimpleAnswersStar.bend | 6 + proofs/CorsSimpleKeepsBody.bend | 6 + proofs/CorsSimpleReflects.bend | 6 - proofs/CorsSimpleRefused.bend | 6 - proofs/CorsSimpleStar.bend | 6 - proofs/CorsStarStaysStar.bend | 6 + proofs/CorsStarWithoutCredentials.bend | 6 - proofs/CorsVaryEmpty.bend | 6 - proofs/CorsVaryOnSpecificOrigin.bend | 6 + proofs/CorsVarySpecific.bend | 6 - proofs/CorsVaryStar.bend | 6 - proofs/DechunkEmpty.bend | 6 - proofs/DechunkEmptyIsEmpty.bend | 6 + proofs/DechunkJoinsTwoChunks.bend | 6 + proofs/DechunkLastChunkAloneIsEmpty.bend | 6 + proofs/DechunkOnlyLast.bend | 6 - proofs/DechunkRfcExample.bend | 6 + proofs/DechunkTwoChunks.bend | 6 - proofs/DechunkWiki.bend | 6 - proofs/DeleteIsIdempotent.bend | 6 + proofs/DeleteNotSafe.bend | 6 + ...qNamesPath.bend => DeleteRequestLine.bend} | 4 +- proofs/DfltHttps.bend | 6 - proofs/DigitsReadDecimal.bend | 6 - proofs/DigitsReadRejectsJunk.bend | 6 - proofs/EqualContentLengthsAreOne.bend | 6 + ...t.bend => ExactContentLengthBodyKept.bend} | 4 +- proofs/FieldLookupFindsFirst.bend | 6 + proofs/FieldLookupIgnoresCase.bend | 6 + proofs/FieldNamesIgnoreCase.bend | 6 + proofs/FieldValueTrimsWhitespace.bend | 6 + proofs/FreshMaxAgeLimit.bend | 6 - proofs/FreshMaxAgeOne.bend | 6 - proofs/FreshMaxAgeOpen.bend | 6 - proofs/FreshMaxAgeOverridesExpires.bend | 6 - proofs/FreshMaxAgeZero.bend | 6 - proofs/FreshNoCache.bend | 6 - proofs/FreshNoStore.bend | 6 - proofs/FreshPublicOnly.bend | 6 - proofs/FreshUnderMaxAge.bend | 6 + proofs/GetAsteriskTargetRejected.bend | 6 + proofs/GetIsIdempotent.bend | 6 + proofs/GetIsSafe.bend | 6 + proofs/GetReplyKeepsBody.bend | 6 - ...tReqNamesPath.bend => GetRequestLine.bend} | 4 +- proofs/HeadHitIgnoresCase.bend | 6 - proofs/HeadIsIdempotent.bend | 6 + proofs/HeadIsSafe.bend | 6 + proofs/HeadLineRejectsContinuation.bend | 6 - proofs/HeadLineRejectsObsFold.bend | 6 - proofs/HeadLineTrims.bend | 6 - ...ReqNamesPath.bend => HeadRequestLine.bend} | 4 +- proofs/HeadersFindFirst.bend | 6 - proofs/HeadersFindIgnoresCase.bend | 6 - proofs/HeadersFindMissing.bend | 6 - proofs/HexlenAlpha.bend | 6 - proofs/HexlenExtensionAndCase.bend | 6 - proofs/HttpsDefaultsTo443.bend | 6 + ...inJoined.bend => JoinedPiecesAreText.bend} | 4 +- proofs/JsonParseRejects.bend | 6 - proofs/JsonParseString.bend | 6 - proofs/JsonRejectsNonJson.bend | 6 + proofs/JsonStringParsesBack.bend | 6 + proofs/LowercaseGetNotSafe.bend | 6 + proofs/MaxAgeOneFreshThenStale.bend | 6 + proofs/MaxAgeOverridesExpires.bend | 6 + proofs/MaxAgeZeroIsStale.bend | 6 + proofs/MethodIdempotentDelete.bend | 6 - proofs/MethodIdempotentGet.bend | 6 - proofs/MethodIdempotentHead.bend | 6 - proofs/MethodIdempotentOptions.bend | 6 - proofs/MethodIdempotentPatch.bend | 6 - proofs/MethodIdempotentPost.bend | 6 - proofs/MethodIdempotentPut.bend | 6 - proofs/MethodSafeDelete.bend | 6 - proofs/MethodSafeGet.bend | 6 - proofs/MethodSafeGetLower.bend | 6 - proofs/MethodSafeHead.bend | 6 - proofs/MethodSafeOptions.bend | 6 - proofs/MethodSafePost.bend | 6 - proofs/MethodSafePut.bend | 6 - proofs/MissingFieldIsEmpty.bend | 6 + ...NoCacheAny.bend => NoCacheNeverFresh.bend} | 4 +- proofs/NoCacheNotFresh.bend | 6 + proofs/NoMaxAgeNotFresh.bend | 6 + ...NoStoreAny.bend => NoStoreNeverFresh.bend} | 4 +- proofs/NoStoreNotFresh.bend | 6 + proofs/NonHeadReplyKeepsBody.bend | 6 + proofs/NonHexChunkSizeTorn.bend | 6 + proofs/ObsFoldRejected.bend | 6 + proofs/OptionsAsteriskNamesServer.bend | 6 + proofs/OptionsIsIdempotent.bend | 6 + proofs/OptionsIsSafe.bend | 6 + ...NamesPath.bend => OptionsRequestLine.bend} | 4 +- proofs/OriginFormKeepsQuery.bend | 6 + ...lain.bend => OtherSchemesDefaultTo80.bend} | 4 +- proofs/ParseChunkedBody.bend | 6 - proofs/ParseChunkedIgnoresCase.bend | 6 - proofs/ParseClenTakesPrefix.bend | 6 - proofs/ParseEqualContentLengths.bend | 6 - proofs/ParseRejectsClAndTe.bend | 6 - proofs/ParseRejectsDisagreeingCl.bend | 6 - proofs/ParseRejectsNonHexChunk.bend | 6 - proofs/PatchNotIdempotent.bend | 6 + proofs/PostNotIdempotent.bend | 6 + proofs/PostNotSafe.bend | 6 + ...ReqNamesPath.bend => PostRequestLine.bend} | 4 +- proofs/PutIsIdempotent.bend | 6 + proofs/PutNotSafe.bend | 6 + ...tReqNamesPath.bend => PutRequestLine.bend} | 4 +- proofs/Reason200.bend | 6 - proofs/Reason200IsOk.bend | 6 + proofs/Reason201.bend | 6 - proofs/Reason201IsCreated.bend | 6 + proofs/Reason204.bend | 6 - proofs/Reason204IsNoContent.bend | 6 + proofs/Reason400.bend | 6 - proofs/Reason400IsBadRequest.bend | 6 + proofs/Reason404.bend | 6 - proofs/Reason404IsNotFound.bend | 6 + proofs/Reason405.bend | 6 - proofs/Reason405IsMethodNotAllowed.bend | 6 + proofs/Reason500.bend | 6 - proofs/Reason500IsInternalServerError.bend | 6 + proofs/ReasonUnknownEmpty.bend | 6 - proofs/RelativeTargetRejected.bend | 6 + ...nd => RequestBodyOfContentLengthKept.bend} | 4 +- ...uestDisagreeingContentLengthsRejected.bend | 6 + ...equestEqualContentLengthsFrameOneBody.bend | 6 + proofs/RequestKeepsFieldNameSpelling.bend | 6 + ...d.bend => RequestLineOpensWithMethod.bend} | 4 +- ...nkLine.bend => RequestNeedsBlankLine.bend} | 4 +- proofs/RequestNeedsRequestLine.bend | 6 + proofs/RequestWithBothFramingsRejected.bend | 6 + ...ResponseDisagreeingContentLengthsTorn.bend | 6 + ...kLine.bend => ResponseNeedsBlankLine.bend} | 4 +- ...dsStatus.bend => ResponseNeedsStatus.bend} | 4 +- proofs/ResponseWithBothFramingsTorn.bend | 6 + ...First.bend => SchemeBeforeFirstColon.bend} | 4 +- proofs/SetCookieParsesBack.bend | 6 + ...nShort.bend => ShortRequestBodyIsBad.bend} | 4 +- proofs/StaleAtMaxAge.bend | 6 + ...lit.bend => TakeDropSplitAtOneOffset.bend} | 4 +- proofs/TokenOkNewline.bend | 6 - proofs/TokenOkPlain.bend | 6 - proofs/TokenOkSpace.bend | 6 - proofs/TokenOkTab.bend | 6 - proofs/UnframedBodyIsRest.bend | 6 + proofs/UnframedIsRest.bend | 6 - proofs/UnknownStatusHasEmptyReason.bend | 6 + ...NeedsSlashes.bend => UrlNeedsSlashes.bend} | 4 +- ...thAbsolute.bend => UrlPathIsAbsolute.bend} | 4 +- src/entities.bend | 13 +- tools/entities.py | 13 +- 459 files changed, 1199 insertions(+), 1196 deletions(-) rename laws/{AskRejectsAbsoluteForm.bend => AbsoluteFormTargetRejected.bend} (100%) rename laws/{AskShowBadIsMarked.bend => BadRequestShowIsMarked.bend} (100%) rename laws/{B64Empty.bend => Base64EmptyIsEmpty.bend} (100%) rename laws/{B64F.bend => Base64EncodesF.bend} (100%) rename laws/{B64Fo.bend => Base64EncodesFo.bend} (100%) rename laws/{B64Foo.bend => Base64EncodesFoo.bend} (100%) rename laws/{B64UserPass.bend => Base64EncodesUserPass.bend} (100%) rename laws/{B64GoEnd.bend => Base64PadsFinalQuantum.bend} (100%) rename laws/{BasicUserPass.bend => BasicIsBase64OfUserPassword.bend} (100%) rename laws/{TokenOkPlain.bend => BearerTokenAcceptsPlain.bend} (100%) rename laws/{TokenOkNewline.bend => BearerTokenRejectsNewline.bend} (100%) rename laws/{TokenOkSpace.bend => BearerTokenRejectsSpace.bend} (100%) rename laws/{TokenOkTab.bend => BearerTokenRejectsTab.bend} (100%) rename laws/{CacheDirectiveCase.bend => CacheDirectiveNamesIgnoreCase.bend} (100%) rename laws/{CacheIgnoresSMaxage.bend => CacheIgnoresUnknownDirective.bend} (100%) rename laws/{CacheDirectives.bend => CacheReadsKnownDirectives.bend} (100%) rename laws/{CacheNoteExpires.bend => CacheRecordsExpires.bend} (100%) rename laws/{CacheMaxAgePresent.bend => CacheRecordsMaxAge.bend} (100%) rename laws/{CacheNoCacheAlone.bend => CacheRecordsNoCache.bend} (100%) rename laws/{CachePrivateAlone.bend => CacheRecordsPrivate.bend} (100%) rename laws/{HexlenAlpha.bend => ChunkSizeHexIgnoresCase.bend} (100%) rename laws/{HexlenIgnoresExtension.bend => ChunkSizeIgnoresExtension.bend} (100%) rename laws/{HexlenExtensionAndCase.bend => ChunkSizeReadsHexSkipsExtension.bend} (100%) rename laws/{ParseChunkedBody.bend => ChunkedBodyJoinsChunks.bend} (100%) rename laws/{ParseChunkedIgnoresCase.bend => ChunkedNameIgnoresCase.bend} (100%) rename laws/{ChunksEmptyFuel.bend => ChunksStopWithoutFuel.bend} (100%) rename laws/{DigitsReadDecimal.bend => ContentLengthReadsDecimal.bend} (100%) rename laws/{DigitsReadRejectsJunk.bend => ContentLengthRejectsJunk.bend} (100%) rename laws/{ParseClenTakesPrefix.bend => ContentLengthTakesPrefix.bend} (100%) rename laws/{HeadLineRejectsContinuation.bend => ContinuationLineRejected.bend} (100%) rename laws/{CookieMaxAgeOpen.bend => CookieAliveUnderMaxAge.bend} (100%) rename laws/{CookieLineAttributes.bend => CookieAttributesJoinWithSemicolons.bend} (100%) rename laws/{CookieDefaultPathRoot.bend => CookieDefaultPathFallsBackToRoot.bend} (100%) rename laws/{CookieDefaultPathDir.bend => CookieDefaultPathIsDirectory.bend} (100%) rename laws/{CookieDefaultPathOneSlash.bend => CookieDefaultPathNeedsTwoSlashes.bend} (100%) rename laws/{CookieDefaultPathTrailing.bend => CookieDefaultPathOmitsTrailingSlash.bend} (100%) rename laws/{CookieDomainSuffix.bend => CookieDomainMatchesDotSuffix.bend} (100%) rename laws/{CookieDomainNotSuffix.bend => CookieDomainNeedsDotBeforeSuffix.bend} (100%) rename laws/{CookieSendOtherHost.bend => CookieHostOnlyNotSentElsewhere.bend} (100%) rename laws/{CookieSendHostOnly.bend => CookieHostOnlySentToHost.bend} (100%) rename laws/{CookieReceiveHostOnly.bend => CookieHostOnlyStoresHost.bend} (100%) rename laws/{CookiePathIdentical.bend => CookieIdenticalPathsMatch.bend} (100%) rename laws/{CookieInsecureOnTcp.bend => CookieInsecureSentOnCleartext.bend} (100%) rename laws/{CookieDomainIpExact.bend => CookieIpHostMatchesExactly.bend} (100%) rename laws/{CookieDomainIpSuffix.bend => CookieIpHostNoSuffixMatch.bend} (100%) rename laws/{CookiePathLongerCookie.bend => CookieLongerPathFails.bend} (100%) rename laws/{CookieMaxAgeOne.bend => CookieMaxAgeOneStaleAtOne.bend} (100%) rename laws/{CookieAgeSets.bend => CookieMaxAgeSetsAge.bend} (100%) rename laws/{CookieMaxAgeZero.bend => CookieMaxAgeZeroNotAlive.bend} (100%) rename laws/{CookieIpv4Name.bend => CookieNameIsNotIpv4.bend} (100%) rename laws/{CookieSendUnsetDomain.bend => CookieNoDomainNotSentElsewhere.bend} (100%) rename laws/{CookiePathPrefix.bend => CookiePathMatchesSlashPrefix.bend} (100%) rename laws/{CookiePathNotPrefix.bend => CookiePathNeedsSegmentBoundary.bend} (100%) rename laws/{CookieIpv4Literal.bend => CookieRecognizesIpv4Literal.bend} (100%) rename laws/{CookieRequestJoins.bend => CookieRequestJoinsPairs.bend} (100%) rename laws/{CookiePathRoot.bend => CookieRootPathMatchesLonger.bend} (100%) rename laws/{CookieSamesiteStrict.bend => CookieSameSiteFoldsCase.bend} (100%) rename laws/{CookieSamesiteNone.bend => CookieSameSiteNoneCanonical.bend} (100%) rename laws/{CookieSecureOnTls.bend => CookieSecureSentOnTls.bend} (100%) rename laws/{CookieSessionAlive.bend => CookieSessionStaysAlive.bend} (100%) rename laws/{CookiePathSlashPrefix.bend => CookieSlashEndedPathIsPrefix.bend} (100%) rename laws/{CookieSamesiteUnknown.bend => CookieUnknownSameSiteDropped.bend} (100%) rename laws/{CorsExplicitOrigin.bend => CorsAllowListReflectsOrigin.bend} (100%) rename laws/{CorsPreflightAllowed.bend => CorsAllowedPreflightIs200.bend} (100%) rename laws/{CorsPreflightBad.bend => CorsBadPreflightIs400.bend} (100%) rename laws/{CorsCredentialsExplicitOrigin.bend => CorsCredentialsKeepExplicitOrigin.bend} (100%) rename laws/{CorsVaryStar.bend => CorsNoVaryForStar.bend} (100%) rename laws/{CorsVaryEmpty.bend => CorsNoVaryWhenRefused.bend} (100%) rename laws/{CorsOnPreflight.bend => CorsOptionsWithMethodIsPreflight.bend} (100%) rename laws/{CorsPreflightStar.bend => CorsPreflightAnswersStar.bend} (100%) rename laws/{CorsPreflightHeaderCase.bend => CorsPreflightHeadersIgnoreCase.bend} (100%) rename laws/{CorsOriginHeader.bend => CorsReadsOriginHeader.bend} (100%) rename laws/{CorsRequestHeadersHeader.bend => CorsReadsRequestHeaders.bend} (100%) rename laws/{CorsRequestMethodHeader.bend => CorsReadsRequestMethod.bend} (100%) rename laws/{CorsSimpleRefused.bend => CorsRefusedLeavesReply.bend} (100%) rename laws/{CorsRefusedOrigin.bend => CorsRefusesMissingOrigin.bend} (100%) rename laws/{CorsSimpleStar.bend => CorsSimpleAnswersStar.bend} (100%) rename laws/{CorsSimpleReflects.bend => CorsSimpleKeepsBody.bend} (100%) rename laws/{CorsStarWithoutCredentials.bend => CorsStarStaysStar.bend} (100%) rename laws/{CorsVarySpecific.bend => CorsVaryOnSpecificOrigin.bend} (100%) rename laws/{DechunkEmpty.bend => DechunkEmptyIsEmpty.bend} (100%) rename laws/{DechunkTwoChunks.bend => DechunkJoinsTwoChunks.bend} (100%) rename laws/{DechunkOnlyLast.bend => DechunkLastChunkAloneIsEmpty.bend} (100%) rename laws/{DechunkWiki.bend => DechunkRfcExample.bend} (100%) rename laws/{MethodIdempotentDelete.bend => DeleteIsIdempotent.bend} (100%) rename laws/{MethodSafeDelete.bend => DeleteNotSafe.bend} (100%) rename laws/{DeleteReqNamesPath.bend => DeleteRequestLine.bend} (100%) rename laws/{ParseEqualContentLengths.bend => EqualContentLengthsAreOne.bend} (100%) rename laws/{ClenExact.bend => ExactContentLengthBodyKept.bend} (100%) rename laws/{HeadersFindFirst.bend => FieldLookupFindsFirst.bend} (100%) rename laws/{HeadersFindIgnoresCase.bend => FieldLookupIgnoresCase.bend} (100%) rename laws/{HeadHitIgnoresCase.bend => FieldNamesIgnoreCase.bend} (100%) rename laws/{HeadLineTrims.bend => FieldValueTrimsWhitespace.bend} (100%) rename laws/{FreshMaxAgeOpen.bend => FreshUnderMaxAge.bend} (100%) rename laws/{AskGetAsteriskRejected.bend => GetAsteriskTargetRejected.bend} (100%) rename laws/{MethodIdempotentGet.bend => GetIsIdempotent.bend} (100%) rename laws/{MethodSafeGet.bend => GetIsSafe.bend} (100%) rename laws/{GetReqNamesPath.bend => GetRequestLine.bend} (100%) rename laws/{MethodIdempotentHead.bend => HeadIsIdempotent.bend} (100%) rename laws/{MethodSafeHead.bend => HeadIsSafe.bend} (100%) rename laws/{HeadReqNamesPath.bend => HeadRequestLine.bend} (100%) rename laws/{DfltHttps.bend => HttpsDefaultsTo443.bend} (100%) rename laws/{JoinJoined.bend => JoinedPiecesAreText.bend} (100%) rename laws/{JsonParseRejects.bend => JsonRejectsNonJson.bend} (100%) rename laws/{JsonParseString.bend => JsonStringParsesBack.bend} (100%) rename laws/{MethodSafeGetLower.bend => LowercaseGetNotSafe.bend} (100%) rename laws/{FreshMaxAgeOne.bend => MaxAgeOneFreshThenStale.bend} (100%) rename laws/{FreshMaxAgeOverridesExpires.bend => MaxAgeOverridesExpires.bend} (100%) rename laws/{FreshMaxAgeZero.bend => MaxAgeZeroIsStale.bend} (100%) rename laws/{HeadersFindMissing.bend => MissingFieldIsEmpty.bend} (100%) rename laws/{FreshNoCacheAny.bend => NoCacheNeverFresh.bend} (100%) rename laws/{FreshNoCache.bend => NoCacheNotFresh.bend} (100%) rename laws/{FreshPublicOnly.bend => NoMaxAgeNotFresh.bend} (100%) rename laws/{FreshNoStoreAny.bend => NoStoreNeverFresh.bend} (100%) rename laws/{FreshNoStore.bend => NoStoreNotFresh.bend} (100%) rename laws/{GetReplyKeepsBody.bend => NonHeadReplyKeepsBody.bend} (100%) rename laws/{ParseRejectsNonHexChunk.bend => NonHexChunkSizeTorn.bend} (100%) rename laws/{HeadLineRejectsObsFold.bend => ObsFoldRejected.bend} (100%) rename laws/{AskOptionsAsterisk.bend => OptionsAsteriskNamesServer.bend} (100%) rename laws/{MethodIdempotentOptions.bend => OptionsIsIdempotent.bend} (100%) rename laws/{MethodSafeOptions.bend => OptionsIsSafe.bend} (100%) rename laws/{OptionsReqNamesPath.bend => OptionsRequestLine.bend} (100%) rename laws/{AskOriginFormKeepsQuery.bend => OriginFormKeepsQuery.bend} (100%) rename laws/{DfltPlain.bend => OtherSchemesDefaultTo80.bend} (100%) rename laws/{MethodIdempotentPatch.bend => PatchNotIdempotent.bend} (100%) rename laws/{MethodIdempotentPost.bend => PostNotIdempotent.bend} (100%) rename laws/{MethodSafePost.bend => PostNotSafe.bend} (100%) rename laws/{PostReqNamesPath.bend => PostRequestLine.bend} (100%) rename laws/{MethodIdempotentPut.bend => PutIsIdempotent.bend} (100%) rename laws/{MethodSafePut.bend => PutNotSafe.bend} (100%) rename laws/{PutReqNamesPath.bend => PutRequestLine.bend} (100%) rename laws/{Reason200.bend => Reason200IsOk.bend} (100%) rename laws/{Reason201.bend => Reason201IsCreated.bend} (100%) rename laws/{Reason204.bend => Reason204IsNoContent.bend} (100%) rename laws/{Reason400.bend => Reason400IsBadRequest.bend} (100%) rename laws/{Reason404.bend => Reason404IsNotFound.bend} (100%) rename laws/{Reason405.bend => Reason405IsMethodNotAllowed.bend} (100%) rename laws/{Reason500.bend => Reason500IsInternalServerError.bend} (100%) rename laws/{AskRejectsRelativeTarget.bend => RelativeTargetRejected.bend} (100%) rename laws/{AskClenExact.bend => RequestBodyOfContentLengthKept.bend} (100%) rename laws/{AskRejectsDisagreeingCl.bend => RequestDisagreeingContentLengthsRejected.bend} (100%) rename laws/{AskEqualContentLengths.bend => RequestEqualContentLengthsFrameOneBody.bend} (100%) rename laws/{AskKeepsFieldNameSpelling.bend => RequestKeepsFieldNameSpelling.bend} (100%) rename laws/{RequestNamesMethod.bend => RequestLineOpensWithMethod.bend} (100%) rename laws/{AskNeedsBlankLine.bend => RequestNeedsBlankLine.bend} (100%) rename laws/{AskNeedsRequestLine.bend => RequestNeedsRequestLine.bend} (100%) rename laws/{AskRejectsClAndTe.bend => RequestWithBothFramingsRejected.bend} (100%) rename laws/{ParseRejectsDisagreeingCl.bend => ResponseDisagreeingContentLengthsTorn.bend} (100%) rename laws/{ParseNeedsBlankLine.bend => ResponseNeedsBlankLine.bend} (100%) rename laws/{ParseNeedsStatus.bend => ResponseNeedsStatus.bend} (100%) rename laws/{ParseRejectsClAndTe.bend => ResponseWithBothFramingsTorn.bend} (100%) rename laws/{ParseSchemeFirst.bend => SchemeBeforeFirstColon.bend} (100%) rename laws/{CookieParseRoundTrip.bend => SetCookieParsesBack.bend} (100%) rename laws/{AskClenShort.bend => ShortRequestBodyIsBad.bend} (100%) rename laws/{FreshMaxAgeLimit.bend => StaleAtMaxAge.bend} (100%) rename laws/{Utf8Split.bend => TakeDropSplitAtOneOffset.bend} (100%) rename laws/{UnframedIsRest.bend => UnframedBodyIsRest.bend} (100%) rename laws/{ReasonUnknownEmpty.bend => UnknownStatusHasEmptyReason.bend} (100%) rename laws/{ParseNeedsSlashes.bend => UrlNeedsSlashes.bend} (100%) rename laws/{PathAbsolute.bend => UrlPathIsAbsolute.bend} (100%) create mode 100644 proofs/AbsoluteFormTargetRejected.bend delete mode 100644 proofs/AskEqualContentLengths.bend delete mode 100644 proofs/AskGetAsteriskRejected.bend delete mode 100644 proofs/AskKeepsFieldNameSpelling.bend delete mode 100644 proofs/AskNeedsRequestLine.bend delete mode 100644 proofs/AskOptionsAsterisk.bend delete mode 100644 proofs/AskOriginFormKeepsQuery.bend delete mode 100644 proofs/AskRejectsAbsoluteForm.bend delete mode 100644 proofs/AskRejectsClAndTe.bend delete mode 100644 proofs/AskRejectsDisagreeingCl.bend delete mode 100644 proofs/AskRejectsRelativeTarget.bend delete mode 100644 proofs/B64Empty.bend delete mode 100644 proofs/B64F.bend delete mode 100644 proofs/B64Fo.bend delete mode 100644 proofs/B64Foo.bend delete mode 100644 proofs/B64GoEnd.bend delete mode 100644 proofs/B64UserPass.bend rename proofs/{AskShowBadIsMarked.bend => BadRequestShowIsMarked.bend} (54%) create mode 100644 proofs/Base64EmptyIsEmpty.bend create mode 100644 proofs/Base64EncodesF.bend create mode 100644 proofs/Base64EncodesFo.bend create mode 100644 proofs/Base64EncodesFoo.bend create mode 100644 proofs/Base64EncodesUserPass.bend create mode 100644 proofs/Base64PadsFinalQuantum.bend create mode 100644 proofs/BasicIsBase64OfUserPassword.bend delete mode 100644 proofs/BasicUserPass.bend create mode 100644 proofs/BearerTokenAcceptsPlain.bend create mode 100644 proofs/BearerTokenRejectsNewline.bend create mode 100644 proofs/BearerTokenRejectsSpace.bend create mode 100644 proofs/BearerTokenRejectsTab.bend delete mode 100644 proofs/CacheDirectiveCase.bend create mode 100644 proofs/CacheDirectiveNamesIgnoreCase.bend delete mode 100644 proofs/CacheDirectives.bend delete mode 100644 proofs/CacheIgnoresSMaxage.bend create mode 100644 proofs/CacheIgnoresUnknownDirective.bend delete mode 100644 proofs/CacheMaxAgePresent.bend delete mode 100644 proofs/CacheNoCacheAlone.bend delete mode 100644 proofs/CacheNoteExpires.bend delete mode 100644 proofs/CachePrivateAlone.bend create mode 100644 proofs/CacheReadsKnownDirectives.bend create mode 100644 proofs/CacheRecordsExpires.bend create mode 100644 proofs/CacheRecordsMaxAge.bend create mode 100644 proofs/CacheRecordsNoCache.bend create mode 100644 proofs/CacheRecordsPrivate.bend create mode 100644 proofs/ChunkSizeHexIgnoresCase.bend rename proofs/{HexlenIgnoresExtension.bend => ChunkSizeIgnoresExtension.bend} (88%) create mode 100644 proofs/ChunkSizeReadsHexSkipsExtension.bend create mode 100644 proofs/ChunkedBodyJoinsChunks.bend create mode 100644 proofs/ChunkedNameIgnoresCase.bend delete mode 100644 proofs/ChunksEmptyFuel.bend create mode 100644 proofs/ChunksStopWithoutFuel.bend create mode 100644 proofs/ContentLengthReadsDecimal.bend create mode 100644 proofs/ContentLengthRejectsJunk.bend create mode 100644 proofs/ContentLengthTakesPrefix.bend create mode 100644 proofs/ContinuationLineRejected.bend create mode 100644 proofs/CookieAliveUnderMaxAge.bend create mode 100644 proofs/CookieAttributesJoinWithSemicolons.bend delete mode 100644 proofs/CookieDefaultPathDir.bend create mode 100644 proofs/CookieDefaultPathFallsBackToRoot.bend create mode 100644 proofs/CookieDefaultPathIsDirectory.bend create mode 100644 proofs/CookieDefaultPathNeedsTwoSlashes.bend create mode 100644 proofs/CookieDefaultPathOmitsTrailingSlash.bend delete mode 100644 proofs/CookieDefaultPathOneSlash.bend delete mode 100644 proofs/CookieDefaultPathRoot.bend delete mode 100644 proofs/CookieDefaultPathTrailing.bend delete mode 100644 proofs/CookieDomainIpExact.bend delete mode 100644 proofs/CookieDomainIpSuffix.bend create mode 100644 proofs/CookieDomainMatchesDotSuffix.bend create mode 100644 proofs/CookieDomainNeedsDotBeforeSuffix.bend delete mode 100644 proofs/CookieDomainNotSuffix.bend delete mode 100644 proofs/CookieDomainSuffix.bend create mode 100644 proofs/CookieHostOnlyNotSentElsewhere.bend create mode 100644 proofs/CookieHostOnlySentToHost.bend create mode 100644 proofs/CookieHostOnlyStoresHost.bend create mode 100644 proofs/CookieIdenticalPathsMatch.bend delete mode 100644 proofs/CookieInsecureOnTcp.bend create mode 100644 proofs/CookieInsecureSentOnCleartext.bend create mode 100644 proofs/CookieIpHostMatchesExactly.bend create mode 100644 proofs/CookieIpHostNoSuffixMatch.bend delete mode 100644 proofs/CookieIpv4Literal.bend delete mode 100644 proofs/CookieIpv4Name.bend delete mode 100644 proofs/CookieLineAttributes.bend create mode 100644 proofs/CookieLongerPathFails.bend delete mode 100644 proofs/CookieMaxAgeOne.bend create mode 100644 proofs/CookieMaxAgeOneStaleAtOne.bend delete mode 100644 proofs/CookieMaxAgeOpen.bend rename proofs/{CookieAgeSets.bend => CookieMaxAgeSetsAge.bend} (70%) delete mode 100644 proofs/CookieMaxAgeZero.bend create mode 100644 proofs/CookieMaxAgeZeroNotAlive.bend create mode 100644 proofs/CookieNameIsNotIpv4.bend create mode 100644 proofs/CookieNoDomainNotSentElsewhere.bend delete mode 100644 proofs/CookieParseRoundTrip.bend delete mode 100644 proofs/CookiePathIdentical.bend delete mode 100644 proofs/CookiePathLongerCookie.bend create mode 100644 proofs/CookiePathMatchesSlashPrefix.bend create mode 100644 proofs/CookiePathNeedsSegmentBoundary.bend delete mode 100644 proofs/CookiePathNotPrefix.bend delete mode 100644 proofs/CookiePathPrefix.bend delete mode 100644 proofs/CookiePathRoot.bend delete mode 100644 proofs/CookiePathSlashPrefix.bend delete mode 100644 proofs/CookieReceiveHostOnly.bend create mode 100644 proofs/CookieRecognizesIpv4Literal.bend delete mode 100644 proofs/CookieRequestJoins.bend create mode 100644 proofs/CookieRequestJoinsPairs.bend create mode 100644 proofs/CookieRootPathMatchesLonger.bend create mode 100644 proofs/CookieSameSiteFoldsCase.bend create mode 100644 proofs/CookieSameSiteNoneCanonical.bend delete mode 100644 proofs/CookieSamesiteNone.bend delete mode 100644 proofs/CookieSamesiteStrict.bend delete mode 100644 proofs/CookieSamesiteUnknown.bend delete mode 100644 proofs/CookieSecureOnTls.bend create mode 100644 proofs/CookieSecureSentOnTls.bend delete mode 100644 proofs/CookieSendHostOnly.bend delete mode 100644 proofs/CookieSendOtherHost.bend delete mode 100644 proofs/CookieSendUnsetDomain.bend delete mode 100644 proofs/CookieSessionAlive.bend create mode 100644 proofs/CookieSessionStaysAlive.bend create mode 100644 proofs/CookieSlashEndedPathIsPrefix.bend create mode 100644 proofs/CookieUnknownSameSiteDropped.bend create mode 100644 proofs/CorsAllowListReflectsOrigin.bend create mode 100644 proofs/CorsAllowedPreflightIs200.bend rename proofs/{CorsPreflightBad.bend => CorsBadPreflightIs400.bend} (56%) delete mode 100644 proofs/CorsCredentialsExplicitOrigin.bend create mode 100644 proofs/CorsCredentialsKeepExplicitOrigin.bend delete mode 100644 proofs/CorsExplicitOrigin.bend create mode 100644 proofs/CorsNoVaryForStar.bend create mode 100644 proofs/CorsNoVaryWhenRefused.bend delete mode 100644 proofs/CorsOnPreflight.bend create mode 100644 proofs/CorsOptionsWithMethodIsPreflight.bend delete mode 100644 proofs/CorsOriginHeader.bend delete mode 100644 proofs/CorsPreflightAllowed.bend create mode 100644 proofs/CorsPreflightAnswersStar.bend delete mode 100644 proofs/CorsPreflightHeaderCase.bend create mode 100644 proofs/CorsPreflightHeadersIgnoreCase.bend delete mode 100644 proofs/CorsPreflightStar.bend create mode 100644 proofs/CorsReadsOriginHeader.bend create mode 100644 proofs/CorsReadsRequestHeaders.bend create mode 100644 proofs/CorsReadsRequestMethod.bend create mode 100644 proofs/CorsRefusedLeavesReply.bend delete mode 100644 proofs/CorsRefusedOrigin.bend create mode 100644 proofs/CorsRefusesMissingOrigin.bend delete mode 100644 proofs/CorsRequestHeadersHeader.bend delete mode 100644 proofs/CorsRequestMethodHeader.bend create mode 100644 proofs/CorsSimpleAnswersStar.bend create mode 100644 proofs/CorsSimpleKeepsBody.bend delete mode 100644 proofs/CorsSimpleReflects.bend delete mode 100644 proofs/CorsSimpleRefused.bend delete mode 100644 proofs/CorsSimpleStar.bend create mode 100644 proofs/CorsStarStaysStar.bend delete mode 100644 proofs/CorsStarWithoutCredentials.bend delete mode 100644 proofs/CorsVaryEmpty.bend create mode 100644 proofs/CorsVaryOnSpecificOrigin.bend delete mode 100644 proofs/CorsVarySpecific.bend delete mode 100644 proofs/CorsVaryStar.bend delete mode 100644 proofs/DechunkEmpty.bend create mode 100644 proofs/DechunkEmptyIsEmpty.bend create mode 100644 proofs/DechunkJoinsTwoChunks.bend create mode 100644 proofs/DechunkLastChunkAloneIsEmpty.bend delete mode 100644 proofs/DechunkOnlyLast.bend create mode 100644 proofs/DechunkRfcExample.bend delete mode 100644 proofs/DechunkTwoChunks.bend delete mode 100644 proofs/DechunkWiki.bend create mode 100644 proofs/DeleteIsIdempotent.bend create mode 100644 proofs/DeleteNotSafe.bend rename proofs/{DeleteReqNamesPath.bend => DeleteRequestLine.bend} (77%) delete mode 100644 proofs/DfltHttps.bend delete mode 100644 proofs/DigitsReadDecimal.bend delete mode 100644 proofs/DigitsReadRejectsJunk.bend create mode 100644 proofs/EqualContentLengthsAreOne.bend rename proofs/{ClenExact.bend => ExactContentLengthBodyKept.bend} (86%) create mode 100644 proofs/FieldLookupFindsFirst.bend create mode 100644 proofs/FieldLookupIgnoresCase.bend create mode 100644 proofs/FieldNamesIgnoreCase.bend create mode 100644 proofs/FieldValueTrimsWhitespace.bend delete mode 100644 proofs/FreshMaxAgeLimit.bend delete mode 100644 proofs/FreshMaxAgeOne.bend delete mode 100644 proofs/FreshMaxAgeOpen.bend delete mode 100644 proofs/FreshMaxAgeOverridesExpires.bend delete mode 100644 proofs/FreshMaxAgeZero.bend delete mode 100644 proofs/FreshNoCache.bend delete mode 100644 proofs/FreshNoStore.bend delete mode 100644 proofs/FreshPublicOnly.bend create mode 100644 proofs/FreshUnderMaxAge.bend create mode 100644 proofs/GetAsteriskTargetRejected.bend create mode 100644 proofs/GetIsIdempotent.bend create mode 100644 proofs/GetIsSafe.bend delete mode 100644 proofs/GetReplyKeepsBody.bend rename proofs/{GetReqNamesPath.bend => GetRequestLine.bend} (75%) delete mode 100644 proofs/HeadHitIgnoresCase.bend create mode 100644 proofs/HeadIsIdempotent.bend create mode 100644 proofs/HeadIsSafe.bend delete mode 100644 proofs/HeadLineRejectsContinuation.bend delete mode 100644 proofs/HeadLineRejectsObsFold.bend delete mode 100644 proofs/HeadLineTrims.bend rename proofs/{HeadReqNamesPath.bend => HeadRequestLine.bend} (75%) delete mode 100644 proofs/HeadersFindFirst.bend delete mode 100644 proofs/HeadersFindIgnoresCase.bend delete mode 100644 proofs/HeadersFindMissing.bend delete mode 100644 proofs/HexlenAlpha.bend delete mode 100644 proofs/HexlenExtensionAndCase.bend create mode 100644 proofs/HttpsDefaultsTo443.bend rename proofs/{JoinJoined.bend => JoinedPiecesAreText.bend} (93%) delete mode 100644 proofs/JsonParseRejects.bend delete mode 100644 proofs/JsonParseString.bend create mode 100644 proofs/JsonRejectsNonJson.bend create mode 100644 proofs/JsonStringParsesBack.bend create mode 100644 proofs/LowercaseGetNotSafe.bend create mode 100644 proofs/MaxAgeOneFreshThenStale.bend create mode 100644 proofs/MaxAgeOverridesExpires.bend create mode 100644 proofs/MaxAgeZeroIsStale.bend delete mode 100644 proofs/MethodIdempotentDelete.bend delete mode 100644 proofs/MethodIdempotentGet.bend delete mode 100644 proofs/MethodIdempotentHead.bend delete mode 100644 proofs/MethodIdempotentOptions.bend delete mode 100644 proofs/MethodIdempotentPatch.bend delete mode 100644 proofs/MethodIdempotentPost.bend delete mode 100644 proofs/MethodIdempotentPut.bend delete mode 100644 proofs/MethodSafeDelete.bend delete mode 100644 proofs/MethodSafeGet.bend delete mode 100644 proofs/MethodSafeGetLower.bend delete mode 100644 proofs/MethodSafeHead.bend delete mode 100644 proofs/MethodSafeOptions.bend delete mode 100644 proofs/MethodSafePost.bend delete mode 100644 proofs/MethodSafePut.bend create mode 100644 proofs/MissingFieldIsEmpty.bend rename proofs/{FreshNoCacheAny.bend => NoCacheNeverFresh.bend} (73%) create mode 100644 proofs/NoCacheNotFresh.bend create mode 100644 proofs/NoMaxAgeNotFresh.bend rename proofs/{FreshNoStoreAny.bend => NoStoreNeverFresh.bend} (66%) create mode 100644 proofs/NoStoreNotFresh.bend create mode 100644 proofs/NonHeadReplyKeepsBody.bend create mode 100644 proofs/NonHexChunkSizeTorn.bend create mode 100644 proofs/ObsFoldRejected.bend create mode 100644 proofs/OptionsAsteriskNamesServer.bend create mode 100644 proofs/OptionsIsIdempotent.bend create mode 100644 proofs/OptionsIsSafe.bend rename proofs/{OptionsReqNamesPath.bend => OptionsRequestLine.bend} (74%) create mode 100644 proofs/OriginFormKeepsQuery.bend rename proofs/{DfltPlain.bend => OtherSchemesDefaultTo80.bend} (50%) delete mode 100644 proofs/ParseChunkedBody.bend delete mode 100644 proofs/ParseChunkedIgnoresCase.bend delete mode 100644 proofs/ParseClenTakesPrefix.bend delete mode 100644 proofs/ParseEqualContentLengths.bend delete mode 100644 proofs/ParseRejectsClAndTe.bend delete mode 100644 proofs/ParseRejectsDisagreeingCl.bend delete mode 100644 proofs/ParseRejectsNonHexChunk.bend create mode 100644 proofs/PatchNotIdempotent.bend create mode 100644 proofs/PostNotIdempotent.bend create mode 100644 proofs/PostNotSafe.bend rename proofs/{PostReqNamesPath.bend => PostRequestLine.bend} (78%) create mode 100644 proofs/PutIsIdempotent.bend create mode 100644 proofs/PutNotSafe.bend rename proofs/{PutReqNamesPath.bend => PutRequestLine.bend} (78%) delete mode 100644 proofs/Reason200.bend create mode 100644 proofs/Reason200IsOk.bend delete mode 100644 proofs/Reason201.bend create mode 100644 proofs/Reason201IsCreated.bend delete mode 100644 proofs/Reason204.bend create mode 100644 proofs/Reason204IsNoContent.bend delete mode 100644 proofs/Reason400.bend create mode 100644 proofs/Reason400IsBadRequest.bend delete mode 100644 proofs/Reason404.bend create mode 100644 proofs/Reason404IsNotFound.bend delete mode 100644 proofs/Reason405.bend create mode 100644 proofs/Reason405IsMethodNotAllowed.bend delete mode 100644 proofs/Reason500.bend create mode 100644 proofs/Reason500IsInternalServerError.bend delete mode 100644 proofs/ReasonUnknownEmpty.bend create mode 100644 proofs/RelativeTargetRejected.bend rename proofs/{AskClenExact.bend => RequestBodyOfContentLengthKept.bend} (86%) create mode 100644 proofs/RequestDisagreeingContentLengthsRejected.bend create mode 100644 proofs/RequestEqualContentLengthsFrameOneBody.bend create mode 100644 proofs/RequestKeepsFieldNameSpelling.bend rename proofs/{RequestNamesMethod.bend => RequestLineOpensWithMethod.bend} (74%) rename proofs/{AskNeedsBlankLine.bend => RequestNeedsBlankLine.bend} (67%) create mode 100644 proofs/RequestNeedsRequestLine.bend create mode 100644 proofs/RequestWithBothFramingsRejected.bend create mode 100644 proofs/ResponseDisagreeingContentLengthsTorn.bend rename proofs/{ParseNeedsBlankLine.bend => ResponseNeedsBlankLine.bend} (67%) rename proofs/{ParseNeedsStatus.bend => ResponseNeedsStatus.bend} (69%) create mode 100644 proofs/ResponseWithBothFramingsTorn.bend rename proofs/{ParseSchemeFirst.bend => SchemeBeforeFirstColon.bend} (67%) create mode 100644 proofs/SetCookieParsesBack.bend rename proofs/{AskClenShort.bend => ShortRequestBodyIsBad.bend} (72%) create mode 100644 proofs/StaleAtMaxAge.bend rename proofs/{Utf8Split.bend => TakeDropSplitAtOneOffset.bend} (90%) delete mode 100644 proofs/TokenOkNewline.bend delete mode 100644 proofs/TokenOkPlain.bend delete mode 100644 proofs/TokenOkSpace.bend delete mode 100644 proofs/TokenOkTab.bend create mode 100644 proofs/UnframedBodyIsRest.bend delete mode 100644 proofs/UnframedIsRest.bend create mode 100644 proofs/UnknownStatusHasEmptyReason.bend rename proofs/{ParseNeedsSlashes.bend => UrlNeedsSlashes.bend} (70%) rename proofs/{PathAbsolute.bend => UrlPathIsAbsolute.bend} (67%) diff --git a/LAWS.bend b/LAWS.bend index 894e47c..5db5a00 100644 --- a/LAWS.bend +++ b/LAWS.bend @@ -26,216 +26,203 @@ # 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 named after its one law, with no prose -# (laws/PathAbsolute.bend, laws/CacheDirectives.bend), and promises nothing -# beyond the equality it states. +# 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 ./laws/AbsoluteFormTargetRejected.bend as AbsoluteFormTargetRejected import ./laws/AbsoluteLocationsResolve.bend as AbsoluteLocationsResolve -import ./laws/AskClenExact.bend as AskClenExact -import ./laws/AskClenShort.bend as AskClenShort -import ./laws/AskEqualContentLengths.bend as AskEqualContentLengths -import ./laws/AskGetAsteriskRejected.bend as AskGetAsteriskRejected -import ./laws/AskKeepsFieldNameSpelling.bend as AskKeepsFieldNameSpelling -import ./laws/AskNeedsBlankLine.bend as AskNeedsBlankLine -import ./laws/AskNeedsRequestLine.bend as AskNeedsRequestLine -import ./laws/AskOptionsAsterisk.bend as AskOptionsAsterisk -import ./laws/AskOriginFormKeepsQuery.bend as AskOriginFormKeepsQuery -import ./laws/AskRejectsAbsoluteForm.bend as AskRejectsAbsoluteForm -import ./laws/AskRejectsClAndTe.bend as AskRejectsClAndTe -import ./laws/AskRejectsDisagreeingCl.bend as AskRejectsDisagreeingCl -import ./laws/AskRejectsRelativeTarget.bend as AskRejectsRelativeTarget -import ./laws/AskShowBadIsMarked.bend as AskShowBadIsMarked -import ./laws/B64Empty.bend as B64Empty -import ./laws/B64F.bend as B64F -import ./laws/B64Fo.bend as B64Fo -import ./laws/B64Foo.bend as B64Foo -import ./laws/B64GoEnd.bend as B64GoEnd -import ./laws/B64UserPass.bend as B64UserPass import ./laws/BadRequestAnswers400.bend as BadRequestAnswers400 +import ./laws/BadRequestShowIsMarked.bend as BadRequestShowIsMarked import ./laws/BareRepliesWriteHeadOnly.bend as BareRepliesWriteHeadOnly -import ./laws/BasicUserPass.bend as BasicUserPass +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/CacheDirectiveCase.bend as CacheDirectiveCase -import ./laws/CacheDirectives.bend as CacheDirectives -import ./laws/CacheIgnoresSMaxage.bend as CacheIgnoresSMaxage +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/CacheMaxAgePresent.bend as CacheMaxAgePresent -import ./laws/CacheNoCacheAlone.bend as CacheNoCacheAlone -import ./laws/CacheNoteExpires.bend as CacheNoteExpires -import ./laws/CachePrivateAlone.bend as CachePrivateAlone +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/ChunksEmptyFuel.bend as ChunksEmptyFuel -import ./laws/ClenExact.bend as ClenExact +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/CookieAgeSets.bend as CookieAgeSets -import ./laws/CookieDefaultPathDir.bend as CookieDefaultPathDir -import ./laws/CookieDefaultPathOneSlash.bend as CookieDefaultPathOneSlash -import ./laws/CookieDefaultPathRoot.bend as CookieDefaultPathRoot -import ./laws/CookieDefaultPathTrailing.bend as CookieDefaultPathTrailing -import ./laws/CookieDomainIpExact.bend as CookieDomainIpExact -import ./laws/CookieDomainIpSuffix.bend as CookieDomainIpSuffix +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/CookieDomainNotSuffix.bend as CookieDomainNotSuffix -import ./laws/CookieDomainSuffix.bend as CookieDomainSuffix +import ./laws/CookieDomainMatchesDotSuffix.bend as CookieDomainMatchesDotSuffix +import ./laws/CookieDomainNeedsDotBeforeSuffix.bend as CookieDomainNeedsDotBeforeSuffix import ./laws/CookieHeaderEchoesStore.bend as CookieHeaderEchoesStore -import ./laws/CookieInsecureOnTcp.bend as CookieInsecureOnTcp -import ./laws/CookieIpv4Literal.bend as CookieIpv4Literal -import ./laws/CookieIpv4Name.bend as CookieIpv4Name -import ./laws/CookieLineAttributes.bend as CookieLineAttributes +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/CookieMaxAgeOne.bend as CookieMaxAgeOne -import ./laws/CookieMaxAgeOpen.bend as CookieMaxAgeOpen -import ./laws/CookieMaxAgeZero.bend as CookieMaxAgeZero -import ./laws/CookieParseRoundTrip.bend as CookieParseRoundTrip -import ./laws/CookiePathIdentical.bend as CookiePathIdentical -import ./laws/CookiePathLongerCookie.bend as CookiePathLongerCookie -import ./laws/CookiePathNotPrefix.bend as CookiePathNotPrefix -import ./laws/CookiePathPrefix.bend as CookiePathPrefix -import ./laws/CookiePathRoot.bend as CookiePathRoot -import ./laws/CookiePathSlashPrefix.bend as CookiePathSlashPrefix -import ./laws/CookieReceiveHostOnly.bend as CookieReceiveHostOnly +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/CookieRequestJoins.bend as CookieRequestJoins -import ./laws/CookieSamesiteNone.bend as CookieSamesiteNone -import ./laws/CookieSamesiteStrict.bend as CookieSamesiteStrict -import ./laws/CookieSamesiteUnknown.bend as CookieSamesiteUnknown +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/CookieSecureOnTls.bend as CookieSecureOnTls -import ./laws/CookieSendHostOnly.bend as CookieSendHostOnly -import ./laws/CookieSendOtherHost.bend as CookieSendOtherHost -import ./laws/CookieSendUnsetDomain.bend as CookieSendUnsetDomain -import ./laws/CookieSessionAlive.bend as CookieSessionAlive -import ./laws/CorsCredentialsExplicitOrigin.bend as CorsCredentialsExplicitOrigin +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/CorsExplicitOrigin.bend as CorsExplicitOrigin import ./laws/CorsGetIsNotPreflight.bend as CorsGetIsNotPreflight -import ./laws/CorsOnPreflight.bend as CorsOnPreflight -import ./laws/CorsOriginHeader.bend as CorsOriginHeader -import ./laws/CorsPreflightAllowed.bend as CorsPreflightAllowed -import ./laws/CorsPreflightBad.bend as CorsPreflightBad -import ./laws/CorsPreflightHeaderCase.bend as CorsPreflightHeaderCase +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/CorsPreflightStar.bend as CorsPreflightStar -import ./laws/CorsRefusedOrigin.bend as CorsRefusedOrigin -import ./laws/CorsRequestHeadersHeader.bend as CorsRequestHeadersHeader -import ./laws/CorsRequestMethodHeader.bend as CorsRequestMethodHeader -import ./laws/CorsSimpleReflects.bend as CorsSimpleReflects -import ./laws/CorsSimpleRefused.bend as CorsSimpleRefused -import ./laws/CorsSimpleStar.bend as CorsSimpleStar -import ./laws/CorsStarWithoutCredentials.bend as CorsStarWithoutCredentials -import ./laws/CorsVaryEmpty.bend as CorsVaryEmpty -import ./laws/CorsVarySpecific.bend as CorsVarySpecific -import ./laws/CorsVaryStar.bend as CorsVaryStar +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/DechunkEmpty.bend as DechunkEmpty +import ./laws/DechunkEmptyIsEmpty.bend as DechunkEmptyIsEmpty import ./laws/DechunkIgnoresExtension.bend as DechunkIgnoresExtension -import ./laws/DechunkOnlyLast.bend as DechunkOnlyLast +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/DechunkTwoChunks.bend as DechunkTwoChunks -import ./laws/DechunkWiki.bend as DechunkWiki -import ./laws/DeleteReqNamesPath.bend as DeleteReqNamesPath -import ./laws/DfltHttps.bend as DfltHttps -import ./laws/DfltPlain.bend as DfltPlain -import ./laws/DigitsReadDecimal.bend as DigitsReadDecimal -import ./laws/DigitsReadRejectsJunk.bend as DigitsReadRejectsJunk +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/FreshMaxAgeLimit.bend as FreshMaxAgeLimit -import ./laws/FreshMaxAgeOne.bend as FreshMaxAgeOne -import ./laws/FreshMaxAgeOpen.bend as FreshMaxAgeOpen -import ./laws/FreshMaxAgeOverridesExpires.bend as FreshMaxAgeOverridesExpires -import ./laws/FreshMaxAgeZero.bend as FreshMaxAgeZero -import ./laws/FreshNoCache.bend as FreshNoCache -import ./laws/FreshNoCacheAny.bend as FreshNoCacheAny -import ./laws/FreshNoStore.bend as FreshNoStore -import ./laws/FreshNoStoreAny.bend as FreshNoStoreAny -import ./laws/FreshPublicOnly.bend as FreshPublicOnly +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/GetReplyKeepsBody.bend as GetReplyKeepsBody -import ./laws/GetReqNamesPath.bend as GetReqNamesPath +import ./laws/GetRequestLine.bend as GetRequestLine import ./laws/HandlerGetsRequestText.bend as HandlerGetsRequestText import ./laws/HeadAndNoContentHaveNoBody.bend as HeadAndNoContentHaveNoBody -import ./laws/HeadHitIgnoresCase.bend as HeadHitIgnoresCase -import ./laws/HeadLineRejectsContinuation.bend as HeadLineRejectsContinuation -import ./laws/HeadLineRejectsObsFold.bend as HeadLineRejectsObsFold -import ./laws/HeadLineTrims.bend as HeadLineTrims +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/HeadReqNamesPath.bend as HeadReqNamesPath -import ./laws/HeadersFindFirst.bend as HeadersFindFirst -import ./laws/HeadersFindIgnoresCase.bend as HeadersFindIgnoresCase -import ./laws/HeadersFindMissing.bend as HeadersFindMissing -import ./laws/HexlenAlpha.bend as HexlenAlpha -import ./laws/HexlenExtensionAndCase.bend as HexlenExtensionAndCase -import ./laws/HexlenIgnoresExtension.bend as HexlenIgnoresExtension +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/JoinJoined.bend as JoinJoined -import ./laws/JsonParseRejects.bend as JsonParseRejects -import ./laws/JsonParseString.bend as JsonParseString +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/MethodIdempotentDelete.bend as MethodIdempotentDelete -import ./laws/MethodIdempotentGet.bend as MethodIdempotentGet -import ./laws/MethodIdempotentHead.bend as MethodIdempotentHead -import ./laws/MethodIdempotentOptions.bend as MethodIdempotentOptions -import ./laws/MethodIdempotentPatch.bend as MethodIdempotentPatch -import ./laws/MethodIdempotentPost.bend as MethodIdempotentPost -import ./laws/MethodIdempotentPut.bend as MethodIdempotentPut -import ./laws/MethodSafeDelete.bend as MethodSafeDelete -import ./laws/MethodSafeGet.bend as MethodSafeGet -import ./laws/MethodSafeGetLower.bend as MethodSafeGetLower -import ./laws/MethodSafeHead.bend as MethodSafeHead -import ./laws/MethodSafeOptions.bend as MethodSafeOptions -import ./laws/MethodSafePost.bend as MethodSafePost -import ./laws/MethodSafePut.bend as MethodSafePut +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/OptionsReqNamesPath.bend as OptionsReqNamesPath +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/ParseChunkedBody.bend as ParseChunkedBody -import ./laws/ParseChunkedIgnoresCase.bend as ParseChunkedIgnoresCase -import ./laws/ParseClenTakesPrefix.bend as ParseClenTakesPrefix -import ./laws/ParseEqualContentLengths.bend as ParseEqualContentLengths -import ./laws/ParseNeedsBlankLine.bend as ParseNeedsBlankLine -import ./laws/ParseNeedsSlashes.bend as ParseNeedsSlashes -import ./laws/ParseNeedsStatus.bend as ParseNeedsStatus import ./laws/ParseQuerySplitsPairs.bend as ParseQuerySplitsPairs -import ./laws/ParseRejectsClAndTe.bend as ParseRejectsClAndTe -import ./laws/ParseRejectsDisagreeingCl.bend as ParseRejectsDisagreeingCl -import ./laws/ParseRejectsNonHexChunk.bend as ParseRejectsNonHexChunk -import ./laws/ParseSchemeFirst.bend as ParseSchemeFirst -import ./laws/PathAbsolute.bend as PathAbsolute +import ./laws/PatchNotIdempotent.bend as PatchNotIdempotent import ./laws/PathFormKeepsSlash.bend as PathFormKeepsSlash import ./laws/PhraseTableLookup.bend as PhraseTableLookup -import ./laws/PostReqNamesPath.bend as PostReqNamesPath +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/PutReqNamesPath.bend as PutReqNamesPath +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 @@ -243,43 +230,56 @@ 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/Reason200.bend as Reason200 -import ./laws/Reason201.bend as Reason201 -import ./laws/Reason204.bend as Reason204 -import ./laws/Reason400.bend as Reason400 -import ./laws/Reason404.bend as Reason404 -import ./laws/Reason405.bend as Reason405 -import ./laws/Reason500.bend as Reason500 +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/ReasonUnknownEmpty.bend as ReasonUnknownEmpty 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/RequestNamesMethod.bend as RequestNamesMethod +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/TokenOkNewline.bend as TokenOkNewline -import ./laws/TokenOkPlain.bend as TokenOkPlain -import ./laws/TokenOkSpace.bend as TokenOkSpace -import ./laws/TokenOkTab.bend as TokenOkTab import ./laws/UnescapeFindsReferences.bend as UnescapeFindsReferences import ./laws/UnescapeUndoesEscape.bend as UnescapeUndoesEscape -import ./laws/UnframedIsRest.bend as UnframedIsRest +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 @@ -290,6 +290,8 @@ 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 @@ -301,5 +303,4 @@ 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/Utf8Split.bend as Utf8Split import ./laws/WithServesGetOctetsAndContext.bend as WithServesGetOctetsAndContext diff --git a/PROOF.bend b/PROOF.bend index af0b32d..89878e6 100644 --- a/PROOF.bend +++ b/PROOF.bend @@ -2,209 +2,195 @@ # `bend PROOF.bend` prints exactly `ALL PROOFS CHECK` as its first line. import Base import ./LAWS.bend as Laws +import ./proofs/AbsoluteFormTargetRejected.bend as PAbsoluteFormTargetRejected import ./proofs/AbsoluteLocationsResolve.bend as PAbsoluteLocationsResolve -import ./proofs/AskClenExact.bend as PAskClenExact -import ./proofs/AskClenShort.bend as PAskClenShort -import ./proofs/AskEqualContentLengths.bend as PAskEqualContentLengths -import ./proofs/AskGetAsteriskRejected.bend as PAskGetAsteriskRejected -import ./proofs/AskKeepsFieldNameSpelling.bend as PAskKeepsFieldNameSpelling -import ./proofs/AskNeedsBlankLine.bend as PAskNeedsBlankLine -import ./proofs/AskNeedsRequestLine.bend as PAskNeedsRequestLine -import ./proofs/AskOptionsAsterisk.bend as PAskOptionsAsterisk -import ./proofs/AskOriginFormKeepsQuery.bend as PAskOriginFormKeepsQuery -import ./proofs/AskRejectsAbsoluteForm.bend as PAskRejectsAbsoluteForm -import ./proofs/AskRejectsClAndTe.bend as PAskRejectsClAndTe -import ./proofs/AskRejectsDisagreeingCl.bend as PAskRejectsDisagreeingCl -import ./proofs/AskRejectsRelativeTarget.bend as PAskRejectsRelativeTarget -import ./proofs/AskShowBadIsMarked.bend as PAskShowBadIsMarked -import ./proofs/B64Empty.bend as PB64Empty -import ./proofs/B64F.bend as PB64F -import ./proofs/B64Fo.bend as PB64Fo -import ./proofs/B64Foo.bend as PB64Foo -import ./proofs/B64GoEnd.bend as PB64GoEnd -import ./proofs/B64UserPass.bend as PB64UserPass import ./proofs/BadRequestAnswers400.bend as PBadRequestAnswers400 +import ./proofs/BadRequestShowIsMarked.bend as PBadRequestShowIsMarked import ./proofs/BareRepliesWriteHeadOnly.bend as PBareRepliesWriteHeadOnly -import ./proofs/BasicUserPass.bend as PBasicUserPass +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/CacheDirectiveCase.bend as PCacheDirectiveCase -import ./proofs/CacheDirectives.bend as PCacheDirectives -import ./proofs/CacheIgnoresSMaxage.bend as PCacheIgnoresSMaxage +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/CacheMaxAgePresent.bend as PCacheMaxAgePresent -import ./proofs/CacheNoCacheAlone.bend as PCacheNoCacheAlone -import ./proofs/CacheNoteExpires.bend as PCacheNoteExpires -import ./proofs/CachePrivateAlone.bend as PCachePrivateAlone +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/ChunksEmptyFuel.bend as PChunksEmptyFuel -import ./proofs/ClenExact.bend as PClenExact +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/CookieAgeSets.bend as PCookieAgeSets -import ./proofs/CookieDefaultPathDir.bend as PCookieDefaultPathDir -import ./proofs/CookieDefaultPathOneSlash.bend as PCookieDefaultPathOneSlash -import ./proofs/CookieDefaultPathRoot.bend as PCookieDefaultPathRoot -import ./proofs/CookieDefaultPathTrailing.bend as PCookieDefaultPathTrailing -import ./proofs/CookieDomainIpExact.bend as PCookieDomainIpExact -import ./proofs/CookieDomainIpSuffix.bend as PCookieDomainIpSuffix +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/CookieDomainNotSuffix.bend as PCookieDomainNotSuffix -import ./proofs/CookieDomainSuffix.bend as PCookieDomainSuffix +import ./proofs/CookieDomainMatchesDotSuffix.bend as PCookieDomainMatchesDotSuffix +import ./proofs/CookieDomainNeedsDotBeforeSuffix.bend as PCookieDomainNeedsDotBeforeSuffix import ./proofs/CookieHeaderEchoesStore.bend as PCookieHeaderEchoesStore -import ./proofs/CookieInsecureOnTcp.bend as PCookieInsecureOnTcp -import ./proofs/CookieIpv4Literal.bend as PCookieIpv4Literal -import ./proofs/CookieIpv4Name.bend as PCookieIpv4Name -import ./proofs/CookieLineAttributes.bend as PCookieLineAttributes +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/CookieMaxAgeOne.bend as PCookieMaxAgeOne -import ./proofs/CookieMaxAgeOpen.bend as PCookieMaxAgeOpen -import ./proofs/CookieMaxAgeZero.bend as PCookieMaxAgeZero -import ./proofs/CookieParseRoundTrip.bend as PCookieParseRoundTrip -import ./proofs/CookiePathIdentical.bend as PCookiePathIdentical -import ./proofs/CookiePathLongerCookie.bend as PCookiePathLongerCookie -import ./proofs/CookiePathNotPrefix.bend as PCookiePathNotPrefix -import ./proofs/CookiePathPrefix.bend as PCookiePathPrefix -import ./proofs/CookiePathRoot.bend as PCookiePathRoot -import ./proofs/CookiePathSlashPrefix.bend as PCookiePathSlashPrefix -import ./proofs/CookieReceiveHostOnly.bend as PCookieReceiveHostOnly +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/CookieRequestJoins.bend as PCookieRequestJoins -import ./proofs/CookieSamesiteNone.bend as PCookieSamesiteNone -import ./proofs/CookieSamesiteStrict.bend as PCookieSamesiteStrict -import ./proofs/CookieSamesiteUnknown.bend as PCookieSamesiteUnknown +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/CookieSecureOnTls.bend as PCookieSecureOnTls -import ./proofs/CookieSendHostOnly.bend as PCookieSendHostOnly -import ./proofs/CookieSendOtherHost.bend as PCookieSendOtherHost -import ./proofs/CookieSendUnsetDomain.bend as PCookieSendUnsetDomain -import ./proofs/CookieSessionAlive.bend as PCookieSessionAlive -import ./proofs/CorsCredentialsExplicitOrigin.bend as PCorsCredentialsExplicitOrigin +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/CorsExplicitOrigin.bend as PCorsExplicitOrigin import ./proofs/CorsGetIsNotPreflight.bend as PCorsGetIsNotPreflight -import ./proofs/CorsOnPreflight.bend as PCorsOnPreflight -import ./proofs/CorsOriginHeader.bend as PCorsOriginHeader -import ./proofs/CorsPreflightAllowed.bend as PCorsPreflightAllowed -import ./proofs/CorsPreflightBad.bend as PCorsPreflightBad -import ./proofs/CorsPreflightHeaderCase.bend as PCorsPreflightHeaderCase +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/CorsPreflightStar.bend as PCorsPreflightStar -import ./proofs/CorsRefusedOrigin.bend as PCorsRefusedOrigin -import ./proofs/CorsRequestHeadersHeader.bend as PCorsRequestHeadersHeader -import ./proofs/CorsRequestMethodHeader.bend as PCorsRequestMethodHeader -import ./proofs/CorsSimpleReflects.bend as PCorsSimpleReflects -import ./proofs/CorsSimpleRefused.bend as PCorsSimpleRefused -import ./proofs/CorsSimpleStar.bend as PCorsSimpleStar -import ./proofs/CorsStarWithoutCredentials.bend as PCorsStarWithoutCredentials -import ./proofs/CorsVaryEmpty.bend as PCorsVaryEmpty -import ./proofs/CorsVarySpecific.bend as PCorsVarySpecific -import ./proofs/CorsVaryStar.bend as PCorsVaryStar +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/DechunkEmpty.bend as PDechunkEmpty +import ./proofs/DechunkEmptyIsEmpty.bend as PDechunkEmptyIsEmpty import ./proofs/DechunkIgnoresExtension.bend as PDechunkIgnoresExtension -import ./proofs/DechunkOnlyLast.bend as PDechunkOnlyLast +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/DechunkTwoChunks.bend as PDechunkTwoChunks -import ./proofs/DechunkWiki.bend as PDechunkWiki -import ./proofs/DeleteReqNamesPath.bend as PDeleteReqNamesPath -import ./proofs/DfltHttps.bend as PDfltHttps -import ./proofs/DfltPlain.bend as PDfltPlain -import ./proofs/DigitsReadDecimal.bend as PDigitsReadDecimal -import ./proofs/DigitsReadRejectsJunk.bend as PDigitsReadRejectsJunk +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/FreshMaxAgeLimit.bend as PFreshMaxAgeLimit -import ./proofs/FreshMaxAgeOne.bend as PFreshMaxAgeOne -import ./proofs/FreshMaxAgeOpen.bend as PFreshMaxAgeOpen -import ./proofs/FreshMaxAgeOverridesExpires.bend as PFreshMaxAgeOverridesExpires -import ./proofs/FreshMaxAgeZero.bend as PFreshMaxAgeZero -import ./proofs/FreshNoCache.bend as PFreshNoCache -import ./proofs/FreshNoCacheAny.bend as PFreshNoCacheAny -import ./proofs/FreshNoStore.bend as PFreshNoStore -import ./proofs/FreshNoStoreAny.bend as PFreshNoStoreAny -import ./proofs/FreshPublicOnly.bend as PFreshPublicOnly +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/GetReplyKeepsBody.bend as PGetReplyKeepsBody -import ./proofs/GetReqNamesPath.bend as PGetReqNamesPath +import ./proofs/GetRequestLine.bend as PGetRequestLine import ./proofs/HandlerGetsRequestText.bend as PHandlerGetsRequestText import ./proofs/HeadAndNoContentHaveNoBody.bend as PHeadAndNoContentHaveNoBody -import ./proofs/HeadHitIgnoresCase.bend as PHeadHitIgnoresCase -import ./proofs/HeadLineRejectsContinuation.bend as PHeadLineRejectsContinuation -import ./proofs/HeadLineRejectsObsFold.bend as PHeadLineRejectsObsFold -import ./proofs/HeadLineTrims.bend as PHeadLineTrims +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/HeadReqNamesPath.bend as PHeadReqNamesPath -import ./proofs/HeadersFindFirst.bend as PHeadersFindFirst -import ./proofs/HeadersFindIgnoresCase.bend as PHeadersFindIgnoresCase -import ./proofs/HeadersFindMissing.bend as PHeadersFindMissing -import ./proofs/HexlenAlpha.bend as PHexlenAlpha -import ./proofs/HexlenExtensionAndCase.bend as PHexlenExtensionAndCase -import ./proofs/HexlenIgnoresExtension.bend as PHexlenIgnoresExtension +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/JoinJoined.bend as PJoinJoined -import ./proofs/JsonParseRejects.bend as PJsonParseRejects -import ./proofs/JsonParseString.bend as PJsonParseString +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/MethodIdempotentDelete.bend as PMethodIdempotentDelete -import ./proofs/MethodIdempotentGet.bend as PMethodIdempotentGet -import ./proofs/MethodIdempotentHead.bend as PMethodIdempotentHead -import ./proofs/MethodIdempotentOptions.bend as PMethodIdempotentOptions -import ./proofs/MethodIdempotentPatch.bend as PMethodIdempotentPatch -import ./proofs/MethodIdempotentPost.bend as PMethodIdempotentPost -import ./proofs/MethodIdempotentPut.bend as PMethodIdempotentPut -import ./proofs/MethodSafeDelete.bend as PMethodSafeDelete -import ./proofs/MethodSafeGet.bend as PMethodSafeGet -import ./proofs/MethodSafeGetLower.bend as PMethodSafeGetLower -import ./proofs/MethodSafeHead.bend as PMethodSafeHead -import ./proofs/MethodSafeOptions.bend as PMethodSafeOptions -import ./proofs/MethodSafePost.bend as PMethodSafePost -import ./proofs/MethodSafePut.bend as PMethodSafePut +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/OptionsReqNamesPath.bend as POptionsReqNamesPath +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/ParseChunkedBody.bend as PParseChunkedBody -import ./proofs/ParseChunkedIgnoresCase.bend as PParseChunkedIgnoresCase -import ./proofs/ParseClenTakesPrefix.bend as PParseClenTakesPrefix -import ./proofs/ParseEqualContentLengths.bend as PParseEqualContentLengths -import ./proofs/ParseNeedsBlankLine.bend as PParseNeedsBlankLine -import ./proofs/ParseNeedsSlashes.bend as PParseNeedsSlashes -import ./proofs/ParseNeedsStatus.bend as PParseNeedsStatus import ./proofs/ParseQuerySplitsPairs.bend as PParseQuerySplitsPairs -import ./proofs/ParseRejectsClAndTe.bend as PParseRejectsClAndTe -import ./proofs/ParseRejectsDisagreeingCl.bend as PParseRejectsDisagreeingCl -import ./proofs/ParseRejectsNonHexChunk.bend as PParseRejectsNonHexChunk -import ./proofs/ParseSchemeFirst.bend as PParseSchemeFirst -import ./proofs/PathAbsolute.bend as PPathAbsolute +import ./proofs/PatchNotIdempotent.bend as PPatchNotIdempotent import ./proofs/PathFormKeepsSlash.bend as PPathFormKeepsSlash import ./proofs/PhraseTableLookup.bend as PPhraseTableLookup -import ./proofs/PostReqNamesPath.bend as PPostReqNamesPath +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/PutReqNamesPath.bend as PPutReqNamesPath +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 @@ -212,43 +198,56 @@ 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/Reason200.bend as PReason200 -import ./proofs/Reason201.bend as PReason201 -import ./proofs/Reason204.bend as PReason204 -import ./proofs/Reason400.bend as PReason400 -import ./proofs/Reason404.bend as PReason404 -import ./proofs/Reason405.bend as PReason405 -import ./proofs/Reason500.bend as PReason500 +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/ReasonUnknownEmpty.bend as PReasonUnknownEmpty 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/RequestNamesMethod.bend as PRequestNamesMethod +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/TokenOkNewline.bend as PTokenOkNewline -import ./proofs/TokenOkPlain.bend as PTokenOkPlain -import ./proofs/TokenOkSpace.bend as PTokenOkSpace -import ./proofs/TokenOkTab.bend as PTokenOkTab import ./proofs/UnescapeFindsReferences.bend as PUnescapeFindsReferences import ./proofs/UnescapeUndoesEscape.bend as PUnescapeUndoesEscape -import ./proofs/UnframedIsRest.bend as PUnframedIsRest +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 @@ -259,6 +258,8 @@ 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 @@ -270,5 +271,4 @@ 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/Utf8Split.bend as PUtf8Split import ./proofs/WithServesGetOctetsAndContext.bend as PWithServesGetOctetsAndContext diff --git a/laws/AskRejectsAbsoluteForm.bend b/laws/AbsoluteFormTargetRejected.bend similarity index 100% rename from laws/AskRejectsAbsoluteForm.bend rename to laws/AbsoluteFormTargetRejected.bend diff --git a/laws/AskShowBadIsMarked.bend b/laws/BadRequestShowIsMarked.bend similarity index 100% rename from laws/AskShowBadIsMarked.bend rename to laws/BadRequestShowIsMarked.bend diff --git a/laws/B64Empty.bend b/laws/Base64EmptyIsEmpty.bend similarity index 100% rename from laws/B64Empty.bend rename to laws/Base64EmptyIsEmpty.bend diff --git a/laws/B64F.bend b/laws/Base64EncodesF.bend similarity index 100% rename from laws/B64F.bend rename to laws/Base64EncodesF.bend diff --git a/laws/B64Fo.bend b/laws/Base64EncodesFo.bend similarity index 100% rename from laws/B64Fo.bend rename to laws/Base64EncodesFo.bend diff --git a/laws/B64Foo.bend b/laws/Base64EncodesFoo.bend similarity index 100% rename from laws/B64Foo.bend rename to laws/Base64EncodesFoo.bend diff --git a/laws/B64UserPass.bend b/laws/Base64EncodesUserPass.bend similarity index 100% rename from laws/B64UserPass.bend rename to laws/Base64EncodesUserPass.bend diff --git a/laws/B64GoEnd.bend b/laws/Base64PadsFinalQuantum.bend similarity index 100% rename from laws/B64GoEnd.bend rename to laws/Base64PadsFinalQuantum.bend diff --git a/laws/BasicUserPass.bend b/laws/BasicIsBase64OfUserPassword.bend similarity index 100% rename from laws/BasicUserPass.bend rename to laws/BasicIsBase64OfUserPassword.bend diff --git a/laws/TokenOkPlain.bend b/laws/BearerTokenAcceptsPlain.bend similarity index 100% rename from laws/TokenOkPlain.bend rename to laws/BearerTokenAcceptsPlain.bend diff --git a/laws/TokenOkNewline.bend b/laws/BearerTokenRejectsNewline.bend similarity index 100% rename from laws/TokenOkNewline.bend rename to laws/BearerTokenRejectsNewline.bend diff --git a/laws/TokenOkSpace.bend b/laws/BearerTokenRejectsSpace.bend similarity index 100% rename from laws/TokenOkSpace.bend rename to laws/BearerTokenRejectsSpace.bend diff --git a/laws/TokenOkTab.bend b/laws/BearerTokenRejectsTab.bend similarity index 100% rename from laws/TokenOkTab.bend rename to laws/BearerTokenRejectsTab.bend diff --git a/laws/CacheDirectiveCase.bend b/laws/CacheDirectiveNamesIgnoreCase.bend similarity index 100% rename from laws/CacheDirectiveCase.bend rename to laws/CacheDirectiveNamesIgnoreCase.bend diff --git a/laws/CacheIgnoresSMaxage.bend b/laws/CacheIgnoresUnknownDirective.bend similarity index 100% rename from laws/CacheIgnoresSMaxage.bend rename to laws/CacheIgnoresUnknownDirective.bend diff --git a/laws/CacheDirectives.bend b/laws/CacheReadsKnownDirectives.bend similarity index 100% rename from laws/CacheDirectives.bend rename to laws/CacheReadsKnownDirectives.bend diff --git a/laws/CacheNoteExpires.bend b/laws/CacheRecordsExpires.bend similarity index 100% rename from laws/CacheNoteExpires.bend rename to laws/CacheRecordsExpires.bend diff --git a/laws/CacheMaxAgePresent.bend b/laws/CacheRecordsMaxAge.bend similarity index 100% rename from laws/CacheMaxAgePresent.bend rename to laws/CacheRecordsMaxAge.bend diff --git a/laws/CacheNoCacheAlone.bend b/laws/CacheRecordsNoCache.bend similarity index 100% rename from laws/CacheNoCacheAlone.bend rename to laws/CacheRecordsNoCache.bend diff --git a/laws/CachePrivateAlone.bend b/laws/CacheRecordsPrivate.bend similarity index 100% rename from laws/CachePrivateAlone.bend rename to laws/CacheRecordsPrivate.bend diff --git a/laws/HexlenAlpha.bend b/laws/ChunkSizeHexIgnoresCase.bend similarity index 100% rename from laws/HexlenAlpha.bend rename to laws/ChunkSizeHexIgnoresCase.bend diff --git a/laws/HexlenIgnoresExtension.bend b/laws/ChunkSizeIgnoresExtension.bend similarity index 100% rename from laws/HexlenIgnoresExtension.bend rename to laws/ChunkSizeIgnoresExtension.bend diff --git a/laws/HexlenExtensionAndCase.bend b/laws/ChunkSizeReadsHexSkipsExtension.bend similarity index 100% rename from laws/HexlenExtensionAndCase.bend rename to laws/ChunkSizeReadsHexSkipsExtension.bend diff --git a/laws/ParseChunkedBody.bend b/laws/ChunkedBodyJoinsChunks.bend similarity index 100% rename from laws/ParseChunkedBody.bend rename to laws/ChunkedBodyJoinsChunks.bend diff --git a/laws/ParseChunkedIgnoresCase.bend b/laws/ChunkedNameIgnoresCase.bend similarity index 100% rename from laws/ParseChunkedIgnoresCase.bend rename to laws/ChunkedNameIgnoresCase.bend diff --git a/laws/ChunksEmptyFuel.bend b/laws/ChunksStopWithoutFuel.bend similarity index 100% rename from laws/ChunksEmptyFuel.bend rename to laws/ChunksStopWithoutFuel.bend diff --git a/laws/DigitsReadDecimal.bend b/laws/ContentLengthReadsDecimal.bend similarity index 100% rename from laws/DigitsReadDecimal.bend rename to laws/ContentLengthReadsDecimal.bend diff --git a/laws/DigitsReadRejectsJunk.bend b/laws/ContentLengthRejectsJunk.bend similarity index 100% rename from laws/DigitsReadRejectsJunk.bend rename to laws/ContentLengthRejectsJunk.bend diff --git a/laws/ParseClenTakesPrefix.bend b/laws/ContentLengthTakesPrefix.bend similarity index 100% rename from laws/ParseClenTakesPrefix.bend rename to laws/ContentLengthTakesPrefix.bend diff --git a/laws/HeadLineRejectsContinuation.bend b/laws/ContinuationLineRejected.bend similarity index 100% rename from laws/HeadLineRejectsContinuation.bend rename to laws/ContinuationLineRejected.bend diff --git a/laws/CookieMaxAgeOpen.bend b/laws/CookieAliveUnderMaxAge.bend similarity index 100% rename from laws/CookieMaxAgeOpen.bend rename to laws/CookieAliveUnderMaxAge.bend diff --git a/laws/CookieLineAttributes.bend b/laws/CookieAttributesJoinWithSemicolons.bend similarity index 100% rename from laws/CookieLineAttributes.bend rename to laws/CookieAttributesJoinWithSemicolons.bend diff --git a/laws/CookieDefaultPathRoot.bend b/laws/CookieDefaultPathFallsBackToRoot.bend similarity index 100% rename from laws/CookieDefaultPathRoot.bend rename to laws/CookieDefaultPathFallsBackToRoot.bend diff --git a/laws/CookieDefaultPathDir.bend b/laws/CookieDefaultPathIsDirectory.bend similarity index 100% rename from laws/CookieDefaultPathDir.bend rename to laws/CookieDefaultPathIsDirectory.bend diff --git a/laws/CookieDefaultPathOneSlash.bend b/laws/CookieDefaultPathNeedsTwoSlashes.bend similarity index 100% rename from laws/CookieDefaultPathOneSlash.bend rename to laws/CookieDefaultPathNeedsTwoSlashes.bend diff --git a/laws/CookieDefaultPathTrailing.bend b/laws/CookieDefaultPathOmitsTrailingSlash.bend similarity index 100% rename from laws/CookieDefaultPathTrailing.bend rename to laws/CookieDefaultPathOmitsTrailingSlash.bend diff --git a/laws/CookieDomainSuffix.bend b/laws/CookieDomainMatchesDotSuffix.bend similarity index 100% rename from laws/CookieDomainSuffix.bend rename to laws/CookieDomainMatchesDotSuffix.bend diff --git a/laws/CookieDomainNotSuffix.bend b/laws/CookieDomainNeedsDotBeforeSuffix.bend similarity index 100% rename from laws/CookieDomainNotSuffix.bend rename to laws/CookieDomainNeedsDotBeforeSuffix.bend diff --git a/laws/CookieSendOtherHost.bend b/laws/CookieHostOnlyNotSentElsewhere.bend similarity index 100% rename from laws/CookieSendOtherHost.bend rename to laws/CookieHostOnlyNotSentElsewhere.bend diff --git a/laws/CookieSendHostOnly.bend b/laws/CookieHostOnlySentToHost.bend similarity index 100% rename from laws/CookieSendHostOnly.bend rename to laws/CookieHostOnlySentToHost.bend diff --git a/laws/CookieReceiveHostOnly.bend b/laws/CookieHostOnlyStoresHost.bend similarity index 100% rename from laws/CookieReceiveHostOnly.bend rename to laws/CookieHostOnlyStoresHost.bend diff --git a/laws/CookiePathIdentical.bend b/laws/CookieIdenticalPathsMatch.bend similarity index 100% rename from laws/CookiePathIdentical.bend rename to laws/CookieIdenticalPathsMatch.bend diff --git a/laws/CookieInsecureOnTcp.bend b/laws/CookieInsecureSentOnCleartext.bend similarity index 100% rename from laws/CookieInsecureOnTcp.bend rename to laws/CookieInsecureSentOnCleartext.bend diff --git a/laws/CookieDomainIpExact.bend b/laws/CookieIpHostMatchesExactly.bend similarity index 100% rename from laws/CookieDomainIpExact.bend rename to laws/CookieIpHostMatchesExactly.bend diff --git a/laws/CookieDomainIpSuffix.bend b/laws/CookieIpHostNoSuffixMatch.bend similarity index 100% rename from laws/CookieDomainIpSuffix.bend rename to laws/CookieIpHostNoSuffixMatch.bend diff --git a/laws/CookiePathLongerCookie.bend b/laws/CookieLongerPathFails.bend similarity index 100% rename from laws/CookiePathLongerCookie.bend rename to laws/CookieLongerPathFails.bend diff --git a/laws/CookieMaxAgeOne.bend b/laws/CookieMaxAgeOneStaleAtOne.bend similarity index 100% rename from laws/CookieMaxAgeOne.bend rename to laws/CookieMaxAgeOneStaleAtOne.bend diff --git a/laws/CookieAgeSets.bend b/laws/CookieMaxAgeSetsAge.bend similarity index 100% rename from laws/CookieAgeSets.bend rename to laws/CookieMaxAgeSetsAge.bend diff --git a/laws/CookieMaxAgeZero.bend b/laws/CookieMaxAgeZeroNotAlive.bend similarity index 100% rename from laws/CookieMaxAgeZero.bend rename to laws/CookieMaxAgeZeroNotAlive.bend diff --git a/laws/CookieIpv4Name.bend b/laws/CookieNameIsNotIpv4.bend similarity index 100% rename from laws/CookieIpv4Name.bend rename to laws/CookieNameIsNotIpv4.bend diff --git a/laws/CookieSendUnsetDomain.bend b/laws/CookieNoDomainNotSentElsewhere.bend similarity index 100% rename from laws/CookieSendUnsetDomain.bend rename to laws/CookieNoDomainNotSentElsewhere.bend diff --git a/laws/CookiePathPrefix.bend b/laws/CookiePathMatchesSlashPrefix.bend similarity index 100% rename from laws/CookiePathPrefix.bend rename to laws/CookiePathMatchesSlashPrefix.bend diff --git a/laws/CookiePathNotPrefix.bend b/laws/CookiePathNeedsSegmentBoundary.bend similarity index 100% rename from laws/CookiePathNotPrefix.bend rename to laws/CookiePathNeedsSegmentBoundary.bend diff --git a/laws/CookieIpv4Literal.bend b/laws/CookieRecognizesIpv4Literal.bend similarity index 100% rename from laws/CookieIpv4Literal.bend rename to laws/CookieRecognizesIpv4Literal.bend diff --git a/laws/CookieRequestJoins.bend b/laws/CookieRequestJoinsPairs.bend similarity index 100% rename from laws/CookieRequestJoins.bend rename to laws/CookieRequestJoinsPairs.bend diff --git a/laws/CookiePathRoot.bend b/laws/CookieRootPathMatchesLonger.bend similarity index 100% rename from laws/CookiePathRoot.bend rename to laws/CookieRootPathMatchesLonger.bend diff --git a/laws/CookieSamesiteStrict.bend b/laws/CookieSameSiteFoldsCase.bend similarity index 100% rename from laws/CookieSamesiteStrict.bend rename to laws/CookieSameSiteFoldsCase.bend diff --git a/laws/CookieSamesiteNone.bend b/laws/CookieSameSiteNoneCanonical.bend similarity index 100% rename from laws/CookieSamesiteNone.bend rename to laws/CookieSameSiteNoneCanonical.bend diff --git a/laws/CookieSecureOnTls.bend b/laws/CookieSecureSentOnTls.bend similarity index 100% rename from laws/CookieSecureOnTls.bend rename to laws/CookieSecureSentOnTls.bend diff --git a/laws/CookieSessionAlive.bend b/laws/CookieSessionStaysAlive.bend similarity index 100% rename from laws/CookieSessionAlive.bend rename to laws/CookieSessionStaysAlive.bend diff --git a/laws/CookiePathSlashPrefix.bend b/laws/CookieSlashEndedPathIsPrefix.bend similarity index 100% rename from laws/CookiePathSlashPrefix.bend rename to laws/CookieSlashEndedPathIsPrefix.bend diff --git a/laws/CookieSamesiteUnknown.bend b/laws/CookieUnknownSameSiteDropped.bend similarity index 100% rename from laws/CookieSamesiteUnknown.bend rename to laws/CookieUnknownSameSiteDropped.bend diff --git a/laws/CorsExplicitOrigin.bend b/laws/CorsAllowListReflectsOrigin.bend similarity index 100% rename from laws/CorsExplicitOrigin.bend rename to laws/CorsAllowListReflectsOrigin.bend diff --git a/laws/CorsPreflightAllowed.bend b/laws/CorsAllowedPreflightIs200.bend similarity index 100% rename from laws/CorsPreflightAllowed.bend rename to laws/CorsAllowedPreflightIs200.bend diff --git a/laws/CorsPreflightBad.bend b/laws/CorsBadPreflightIs400.bend similarity index 100% rename from laws/CorsPreflightBad.bend rename to laws/CorsBadPreflightIs400.bend diff --git a/laws/CorsCredentialsExplicitOrigin.bend b/laws/CorsCredentialsKeepExplicitOrigin.bend similarity index 100% rename from laws/CorsCredentialsExplicitOrigin.bend rename to laws/CorsCredentialsKeepExplicitOrigin.bend diff --git a/laws/CorsVaryStar.bend b/laws/CorsNoVaryForStar.bend similarity index 100% rename from laws/CorsVaryStar.bend rename to laws/CorsNoVaryForStar.bend diff --git a/laws/CorsVaryEmpty.bend b/laws/CorsNoVaryWhenRefused.bend similarity index 100% rename from laws/CorsVaryEmpty.bend rename to laws/CorsNoVaryWhenRefused.bend diff --git a/laws/CorsOnPreflight.bend b/laws/CorsOptionsWithMethodIsPreflight.bend similarity index 100% rename from laws/CorsOnPreflight.bend rename to laws/CorsOptionsWithMethodIsPreflight.bend diff --git a/laws/CorsPreflightStar.bend b/laws/CorsPreflightAnswersStar.bend similarity index 100% rename from laws/CorsPreflightStar.bend rename to laws/CorsPreflightAnswersStar.bend diff --git a/laws/CorsPreflightHeaderCase.bend b/laws/CorsPreflightHeadersIgnoreCase.bend similarity index 100% rename from laws/CorsPreflightHeaderCase.bend rename to laws/CorsPreflightHeadersIgnoreCase.bend diff --git a/laws/CorsOriginHeader.bend b/laws/CorsReadsOriginHeader.bend similarity index 100% rename from laws/CorsOriginHeader.bend rename to laws/CorsReadsOriginHeader.bend diff --git a/laws/CorsRequestHeadersHeader.bend b/laws/CorsReadsRequestHeaders.bend similarity index 100% rename from laws/CorsRequestHeadersHeader.bend rename to laws/CorsReadsRequestHeaders.bend diff --git a/laws/CorsRequestMethodHeader.bend b/laws/CorsReadsRequestMethod.bend similarity index 100% rename from laws/CorsRequestMethodHeader.bend rename to laws/CorsReadsRequestMethod.bend diff --git a/laws/CorsSimpleRefused.bend b/laws/CorsRefusedLeavesReply.bend similarity index 100% rename from laws/CorsSimpleRefused.bend rename to laws/CorsRefusedLeavesReply.bend diff --git a/laws/CorsRefusedOrigin.bend b/laws/CorsRefusesMissingOrigin.bend similarity index 100% rename from laws/CorsRefusedOrigin.bend rename to laws/CorsRefusesMissingOrigin.bend diff --git a/laws/CorsSimpleStar.bend b/laws/CorsSimpleAnswersStar.bend similarity index 100% rename from laws/CorsSimpleStar.bend rename to laws/CorsSimpleAnswersStar.bend diff --git a/laws/CorsSimpleReflects.bend b/laws/CorsSimpleKeepsBody.bend similarity index 100% rename from laws/CorsSimpleReflects.bend rename to laws/CorsSimpleKeepsBody.bend diff --git a/laws/CorsStarWithoutCredentials.bend b/laws/CorsStarStaysStar.bend similarity index 100% rename from laws/CorsStarWithoutCredentials.bend rename to laws/CorsStarStaysStar.bend diff --git a/laws/CorsVarySpecific.bend b/laws/CorsVaryOnSpecificOrigin.bend similarity index 100% rename from laws/CorsVarySpecific.bend rename to laws/CorsVaryOnSpecificOrigin.bend diff --git a/laws/DechunkEmpty.bend b/laws/DechunkEmptyIsEmpty.bend similarity index 100% rename from laws/DechunkEmpty.bend rename to laws/DechunkEmptyIsEmpty.bend diff --git a/laws/DechunkTwoChunks.bend b/laws/DechunkJoinsTwoChunks.bend similarity index 100% rename from laws/DechunkTwoChunks.bend rename to laws/DechunkJoinsTwoChunks.bend diff --git a/laws/DechunkOnlyLast.bend b/laws/DechunkLastChunkAloneIsEmpty.bend similarity index 100% rename from laws/DechunkOnlyLast.bend rename to laws/DechunkLastChunkAloneIsEmpty.bend diff --git a/laws/DechunkWiki.bend b/laws/DechunkRfcExample.bend similarity index 100% rename from laws/DechunkWiki.bend rename to laws/DechunkRfcExample.bend diff --git a/laws/MethodIdempotentDelete.bend b/laws/DeleteIsIdempotent.bend similarity index 100% rename from laws/MethodIdempotentDelete.bend rename to laws/DeleteIsIdempotent.bend diff --git a/laws/MethodSafeDelete.bend b/laws/DeleteNotSafe.bend similarity index 100% rename from laws/MethodSafeDelete.bend rename to laws/DeleteNotSafe.bend diff --git a/laws/DeleteReqNamesPath.bend b/laws/DeleteRequestLine.bend similarity index 100% rename from laws/DeleteReqNamesPath.bend rename to laws/DeleteRequestLine.bend diff --git a/laws/ParseEqualContentLengths.bend b/laws/EqualContentLengthsAreOne.bend similarity index 100% rename from laws/ParseEqualContentLengths.bend rename to laws/EqualContentLengthsAreOne.bend diff --git a/laws/ClenExact.bend b/laws/ExactContentLengthBodyKept.bend similarity index 100% rename from laws/ClenExact.bend rename to laws/ExactContentLengthBodyKept.bend diff --git a/laws/HeadersFindFirst.bend b/laws/FieldLookupFindsFirst.bend similarity index 100% rename from laws/HeadersFindFirst.bend rename to laws/FieldLookupFindsFirst.bend diff --git a/laws/HeadersFindIgnoresCase.bend b/laws/FieldLookupIgnoresCase.bend similarity index 100% rename from laws/HeadersFindIgnoresCase.bend rename to laws/FieldLookupIgnoresCase.bend diff --git a/laws/HeadHitIgnoresCase.bend b/laws/FieldNamesIgnoreCase.bend similarity index 100% rename from laws/HeadHitIgnoresCase.bend rename to laws/FieldNamesIgnoreCase.bend diff --git a/laws/HeadLineTrims.bend b/laws/FieldValueTrimsWhitespace.bend similarity index 100% rename from laws/HeadLineTrims.bend rename to laws/FieldValueTrimsWhitespace.bend diff --git a/laws/FreshMaxAgeOpen.bend b/laws/FreshUnderMaxAge.bend similarity index 100% rename from laws/FreshMaxAgeOpen.bend rename to laws/FreshUnderMaxAge.bend diff --git a/laws/AskGetAsteriskRejected.bend b/laws/GetAsteriskTargetRejected.bend similarity index 100% rename from laws/AskGetAsteriskRejected.bend rename to laws/GetAsteriskTargetRejected.bend diff --git a/laws/MethodIdempotentGet.bend b/laws/GetIsIdempotent.bend similarity index 100% rename from laws/MethodIdempotentGet.bend rename to laws/GetIsIdempotent.bend diff --git a/laws/MethodSafeGet.bend b/laws/GetIsSafe.bend similarity index 100% rename from laws/MethodSafeGet.bend rename to laws/GetIsSafe.bend diff --git a/laws/GetReqNamesPath.bend b/laws/GetRequestLine.bend similarity index 100% rename from laws/GetReqNamesPath.bend rename to laws/GetRequestLine.bend diff --git a/laws/MethodIdempotentHead.bend b/laws/HeadIsIdempotent.bend similarity index 100% rename from laws/MethodIdempotentHead.bend rename to laws/HeadIsIdempotent.bend diff --git a/laws/MethodSafeHead.bend b/laws/HeadIsSafe.bend similarity index 100% rename from laws/MethodSafeHead.bend rename to laws/HeadIsSafe.bend diff --git a/laws/HeadReqNamesPath.bend b/laws/HeadRequestLine.bend similarity index 100% rename from laws/HeadReqNamesPath.bend rename to laws/HeadRequestLine.bend diff --git a/laws/DfltHttps.bend b/laws/HttpsDefaultsTo443.bend similarity index 100% rename from laws/DfltHttps.bend rename to laws/HttpsDefaultsTo443.bend diff --git a/laws/JoinJoined.bend b/laws/JoinedPiecesAreText.bend similarity index 100% rename from laws/JoinJoined.bend rename to laws/JoinedPiecesAreText.bend diff --git a/laws/JsonParseRejects.bend b/laws/JsonRejectsNonJson.bend similarity index 100% rename from laws/JsonParseRejects.bend rename to laws/JsonRejectsNonJson.bend diff --git a/laws/JsonParseString.bend b/laws/JsonStringParsesBack.bend similarity index 100% rename from laws/JsonParseString.bend rename to laws/JsonStringParsesBack.bend diff --git a/laws/MethodSafeGetLower.bend b/laws/LowercaseGetNotSafe.bend similarity index 100% rename from laws/MethodSafeGetLower.bend rename to laws/LowercaseGetNotSafe.bend diff --git a/laws/FreshMaxAgeOne.bend b/laws/MaxAgeOneFreshThenStale.bend similarity index 100% rename from laws/FreshMaxAgeOne.bend rename to laws/MaxAgeOneFreshThenStale.bend diff --git a/laws/FreshMaxAgeOverridesExpires.bend b/laws/MaxAgeOverridesExpires.bend similarity index 100% rename from laws/FreshMaxAgeOverridesExpires.bend rename to laws/MaxAgeOverridesExpires.bend diff --git a/laws/FreshMaxAgeZero.bend b/laws/MaxAgeZeroIsStale.bend similarity index 100% rename from laws/FreshMaxAgeZero.bend rename to laws/MaxAgeZeroIsStale.bend diff --git a/laws/HeadersFindMissing.bend b/laws/MissingFieldIsEmpty.bend similarity index 100% rename from laws/HeadersFindMissing.bend rename to laws/MissingFieldIsEmpty.bend diff --git a/laws/FreshNoCacheAny.bend b/laws/NoCacheNeverFresh.bend similarity index 100% rename from laws/FreshNoCacheAny.bend rename to laws/NoCacheNeverFresh.bend diff --git a/laws/FreshNoCache.bend b/laws/NoCacheNotFresh.bend similarity index 100% rename from laws/FreshNoCache.bend rename to laws/NoCacheNotFresh.bend diff --git a/laws/FreshPublicOnly.bend b/laws/NoMaxAgeNotFresh.bend similarity index 100% rename from laws/FreshPublicOnly.bend rename to laws/NoMaxAgeNotFresh.bend diff --git a/laws/FreshNoStoreAny.bend b/laws/NoStoreNeverFresh.bend similarity index 100% rename from laws/FreshNoStoreAny.bend rename to laws/NoStoreNeverFresh.bend diff --git a/laws/FreshNoStore.bend b/laws/NoStoreNotFresh.bend similarity index 100% rename from laws/FreshNoStore.bend rename to laws/NoStoreNotFresh.bend diff --git a/laws/GetReplyKeepsBody.bend b/laws/NonHeadReplyKeepsBody.bend similarity index 100% rename from laws/GetReplyKeepsBody.bend rename to laws/NonHeadReplyKeepsBody.bend diff --git a/laws/ParseRejectsNonHexChunk.bend b/laws/NonHexChunkSizeTorn.bend similarity index 100% rename from laws/ParseRejectsNonHexChunk.bend rename to laws/NonHexChunkSizeTorn.bend diff --git a/laws/HeadLineRejectsObsFold.bend b/laws/ObsFoldRejected.bend similarity index 100% rename from laws/HeadLineRejectsObsFold.bend rename to laws/ObsFoldRejected.bend diff --git a/laws/AskOptionsAsterisk.bend b/laws/OptionsAsteriskNamesServer.bend similarity index 100% rename from laws/AskOptionsAsterisk.bend rename to laws/OptionsAsteriskNamesServer.bend diff --git a/laws/MethodIdempotentOptions.bend b/laws/OptionsIsIdempotent.bend similarity index 100% rename from laws/MethodIdempotentOptions.bend rename to laws/OptionsIsIdempotent.bend diff --git a/laws/MethodSafeOptions.bend b/laws/OptionsIsSafe.bend similarity index 100% rename from laws/MethodSafeOptions.bend rename to laws/OptionsIsSafe.bend diff --git a/laws/OptionsReqNamesPath.bend b/laws/OptionsRequestLine.bend similarity index 100% rename from laws/OptionsReqNamesPath.bend rename to laws/OptionsRequestLine.bend diff --git a/laws/AskOriginFormKeepsQuery.bend b/laws/OriginFormKeepsQuery.bend similarity index 100% rename from laws/AskOriginFormKeepsQuery.bend rename to laws/OriginFormKeepsQuery.bend diff --git a/laws/DfltPlain.bend b/laws/OtherSchemesDefaultTo80.bend similarity index 100% rename from laws/DfltPlain.bend rename to laws/OtherSchemesDefaultTo80.bend diff --git a/laws/MethodIdempotentPatch.bend b/laws/PatchNotIdempotent.bend similarity index 100% rename from laws/MethodIdempotentPatch.bend rename to laws/PatchNotIdempotent.bend diff --git a/laws/MethodIdempotentPost.bend b/laws/PostNotIdempotent.bend similarity index 100% rename from laws/MethodIdempotentPost.bend rename to laws/PostNotIdempotent.bend diff --git a/laws/MethodSafePost.bend b/laws/PostNotSafe.bend similarity index 100% rename from laws/MethodSafePost.bend rename to laws/PostNotSafe.bend diff --git a/laws/PostReqNamesPath.bend b/laws/PostRequestLine.bend similarity index 100% rename from laws/PostReqNamesPath.bend rename to laws/PostRequestLine.bend diff --git a/laws/MethodIdempotentPut.bend b/laws/PutIsIdempotent.bend similarity index 100% rename from laws/MethodIdempotentPut.bend rename to laws/PutIsIdempotent.bend diff --git a/laws/MethodSafePut.bend b/laws/PutNotSafe.bend similarity index 100% rename from laws/MethodSafePut.bend rename to laws/PutNotSafe.bend diff --git a/laws/PutReqNamesPath.bend b/laws/PutRequestLine.bend similarity index 100% rename from laws/PutReqNamesPath.bend rename to laws/PutRequestLine.bend diff --git a/laws/Reason200.bend b/laws/Reason200IsOk.bend similarity index 100% rename from laws/Reason200.bend rename to laws/Reason200IsOk.bend diff --git a/laws/Reason201.bend b/laws/Reason201IsCreated.bend similarity index 100% rename from laws/Reason201.bend rename to laws/Reason201IsCreated.bend diff --git a/laws/Reason204.bend b/laws/Reason204IsNoContent.bend similarity index 100% rename from laws/Reason204.bend rename to laws/Reason204IsNoContent.bend diff --git a/laws/Reason400.bend b/laws/Reason400IsBadRequest.bend similarity index 100% rename from laws/Reason400.bend rename to laws/Reason400IsBadRequest.bend diff --git a/laws/Reason404.bend b/laws/Reason404IsNotFound.bend similarity index 100% rename from laws/Reason404.bend rename to laws/Reason404IsNotFound.bend diff --git a/laws/Reason405.bend b/laws/Reason405IsMethodNotAllowed.bend similarity index 100% rename from laws/Reason405.bend rename to laws/Reason405IsMethodNotAllowed.bend diff --git a/laws/Reason500.bend b/laws/Reason500IsInternalServerError.bend similarity index 100% rename from laws/Reason500.bend rename to laws/Reason500IsInternalServerError.bend diff --git a/laws/AskRejectsRelativeTarget.bend b/laws/RelativeTargetRejected.bend similarity index 100% rename from laws/AskRejectsRelativeTarget.bend rename to laws/RelativeTargetRejected.bend diff --git a/laws/AskClenExact.bend b/laws/RequestBodyOfContentLengthKept.bend similarity index 100% rename from laws/AskClenExact.bend rename to laws/RequestBodyOfContentLengthKept.bend diff --git a/laws/AskRejectsDisagreeingCl.bend b/laws/RequestDisagreeingContentLengthsRejected.bend similarity index 100% rename from laws/AskRejectsDisagreeingCl.bend rename to laws/RequestDisagreeingContentLengthsRejected.bend diff --git a/laws/AskEqualContentLengths.bend b/laws/RequestEqualContentLengthsFrameOneBody.bend similarity index 100% rename from laws/AskEqualContentLengths.bend rename to laws/RequestEqualContentLengthsFrameOneBody.bend diff --git a/laws/AskKeepsFieldNameSpelling.bend b/laws/RequestKeepsFieldNameSpelling.bend similarity index 100% rename from laws/AskKeepsFieldNameSpelling.bend rename to laws/RequestKeepsFieldNameSpelling.bend diff --git a/laws/RequestNamesMethod.bend b/laws/RequestLineOpensWithMethod.bend similarity index 100% rename from laws/RequestNamesMethod.bend rename to laws/RequestLineOpensWithMethod.bend diff --git a/laws/AskNeedsBlankLine.bend b/laws/RequestNeedsBlankLine.bend similarity index 100% rename from laws/AskNeedsBlankLine.bend rename to laws/RequestNeedsBlankLine.bend diff --git a/laws/AskNeedsRequestLine.bend b/laws/RequestNeedsRequestLine.bend similarity index 100% rename from laws/AskNeedsRequestLine.bend rename to laws/RequestNeedsRequestLine.bend diff --git a/laws/AskRejectsClAndTe.bend b/laws/RequestWithBothFramingsRejected.bend similarity index 100% rename from laws/AskRejectsClAndTe.bend rename to laws/RequestWithBothFramingsRejected.bend diff --git a/laws/ParseRejectsDisagreeingCl.bend b/laws/ResponseDisagreeingContentLengthsTorn.bend similarity index 100% rename from laws/ParseRejectsDisagreeingCl.bend rename to laws/ResponseDisagreeingContentLengthsTorn.bend diff --git a/laws/ParseNeedsBlankLine.bend b/laws/ResponseNeedsBlankLine.bend similarity index 100% rename from laws/ParseNeedsBlankLine.bend rename to laws/ResponseNeedsBlankLine.bend diff --git a/laws/ParseNeedsStatus.bend b/laws/ResponseNeedsStatus.bend similarity index 100% rename from laws/ParseNeedsStatus.bend rename to laws/ResponseNeedsStatus.bend diff --git a/laws/ParseRejectsClAndTe.bend b/laws/ResponseWithBothFramingsTorn.bend similarity index 100% rename from laws/ParseRejectsClAndTe.bend rename to laws/ResponseWithBothFramingsTorn.bend diff --git a/laws/ParseSchemeFirst.bend b/laws/SchemeBeforeFirstColon.bend similarity index 100% rename from laws/ParseSchemeFirst.bend rename to laws/SchemeBeforeFirstColon.bend diff --git a/laws/CookieParseRoundTrip.bend b/laws/SetCookieParsesBack.bend similarity index 100% rename from laws/CookieParseRoundTrip.bend rename to laws/SetCookieParsesBack.bend diff --git a/laws/AskClenShort.bend b/laws/ShortRequestBodyIsBad.bend similarity index 100% rename from laws/AskClenShort.bend rename to laws/ShortRequestBodyIsBad.bend diff --git a/laws/FreshMaxAgeLimit.bend b/laws/StaleAtMaxAge.bend similarity index 100% rename from laws/FreshMaxAgeLimit.bend rename to laws/StaleAtMaxAge.bend diff --git a/laws/Utf8Split.bend b/laws/TakeDropSplitAtOneOffset.bend similarity index 100% rename from laws/Utf8Split.bend rename to laws/TakeDropSplitAtOneOffset.bend diff --git a/laws/UnframedIsRest.bend b/laws/UnframedBodyIsRest.bend similarity index 100% rename from laws/UnframedIsRest.bend rename to laws/UnframedBodyIsRest.bend diff --git a/laws/ReasonUnknownEmpty.bend b/laws/UnknownStatusHasEmptyReason.bend similarity index 100% rename from laws/ReasonUnknownEmpty.bend rename to laws/UnknownStatusHasEmptyReason.bend diff --git a/laws/ParseNeedsSlashes.bend b/laws/UrlNeedsSlashes.bend similarity index 100% rename from laws/ParseNeedsSlashes.bend rename to laws/UrlNeedsSlashes.bend diff --git a/laws/PathAbsolute.bend b/laws/UrlPathIsAbsolute.bend similarity index 100% rename from laws/PathAbsolute.bend rename to laws/UrlPathIsAbsolute.bend 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/AskEqualContentLengths.bend b/proofs/AskEqualContentLengths.bend deleted file mode 100644 index 0311be1..0000000 --- a/proofs/AskEqualContentLengths.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/AskEqualContentLengths.bend -import Base -import ../laws/AskEqualContentLengths.bend as Laws - -def Laws.ask_equal_content_lengths(): - {==} diff --git a/proofs/AskGetAsteriskRejected.bend b/proofs/AskGetAsteriskRejected.bend deleted file mode 100644 index ccc6b2a..0000000 --- a/proofs/AskGetAsteriskRejected.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/AskGetAsteriskRejected.bend -import Base -import ../laws/AskGetAsteriskRejected.bend as Laws - -def Laws.ask_get_asterisk_rejected(): - {==} diff --git a/proofs/AskKeepsFieldNameSpelling.bend b/proofs/AskKeepsFieldNameSpelling.bend deleted file mode 100644 index fa41d49..0000000 --- a/proofs/AskKeepsFieldNameSpelling.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/AskKeepsFieldNameSpelling.bend -import Base -import ../laws/AskKeepsFieldNameSpelling.bend as Laws - -def Laws.ask_keeps_field_name_spelling(): - {==} diff --git a/proofs/AskNeedsRequestLine.bend b/proofs/AskNeedsRequestLine.bend deleted file mode 100644 index 4ff8e0b..0000000 --- a/proofs/AskNeedsRequestLine.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/AskNeedsRequestLine.bend -import Base -import ../laws/AskNeedsRequestLine.bend as Laws - -def Laws.ask_needs_request_line(_body): - {==} diff --git a/proofs/AskOptionsAsterisk.bend b/proofs/AskOptionsAsterisk.bend deleted file mode 100644 index 2f67194..0000000 --- a/proofs/AskOptionsAsterisk.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/AskOptionsAsterisk.bend -import Base -import ../laws/AskOptionsAsterisk.bend as Laws - -def Laws.ask_options_asterisk(): - {==} diff --git a/proofs/AskOriginFormKeepsQuery.bend b/proofs/AskOriginFormKeepsQuery.bend deleted file mode 100644 index 9f41efb..0000000 --- a/proofs/AskOriginFormKeepsQuery.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/AskOriginFormKeepsQuery.bend -import Base -import ../laws/AskOriginFormKeepsQuery.bend as Laws - -def Laws.ask_origin_form_keeps_query(): - {==} diff --git a/proofs/AskRejectsAbsoluteForm.bend b/proofs/AskRejectsAbsoluteForm.bend deleted file mode 100644 index 4c71832..0000000 --- a/proofs/AskRejectsAbsoluteForm.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/AskRejectsAbsoluteForm.bend -import Base -import ../laws/AskRejectsAbsoluteForm.bend as Laws - -def Laws.ask_rejects_absolute_form(): - {==} diff --git a/proofs/AskRejectsClAndTe.bend b/proofs/AskRejectsClAndTe.bend deleted file mode 100644 index 2bbb339..0000000 --- a/proofs/AskRejectsClAndTe.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/AskRejectsClAndTe.bend -import Base -import ../laws/AskRejectsClAndTe.bend as Laws - -def Laws.ask_rejects_cl_and_te(): - {==} diff --git a/proofs/AskRejectsDisagreeingCl.bend b/proofs/AskRejectsDisagreeingCl.bend deleted file mode 100644 index b5c97ff..0000000 --- a/proofs/AskRejectsDisagreeingCl.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/AskRejectsDisagreeingCl.bend -import Base -import ../laws/AskRejectsDisagreeingCl.bend as Laws - -def Laws.ask_rejects_disagreeing_cl(): - {==} diff --git a/proofs/AskRejectsRelativeTarget.bend b/proofs/AskRejectsRelativeTarget.bend deleted file mode 100644 index 42b1ae9..0000000 --- a/proofs/AskRejectsRelativeTarget.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/AskRejectsRelativeTarget.bend -import Base -import ../laws/AskRejectsRelativeTarget.bend as Laws - -def Laws.ask_rejects_relative_target(): - {==} diff --git a/proofs/B64Empty.bend b/proofs/B64Empty.bend deleted file mode 100644 index 752a884..0000000 --- a/proofs/B64Empty.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/B64Empty.bend -import Base -import ../laws/B64Empty.bend as Laws - -def Laws.b64_empty(): - {==} diff --git a/proofs/B64F.bend b/proofs/B64F.bend deleted file mode 100644 index 8a1a0a2..0000000 --- a/proofs/B64F.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/B64F.bend -import Base -import ../laws/B64F.bend as Laws - -def Laws.b64_f(): - {==} diff --git a/proofs/B64Fo.bend b/proofs/B64Fo.bend deleted file mode 100644 index 0aae5ad..0000000 --- a/proofs/B64Fo.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/B64Fo.bend -import Base -import ../laws/B64Fo.bend as Laws - -def Laws.b64_fo(): - {==} diff --git a/proofs/B64Foo.bend b/proofs/B64Foo.bend deleted file mode 100644 index f0756c1..0000000 --- a/proofs/B64Foo.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/B64Foo.bend -import Base -import ../laws/B64Foo.bend as Laws - -def Laws.b64_foo(): - {==} diff --git a/proofs/B64GoEnd.bend b/proofs/B64GoEnd.bend deleted file mode 100644 index 0b3f6a4..0000000 --- a/proofs/B64GoEnd.bend +++ /dev/null @@ -1,7 +0,0 @@ -# proofs of laws/B64GoEnd.bend -import Base -import ../laws/B64GoEnd.bend as Laws - -# b64_go_end: go matches the input first -def Laws.b64_go_end(_phase): - {==} diff --git a/proofs/B64UserPass.bend b/proofs/B64UserPass.bend deleted file mode 100644 index 08c3825..0000000 --- a/proofs/B64UserPass.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/B64UserPass.bend -import Base -import ../laws/B64UserPass.bend as Laws - -def Laws.b64_user_pass(): - {==} diff --git a/proofs/AskShowBadIsMarked.bend b/proofs/BadRequestShowIsMarked.bend similarity index 54% rename from proofs/AskShowBadIsMarked.bend rename to proofs/BadRequestShowIsMarked.bend index a640221..2977444 100644 --- a/proofs/AskShowBadIsMarked.bend +++ b/proofs/BadRequestShowIsMarked.bend @@ -1,6 +1,6 @@ -# proofs of laws/AskShowBadIsMarked.bend +# proofs of laws/BadRequestShowIsMarked.bend import Base -import ../laws/AskShowBadIsMarked.bend as Laws +import ../laws/BadRequestShowIsMarked.bend as Laws import ./lemmas/BaseEqualities.bend as M1 def Laws.ask_show_bad_is_marked(why): 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/BasicUserPass.bend b/proofs/BasicUserPass.bend deleted file mode 100644 index be16649..0000000 --- a/proofs/BasicUserPass.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/BasicUserPass.bend -import Base -import ../laws/BasicUserPass.bend as Laws - -def Laws.basic_user_pass(): - {==} 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/CacheDirectiveCase.bend b/proofs/CacheDirectiveCase.bend deleted file mode 100644 index 86be227..0000000 --- a/proofs/CacheDirectiveCase.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CacheDirectiveCase.bend -import Base -import ../laws/CacheDirectiveCase.bend as Laws - -def Laws.cache_directive_case(): - {==} 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/CacheDirectives.bend b/proofs/CacheDirectives.bend deleted file mode 100644 index 4da7916..0000000 --- a/proofs/CacheDirectives.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CacheDirectives.bend -import Base -import ../laws/CacheDirectives.bend as Laws - -def Laws.cache_directives(): - {==} diff --git a/proofs/CacheIgnoresSMaxage.bend b/proofs/CacheIgnoresSMaxage.bend deleted file mode 100644 index d53bb34..0000000 --- a/proofs/CacheIgnoresSMaxage.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CacheIgnoresSMaxage.bend -import Base -import ../laws/CacheIgnoresSMaxage.bend as Laws - -def Laws.cache_ignores_s_maxage(): - {==} 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/CacheMaxAgePresent.bend b/proofs/CacheMaxAgePresent.bend deleted file mode 100644 index e3c4afa..0000000 --- a/proofs/CacheMaxAgePresent.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CacheMaxAgePresent.bend -import Base -import ../laws/CacheMaxAgePresent.bend as Laws - -def Laws.cache_max_age_present(): - {==} diff --git a/proofs/CacheNoCacheAlone.bend b/proofs/CacheNoCacheAlone.bend deleted file mode 100644 index 00d20d1..0000000 --- a/proofs/CacheNoCacheAlone.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CacheNoCacheAlone.bend -import Base -import ../laws/CacheNoCacheAlone.bend as Laws - -def Laws.cache_no_cache_alone(): - {==} diff --git a/proofs/CacheNoteExpires.bend b/proofs/CacheNoteExpires.bend deleted file mode 100644 index 5e8afb7..0000000 --- a/proofs/CacheNoteExpires.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CacheNoteExpires.bend -import Base -import ../laws/CacheNoteExpires.bend as Laws - -def Laws.cache_note_expires(): - {==} diff --git a/proofs/CachePrivateAlone.bend b/proofs/CachePrivateAlone.bend deleted file mode 100644 index b8918d7..0000000 --- a/proofs/CachePrivateAlone.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CachePrivateAlone.bend -import Base -import ../laws/CachePrivateAlone.bend as Laws - -def Laws.cache_private_alone(): - {==} 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/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/HexlenIgnoresExtension.bend b/proofs/ChunkSizeIgnoresExtension.bend similarity index 88% rename from proofs/HexlenIgnoresExtension.bend rename to proofs/ChunkSizeIgnoresExtension.bend index 664ac12..e0b2025 100644 --- a/proofs/HexlenIgnoresExtension.bend +++ b/proofs/ChunkSizeIgnoresExtension.bend @@ -1,7 +1,7 @@ -# proofs of laws/HexlenIgnoresExtension.bend +# proofs of laws/ChunkSizeIgnoresExtension.bend import Base import ../src/http.bend as Http -import ../laws/HexlenIgnoresExtension.bend as Laws +import ../laws/ChunkSizeIgnoresExtension.bend as Laws import ../laws/SplitAtCharacter.bend as L1 import ./lemmas/BaseEqualities.bend as M1 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/ChunksEmptyFuel.bend b/proofs/ChunksEmptyFuel.bend deleted file mode 100644 index d677921..0000000 --- a/proofs/ChunksEmptyFuel.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/ChunksEmptyFuel.bend -import Base -import ../laws/ChunksEmptyFuel.bend as Laws - -def Laws.chunks_empty_fuel(): - {==} 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/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/CookieDefaultPathDir.bend b/proofs/CookieDefaultPathDir.bend deleted file mode 100644 index cc2e5a6..0000000 --- a/proofs/CookieDefaultPathDir.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieDefaultPathDir.bend -import Base -import ../laws/CookieDefaultPathDir.bend as Laws - -def Laws.cookie_default_path_dir(): - {==} 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/CookieDefaultPathOneSlash.bend b/proofs/CookieDefaultPathOneSlash.bend deleted file mode 100644 index 14ff6a2..0000000 --- a/proofs/CookieDefaultPathOneSlash.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieDefaultPathOneSlash.bend -import Base -import ../laws/CookieDefaultPathOneSlash.bend as Laws - -def Laws.cookie_default_path_one_slash(): - {==} diff --git a/proofs/CookieDefaultPathRoot.bend b/proofs/CookieDefaultPathRoot.bend deleted file mode 100644 index 8f586e5..0000000 --- a/proofs/CookieDefaultPathRoot.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieDefaultPathRoot.bend -import Base -import ../laws/CookieDefaultPathRoot.bend as Laws - -def Laws.cookie_default_path_root(): - {==} diff --git a/proofs/CookieDefaultPathTrailing.bend b/proofs/CookieDefaultPathTrailing.bend deleted file mode 100644 index e6326ac..0000000 --- a/proofs/CookieDefaultPathTrailing.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieDefaultPathTrailing.bend -import Base -import ../laws/CookieDefaultPathTrailing.bend as Laws - -def Laws.cookie_default_path_trailing(): - {==} diff --git a/proofs/CookieDomainIpExact.bend b/proofs/CookieDomainIpExact.bend deleted file mode 100644 index b89147c..0000000 --- a/proofs/CookieDomainIpExact.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieDomainIpExact.bend -import Base -import ../laws/CookieDomainIpExact.bend as Laws - -def Laws.cookie_domain_ip_exact(): - {==} diff --git a/proofs/CookieDomainIpSuffix.bend b/proofs/CookieDomainIpSuffix.bend deleted file mode 100644 index 8600434..0000000 --- a/proofs/CookieDomainIpSuffix.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieDomainIpSuffix.bend -import Base -import ../laws/CookieDomainIpSuffix.bend as Laws - -def Laws.cookie_domain_ip_suffix(): - {==} 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/CookieDomainNotSuffix.bend b/proofs/CookieDomainNotSuffix.bend deleted file mode 100644 index 18ea71d..0000000 --- a/proofs/CookieDomainNotSuffix.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieDomainNotSuffix.bend -import Base -import ../laws/CookieDomainNotSuffix.bend as Laws - -def Laws.cookie_domain_not_suffix(): - {==} diff --git a/proofs/CookieDomainSuffix.bend b/proofs/CookieDomainSuffix.bend deleted file mode 100644 index 83ba1d6..0000000 --- a/proofs/CookieDomainSuffix.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieDomainSuffix.bend -import Base -import ../laws/CookieDomainSuffix.bend as Laws - -def Laws.cookie_domain_suffix(): - {==} 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/CookieInsecureOnTcp.bend b/proofs/CookieInsecureOnTcp.bend deleted file mode 100644 index 78a6f61..0000000 --- a/proofs/CookieInsecureOnTcp.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieInsecureOnTcp.bend -import Base -import ../laws/CookieInsecureOnTcp.bend as Laws - -def Laws.cookie_insecure_on_tcp(): - {==} 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/CookieIpv4Literal.bend b/proofs/CookieIpv4Literal.bend deleted file mode 100644 index 0c831a8..0000000 --- a/proofs/CookieIpv4Literal.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieIpv4Literal.bend -import Base -import ../laws/CookieIpv4Literal.bend as Laws - -def Laws.cookie_ipv4_literal(): - {==} diff --git a/proofs/CookieIpv4Name.bend b/proofs/CookieIpv4Name.bend deleted file mode 100644 index 28b8827..0000000 --- a/proofs/CookieIpv4Name.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieIpv4Name.bend -import Base -import ../laws/CookieIpv4Name.bend as Laws - -def Laws.cookie_ipv4_name(): - {==} diff --git a/proofs/CookieLineAttributes.bend b/proofs/CookieLineAttributes.bend deleted file mode 100644 index 44c960a..0000000 --- a/proofs/CookieLineAttributes.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieLineAttributes.bend -import Base -import ../laws/CookieLineAttributes.bend as Laws - -def Laws.cookie_line_attributes(): - {==} 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/CookieMaxAgeOne.bend b/proofs/CookieMaxAgeOne.bend deleted file mode 100644 index cbeeed6..0000000 --- a/proofs/CookieMaxAgeOne.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieMaxAgeOne.bend -import Base -import ../laws/CookieMaxAgeOne.bend as Laws - -def Laws.cookie_max_age_one(): - {==} 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/CookieMaxAgeOpen.bend b/proofs/CookieMaxAgeOpen.bend deleted file mode 100644 index 42b4356..0000000 --- a/proofs/CookieMaxAgeOpen.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieMaxAgeOpen.bend -import Base -import ../laws/CookieMaxAgeOpen.bend as Laws - -def Laws.cookie_max_age_open(): - {==} diff --git a/proofs/CookieAgeSets.bend b/proofs/CookieMaxAgeSetsAge.bend similarity index 70% rename from proofs/CookieAgeSets.bend rename to proofs/CookieMaxAgeSetsAge.bend index 2cf8e4a..619cd5b 100644 --- a/proofs/CookieAgeSets.bend +++ b/proofs/CookieMaxAgeSetsAge.bend @@ -1,6 +1,6 @@ -# proofs of laws/CookieAgeSets.bend +# proofs of laws/CookieMaxAgeSetsAge.bend import Base -import ../laws/CookieAgeSets.bend as Laws +import ../laws/CookieMaxAgeSetsAge.bend as Laws # cookie_age_sets holds by unfolding cookie.age def Laws.cookie_age_sets( diff --git a/proofs/CookieMaxAgeZero.bend b/proofs/CookieMaxAgeZero.bend deleted file mode 100644 index 429fdc5..0000000 --- a/proofs/CookieMaxAgeZero.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieMaxAgeZero.bend -import Base -import ../laws/CookieMaxAgeZero.bend as Laws - -def Laws.cookie_max_age_zero(): - {==} 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/CookieParseRoundTrip.bend b/proofs/CookieParseRoundTrip.bend deleted file mode 100644 index d1c19b6..0000000 --- a/proofs/CookieParseRoundTrip.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieParseRoundTrip.bend -import Base -import ../laws/CookieParseRoundTrip.bend as Laws - -def Laws.cookie_parse_round_trip(): - {==} diff --git a/proofs/CookiePathIdentical.bend b/proofs/CookiePathIdentical.bend deleted file mode 100644 index b6d048b..0000000 --- a/proofs/CookiePathIdentical.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookiePathIdentical.bend -import Base -import ../laws/CookiePathIdentical.bend as Laws - -def Laws.cookie_path_identical(): - {==} diff --git a/proofs/CookiePathLongerCookie.bend b/proofs/CookiePathLongerCookie.bend deleted file mode 100644 index 45cdb36..0000000 --- a/proofs/CookiePathLongerCookie.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookiePathLongerCookie.bend -import Base -import ../laws/CookiePathLongerCookie.bend as Laws - -def Laws.cookie_path_longer_cookie(): - {==} 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/CookiePathNotPrefix.bend b/proofs/CookiePathNotPrefix.bend deleted file mode 100644 index 45a79b5..0000000 --- a/proofs/CookiePathNotPrefix.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookiePathNotPrefix.bend -import Base -import ../laws/CookiePathNotPrefix.bend as Laws - -def Laws.cookie_path_not_prefix(): - {==} diff --git a/proofs/CookiePathPrefix.bend b/proofs/CookiePathPrefix.bend deleted file mode 100644 index 0aa37d1..0000000 --- a/proofs/CookiePathPrefix.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookiePathPrefix.bend -import Base -import ../laws/CookiePathPrefix.bend as Laws - -def Laws.cookie_path_prefix(): - {==} diff --git a/proofs/CookiePathRoot.bend b/proofs/CookiePathRoot.bend deleted file mode 100644 index d6a4a24..0000000 --- a/proofs/CookiePathRoot.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookiePathRoot.bend -import Base -import ../laws/CookiePathRoot.bend as Laws - -def Laws.cookie_path_root(): - {==} diff --git a/proofs/CookiePathSlashPrefix.bend b/proofs/CookiePathSlashPrefix.bend deleted file mode 100644 index 6805930..0000000 --- a/proofs/CookiePathSlashPrefix.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookiePathSlashPrefix.bend -import Base -import ../laws/CookiePathSlashPrefix.bend as Laws - -def Laws.cookie_path_slash_prefix(): - {==} diff --git a/proofs/CookieReceiveHostOnly.bend b/proofs/CookieReceiveHostOnly.bend deleted file mode 100644 index 24b6be5..0000000 --- a/proofs/CookieReceiveHostOnly.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieReceiveHostOnly.bend -import Base -import ../laws/CookieReceiveHostOnly.bend as Laws - -def Laws.cookie_receive_host_only(): - {==} 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/CookieRequestJoins.bend b/proofs/CookieRequestJoins.bend deleted file mode 100644 index 7aaaa37..0000000 --- a/proofs/CookieRequestJoins.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieRequestJoins.bend -import Base -import ../laws/CookieRequestJoins.bend as Laws - -def Laws.cookie_request_joins(): - {==} 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/CookieSamesiteNone.bend b/proofs/CookieSamesiteNone.bend deleted file mode 100644 index a63f3f0..0000000 --- a/proofs/CookieSamesiteNone.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieSamesiteNone.bend -import Base -import ../laws/CookieSamesiteNone.bend as Laws - -def Laws.cookie_samesite_none(): - {==} diff --git a/proofs/CookieSamesiteStrict.bend b/proofs/CookieSamesiteStrict.bend deleted file mode 100644 index a8d787d..0000000 --- a/proofs/CookieSamesiteStrict.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieSamesiteStrict.bend -import Base -import ../laws/CookieSamesiteStrict.bend as Laws - -def Laws.cookie_samesite_strict(): - {==} diff --git a/proofs/CookieSamesiteUnknown.bend b/proofs/CookieSamesiteUnknown.bend deleted file mode 100644 index 5776300..0000000 --- a/proofs/CookieSamesiteUnknown.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieSamesiteUnknown.bend -import Base -import ../laws/CookieSamesiteUnknown.bend as Laws - -def Laws.cookie_samesite_unknown(): - {==} diff --git a/proofs/CookieSecureOnTls.bend b/proofs/CookieSecureOnTls.bend deleted file mode 100644 index c62e2fe..0000000 --- a/proofs/CookieSecureOnTls.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieSecureOnTls.bend -import Base -import ../laws/CookieSecureOnTls.bend as Laws - -def Laws.cookie_secure_on_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/CookieSendHostOnly.bend b/proofs/CookieSendHostOnly.bend deleted file mode 100644 index 42396ec..0000000 --- a/proofs/CookieSendHostOnly.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieSendHostOnly.bend -import Base -import ../laws/CookieSendHostOnly.bend as Laws - -def Laws.cookie_send_host_only(): - {==} diff --git a/proofs/CookieSendOtherHost.bend b/proofs/CookieSendOtherHost.bend deleted file mode 100644 index c655aec..0000000 --- a/proofs/CookieSendOtherHost.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieSendOtherHost.bend -import Base -import ../laws/CookieSendOtherHost.bend as Laws - -def Laws.cookie_send_other_host(): - {==} diff --git a/proofs/CookieSendUnsetDomain.bend b/proofs/CookieSendUnsetDomain.bend deleted file mode 100644 index 9854e7c..0000000 --- a/proofs/CookieSendUnsetDomain.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieSendUnsetDomain.bend -import Base -import ../laws/CookieSendUnsetDomain.bend as Laws - -def Laws.cookie_send_unset_domain(): - {==} diff --git a/proofs/CookieSessionAlive.bend b/proofs/CookieSessionAlive.bend deleted file mode 100644 index fd789ef..0000000 --- a/proofs/CookieSessionAlive.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CookieSessionAlive.bend -import Base -import ../laws/CookieSessionAlive.bend as Laws - -def Laws.cookie_session_alive(): - {==} 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/CorsPreflightBad.bend b/proofs/CorsBadPreflightIs400.bend similarity index 56% rename from proofs/CorsPreflightBad.bend rename to proofs/CorsBadPreflightIs400.bend index 106f937..faa172b 100644 --- a/proofs/CorsPreflightBad.bend +++ b/proofs/CorsBadPreflightIs400.bend @@ -1,6 +1,6 @@ -# proofs of laws/CorsPreflightBad.bend +# proofs of laws/CorsBadPreflightIs400.bend import Base -import ../laws/CorsPreflightBad.bend as Laws +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/CorsCredentialsExplicitOrigin.bend b/proofs/CorsCredentialsExplicitOrigin.bend deleted file mode 100644 index 7438c83..0000000 --- a/proofs/CorsCredentialsExplicitOrigin.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsCredentialsExplicitOrigin.bend -import Base -import ../laws/CorsCredentialsExplicitOrigin.bend as Laws - -def Laws.cors_credentials_explicit_origin(): - {==} 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/CorsExplicitOrigin.bend b/proofs/CorsExplicitOrigin.bend deleted file mode 100644 index 0e821cc..0000000 --- a/proofs/CorsExplicitOrigin.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsExplicitOrigin.bend -import Base -import ../laws/CorsExplicitOrigin.bend as Laws - -def Laws.cors_explicit_origin(): - {==} 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/CorsOnPreflight.bend b/proofs/CorsOnPreflight.bend deleted file mode 100644 index 6a9e548..0000000 --- a/proofs/CorsOnPreflight.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsOnPreflight.bend -import Base -import ../laws/CorsOnPreflight.bend as Laws - -def Laws.cors_on_preflight(): - {==} 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/CorsOriginHeader.bend b/proofs/CorsOriginHeader.bend deleted file mode 100644 index 491b0b3..0000000 --- a/proofs/CorsOriginHeader.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsOriginHeader.bend -import Base -import ../laws/CorsOriginHeader.bend as Laws - -def Laws.cors_origin_header(): - {==} diff --git a/proofs/CorsPreflightAllowed.bend b/proofs/CorsPreflightAllowed.bend deleted file mode 100644 index a12d96b..0000000 --- a/proofs/CorsPreflightAllowed.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsPreflightAllowed.bend -import Base -import ../laws/CorsPreflightAllowed.bend as Laws - -def Laws.cors_preflight_allowed(): - {==} 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/CorsPreflightHeaderCase.bend b/proofs/CorsPreflightHeaderCase.bend deleted file mode 100644 index 363cb86..0000000 --- a/proofs/CorsPreflightHeaderCase.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsPreflightHeaderCase.bend -import Base -import ../laws/CorsPreflightHeaderCase.bend as Laws - -def Laws.cors_preflight_header_case(): - {==} 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/CorsPreflightStar.bend b/proofs/CorsPreflightStar.bend deleted file mode 100644 index c728023..0000000 --- a/proofs/CorsPreflightStar.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsPreflightStar.bend -import Base -import ../laws/CorsPreflightStar.bend as Laws - -def Laws.cors_preflight_star(): - {==} 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/CorsRefusedOrigin.bend b/proofs/CorsRefusedOrigin.bend deleted file mode 100644 index ee79e17..0000000 --- a/proofs/CorsRefusedOrigin.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsRefusedOrigin.bend -import Base -import ../laws/CorsRefusedOrigin.bend as Laws - -def Laws.cors_refused_origin(): - {==} 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/CorsRequestHeadersHeader.bend b/proofs/CorsRequestHeadersHeader.bend deleted file mode 100644 index d89e8d5..0000000 --- a/proofs/CorsRequestHeadersHeader.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsRequestHeadersHeader.bend -import Base -import ../laws/CorsRequestHeadersHeader.bend as Laws - -def Laws.cors_request_headers_header(): - {==} diff --git a/proofs/CorsRequestMethodHeader.bend b/proofs/CorsRequestMethodHeader.bend deleted file mode 100644 index 609b00a..0000000 --- a/proofs/CorsRequestMethodHeader.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsRequestMethodHeader.bend -import Base -import ../laws/CorsRequestMethodHeader.bend as Laws - -def Laws.cors_request_method_header(): - {==} 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/CorsSimpleReflects.bend b/proofs/CorsSimpleReflects.bend deleted file mode 100644 index e245662..0000000 --- a/proofs/CorsSimpleReflects.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsSimpleReflects.bend -import Base -import ../laws/CorsSimpleReflects.bend as Laws - -def Laws.cors_simple_reflects(): - {==} diff --git a/proofs/CorsSimpleRefused.bend b/proofs/CorsSimpleRefused.bend deleted file mode 100644 index ebad234..0000000 --- a/proofs/CorsSimpleRefused.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsSimpleRefused.bend -import Base -import ../laws/CorsSimpleRefused.bend as Laws - -def Laws.cors_simple_refused(): - {==} diff --git a/proofs/CorsSimpleStar.bend b/proofs/CorsSimpleStar.bend deleted file mode 100644 index 3a9a53f..0000000 --- a/proofs/CorsSimpleStar.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsSimpleStar.bend -import Base -import ../laws/CorsSimpleStar.bend as Laws - -def Laws.cors_simple_star(): - {==} 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/CorsStarWithoutCredentials.bend b/proofs/CorsStarWithoutCredentials.bend deleted file mode 100644 index e173fc2..0000000 --- a/proofs/CorsStarWithoutCredentials.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsStarWithoutCredentials.bend -import Base -import ../laws/CorsStarWithoutCredentials.bend as Laws - -def Laws.cors_star_without_credentials(): - {==} diff --git a/proofs/CorsVaryEmpty.bend b/proofs/CorsVaryEmpty.bend deleted file mode 100644 index e0b26d9..0000000 --- a/proofs/CorsVaryEmpty.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsVaryEmpty.bend -import Base -import ../laws/CorsVaryEmpty.bend as Laws - -def Laws.cors_vary_empty(): - {==} 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/CorsVarySpecific.bend b/proofs/CorsVarySpecific.bend deleted file mode 100644 index 17edb02..0000000 --- a/proofs/CorsVarySpecific.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsVarySpecific.bend -import Base -import ../laws/CorsVarySpecific.bend as Laws - -def Laws.cors_vary_specific(): - {==} diff --git a/proofs/CorsVaryStar.bend b/proofs/CorsVaryStar.bend deleted file mode 100644 index f6b0880..0000000 --- a/proofs/CorsVaryStar.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/CorsVaryStar.bend -import Base -import ../laws/CorsVaryStar.bend as Laws - -def Laws.cors_vary_star(): - {==} diff --git a/proofs/DechunkEmpty.bend b/proofs/DechunkEmpty.bend deleted file mode 100644 index 9296144..0000000 --- a/proofs/DechunkEmpty.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/DechunkEmpty.bend -import Base -import ../laws/DechunkEmpty.bend as Laws - -def Laws.dechunk_empty(): - {==} 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/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/DechunkOnlyLast.bend b/proofs/DechunkOnlyLast.bend deleted file mode 100644 index 87f51fe..0000000 --- a/proofs/DechunkOnlyLast.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/DechunkOnlyLast.bend -import Base -import ../laws/DechunkOnlyLast.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/DechunkTwoChunks.bend b/proofs/DechunkTwoChunks.bend deleted file mode 100644 index bb29a41..0000000 --- a/proofs/DechunkTwoChunks.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/DechunkTwoChunks.bend -import Base -import ../laws/DechunkTwoChunks.bend as Laws - -def Laws.dechunk_two_chunks(): - {==} diff --git a/proofs/DechunkWiki.bend b/proofs/DechunkWiki.bend deleted file mode 100644 index cc89769..0000000 --- a/proofs/DechunkWiki.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/DechunkWiki.bend -import Base -import ../laws/DechunkWiki.bend as Laws - -def Laws.dechunk_wiki(): - {==} 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/DeleteReqNamesPath.bend b/proofs/DeleteRequestLine.bend similarity index 77% rename from proofs/DeleteReqNamesPath.bend rename to proofs/DeleteRequestLine.bend index 86ac9a2..47fecc6 100644 --- a/proofs/DeleteReqNamesPath.bend +++ b/proofs/DeleteRequestLine.bend @@ -1,7 +1,7 @@ -# proofs of laws/DeleteReqNamesPath.bend +# proofs of laws/DeleteRequestLine.bend import Base import ../src/http.bend as Http -import ../laws/DeleteReqNamesPath.bend as Laws +import ../laws/DeleteRequestLine.bend as Laws import ./lemmas/BaseEqualities.bend as M1 def Laws.delete_req_names_path(host, path, hs, body): diff --git a/proofs/DfltHttps.bend b/proofs/DfltHttps.bend deleted file mode 100644 index b57f147..0000000 --- a/proofs/DfltHttps.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/DfltHttps.bend -import Base -import ../laws/DfltHttps.bend as Laws - -def Laws.dflt_https(): - {==} diff --git a/proofs/DigitsReadDecimal.bend b/proofs/DigitsReadDecimal.bend deleted file mode 100644 index 6bd73c3..0000000 --- a/proofs/DigitsReadDecimal.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/DigitsReadDecimal.bend -import Base -import ../laws/DigitsReadDecimal.bend as Laws - -def Laws.digits_read_decimal(): - {==} diff --git a/proofs/DigitsReadRejectsJunk.bend b/proofs/DigitsReadRejectsJunk.bend deleted file mode 100644 index 128a665..0000000 --- a/proofs/DigitsReadRejectsJunk.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/DigitsReadRejectsJunk.bend -import Base -import ../laws/DigitsReadRejectsJunk.bend as Laws - -def Laws.digits_read_rejects_junk(): - {==} 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/ClenExact.bend b/proofs/ExactContentLengthBodyKept.bend similarity index 86% rename from proofs/ClenExact.bend rename to proofs/ExactContentLengthBodyKept.bend index 0931fe3..7bf5e36 100644 --- a/proofs/ClenExact.bend +++ b/proofs/ExactContentLengthBodyKept.bend @@ -1,7 +1,7 @@ -# proofs of laws/ClenExact.bend +# proofs of laws/ExactContentLengthBodyKept.bend import Base import ../src/http.bend as Http -import ../laws/ClenExact.bend as Laws +import ../laws/ExactContentLengthBodyKept.bend as Laws import ./lemmas/NatOrder.bend as M1 def Laws.clen_exact(st, hs, body): 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/FreshMaxAgeLimit.bend b/proofs/FreshMaxAgeLimit.bend deleted file mode 100644 index 9173819..0000000 --- a/proofs/FreshMaxAgeLimit.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/FreshMaxAgeLimit.bend -import Base -import ../laws/FreshMaxAgeLimit.bend as Laws - -def Laws.fresh_max_age_limit(): - {==} diff --git a/proofs/FreshMaxAgeOne.bend b/proofs/FreshMaxAgeOne.bend deleted file mode 100644 index fd5b9d8..0000000 --- a/proofs/FreshMaxAgeOne.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/FreshMaxAgeOne.bend -import Base -import ../laws/FreshMaxAgeOne.bend as Laws - -def Laws.fresh_max_age_one(): - {==} diff --git a/proofs/FreshMaxAgeOpen.bend b/proofs/FreshMaxAgeOpen.bend deleted file mode 100644 index 1a7a4f5..0000000 --- a/proofs/FreshMaxAgeOpen.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/FreshMaxAgeOpen.bend -import Base -import ../laws/FreshMaxAgeOpen.bend as Laws - -def Laws.fresh_max_age_open(): - {==} diff --git a/proofs/FreshMaxAgeOverridesExpires.bend b/proofs/FreshMaxAgeOverridesExpires.bend deleted file mode 100644 index 9b4ea5c..0000000 --- a/proofs/FreshMaxAgeOverridesExpires.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/FreshMaxAgeOverridesExpires.bend -import Base -import ../laws/FreshMaxAgeOverridesExpires.bend as Laws - -def Laws.fresh_max_age_overrides_expires(): - {==} diff --git a/proofs/FreshMaxAgeZero.bend b/proofs/FreshMaxAgeZero.bend deleted file mode 100644 index 0153bc1..0000000 --- a/proofs/FreshMaxAgeZero.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/FreshMaxAgeZero.bend -import Base -import ../laws/FreshMaxAgeZero.bend as Laws - -def Laws.fresh_max_age_zero(): - {==} diff --git a/proofs/FreshNoCache.bend b/proofs/FreshNoCache.bend deleted file mode 100644 index 96cddd4..0000000 --- a/proofs/FreshNoCache.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/FreshNoCache.bend -import Base -import ../laws/FreshNoCache.bend as Laws - -def Laws.fresh_no_cache(): - {==} diff --git a/proofs/FreshNoStore.bend b/proofs/FreshNoStore.bend deleted file mode 100644 index 681c710..0000000 --- a/proofs/FreshNoStore.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/FreshNoStore.bend -import Base -import ../laws/FreshNoStore.bend as Laws - -def Laws.fresh_no_store(): - {==} diff --git a/proofs/FreshPublicOnly.bend b/proofs/FreshPublicOnly.bend deleted file mode 100644 index 64a46c2..0000000 --- a/proofs/FreshPublicOnly.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/FreshPublicOnly.bend -import Base -import ../laws/FreshPublicOnly.bend as Laws - -def Laws.fresh_public_only(): - {==} 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/GetReplyKeepsBody.bend b/proofs/GetReplyKeepsBody.bend deleted file mode 100644 index ec0df74..0000000 --- a/proofs/GetReplyKeepsBody.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/GetReplyKeepsBody.bend -import Base -import ../laws/GetReplyKeepsBody.bend as Laws - -def Laws.get_reply_keeps_body(): - {==} diff --git a/proofs/GetReqNamesPath.bend b/proofs/GetRequestLine.bend similarity index 75% rename from proofs/GetReqNamesPath.bend rename to proofs/GetRequestLine.bend index 312b933..bb5959d 100644 --- a/proofs/GetReqNamesPath.bend +++ b/proofs/GetRequestLine.bend @@ -1,7 +1,7 @@ -# proofs of laws/GetReqNamesPath.bend +# proofs of laws/GetRequestLine.bend import Base import ../src/http.bend as Http -import ../laws/GetReqNamesPath.bend as Laws +import ../laws/GetRequestLine.bend as Laws import ./lemmas/BaseEqualities.bend as M1 def Laws.get_req_names_path(host, path, hs): diff --git a/proofs/HeadHitIgnoresCase.bend b/proofs/HeadHitIgnoresCase.bend deleted file mode 100644 index ce27207..0000000 --- a/proofs/HeadHitIgnoresCase.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/HeadHitIgnoresCase.bend -import Base -import ../laws/HeadHitIgnoresCase.bend as Laws - -def Laws.head_hit_ignores_case(_h, _name): - {==} 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/HeadLineRejectsContinuation.bend b/proofs/HeadLineRejectsContinuation.bend deleted file mode 100644 index 7f12685..0000000 --- a/proofs/HeadLineRejectsContinuation.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/HeadLineRejectsContinuation.bend -import Base -import ../laws/HeadLineRejectsContinuation.bend as Laws - -def Laws.head_line_rejects_continuation(): - {==} diff --git a/proofs/HeadLineRejectsObsFold.bend b/proofs/HeadLineRejectsObsFold.bend deleted file mode 100644 index eb399db..0000000 --- a/proofs/HeadLineRejectsObsFold.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/HeadLineRejectsObsFold.bend -import Base -import ../laws/HeadLineRejectsObsFold.bend as Laws - -def Laws.head_line_rejects_obs_fold(): - {==} diff --git a/proofs/HeadLineTrims.bend b/proofs/HeadLineTrims.bend deleted file mode 100644 index 9dd60b4..0000000 --- a/proofs/HeadLineTrims.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/HeadLineTrims.bend -import Base -import ../laws/HeadLineTrims.bend as Laws - -def Laws.head_line_trims(): - {==} diff --git a/proofs/HeadReqNamesPath.bend b/proofs/HeadRequestLine.bend similarity index 75% rename from proofs/HeadReqNamesPath.bend rename to proofs/HeadRequestLine.bend index 4858fbb..100c8c9 100644 --- a/proofs/HeadReqNamesPath.bend +++ b/proofs/HeadRequestLine.bend @@ -1,7 +1,7 @@ -# proofs of laws/HeadReqNamesPath.bend +# proofs of laws/HeadRequestLine.bend import Base import ../src/http.bend as Http -import ../laws/HeadReqNamesPath.bend as Laws +import ../laws/HeadRequestLine.bend as Laws import ./lemmas/BaseEqualities.bend as M1 def Laws.head_req_names_path(host, path, hs): diff --git a/proofs/HeadersFindFirst.bend b/proofs/HeadersFindFirst.bend deleted file mode 100644 index 939ac3f..0000000 --- a/proofs/HeadersFindFirst.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/HeadersFindFirst.bend -import Base -import ../laws/HeadersFindFirst.bend as Laws - -def Laws.headers_find_first(): - {==} diff --git a/proofs/HeadersFindIgnoresCase.bend b/proofs/HeadersFindIgnoresCase.bend deleted file mode 100644 index 24d5277..0000000 --- a/proofs/HeadersFindIgnoresCase.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/HeadersFindIgnoresCase.bend -import Base -import ../laws/HeadersFindIgnoresCase.bend as Laws - -def Laws.headers_find_ignores_case(): - {==} diff --git a/proofs/HeadersFindMissing.bend b/proofs/HeadersFindMissing.bend deleted file mode 100644 index 02e30f6..0000000 --- a/proofs/HeadersFindMissing.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/HeadersFindMissing.bend -import Base -import ../laws/HeadersFindMissing.bend as Laws - -def Laws.headers_find_missing(): - {==} diff --git a/proofs/HexlenAlpha.bend b/proofs/HexlenAlpha.bend deleted file mode 100644 index 6bc0a52..0000000 --- a/proofs/HexlenAlpha.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/HexlenAlpha.bend -import Base -import ../laws/HexlenAlpha.bend as Laws - -def Laws.hexlen_alpha(): - {==} diff --git a/proofs/HexlenExtensionAndCase.bend b/proofs/HexlenExtensionAndCase.bend deleted file mode 100644 index 33aef04..0000000 --- a/proofs/HexlenExtensionAndCase.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/HexlenExtensionAndCase.bend -import Base -import ../laws/HexlenExtensionAndCase.bend as Laws - -def Laws.hexlen_extension_and_case(): - {==} 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/JoinJoined.bend b/proofs/JoinedPiecesAreText.bend similarity index 93% rename from proofs/JoinJoined.bend rename to proofs/JoinedPiecesAreText.bend index 5c92291..4bed388 100644 --- a/proofs/JoinJoined.bend +++ b/proofs/JoinedPiecesAreText.bend @@ -1,7 +1,7 @@ -# proofs of laws/JoinJoined.bend +# proofs of laws/JoinedPiecesAreText.bend import Base import ../src/wirespec.bend as Spec -import ../laws/JoinJoined.bend as Laws +import ../laws/JoinedPiecesAreText.bend as Laws law app_nil: for x: String diff --git a/proofs/JsonParseRejects.bend b/proofs/JsonParseRejects.bend deleted file mode 100644 index 5e0edb2..0000000 --- a/proofs/JsonParseRejects.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/JsonParseRejects.bend -import Base -import ../laws/JsonParseRejects.bend as Laws - -def Laws.json_parse_rejects(): - {==} diff --git a/proofs/JsonParseString.bend b/proofs/JsonParseString.bend deleted file mode 100644 index 417cd3a..0000000 --- a/proofs/JsonParseString.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/JsonParseString.bend -import Base -import ../laws/JsonParseString.bend as Laws - -def Laws.json_parse_string(): - {==} 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/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/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/MethodIdempotentDelete.bend b/proofs/MethodIdempotentDelete.bend deleted file mode 100644 index f6139a2..0000000 --- a/proofs/MethodIdempotentDelete.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodIdempotentDelete.bend -import Base -import ../laws/MethodIdempotentDelete.bend as Laws - -def Laws.method_idempotent_delete(): - {==} diff --git a/proofs/MethodIdempotentGet.bend b/proofs/MethodIdempotentGet.bend deleted file mode 100644 index bc651cd..0000000 --- a/proofs/MethodIdempotentGet.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodIdempotentGet.bend -import Base -import ../laws/MethodIdempotentGet.bend as Laws - -def Laws.method_idempotent_get(): - {==} diff --git a/proofs/MethodIdempotentHead.bend b/proofs/MethodIdempotentHead.bend deleted file mode 100644 index 8d08328..0000000 --- a/proofs/MethodIdempotentHead.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodIdempotentHead.bend -import Base -import ../laws/MethodIdempotentHead.bend as Laws - -def Laws.method_idempotent_head(): - {==} diff --git a/proofs/MethodIdempotentOptions.bend b/proofs/MethodIdempotentOptions.bend deleted file mode 100644 index c1d6c14..0000000 --- a/proofs/MethodIdempotentOptions.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodIdempotentOptions.bend -import Base -import ../laws/MethodIdempotentOptions.bend as Laws - -def Laws.method_idempotent_options(): - {==} diff --git a/proofs/MethodIdempotentPatch.bend b/proofs/MethodIdempotentPatch.bend deleted file mode 100644 index f68bb3f..0000000 --- a/proofs/MethodIdempotentPatch.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodIdempotentPatch.bend -import Base -import ../laws/MethodIdempotentPatch.bend as Laws - -def Laws.method_idempotent_patch(): - {==} diff --git a/proofs/MethodIdempotentPost.bend b/proofs/MethodIdempotentPost.bend deleted file mode 100644 index ff89fce..0000000 --- a/proofs/MethodIdempotentPost.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodIdempotentPost.bend -import Base -import ../laws/MethodIdempotentPost.bend as Laws - -def Laws.method_idempotent_post(): - {==} diff --git a/proofs/MethodIdempotentPut.bend b/proofs/MethodIdempotentPut.bend deleted file mode 100644 index 92b6e15..0000000 --- a/proofs/MethodIdempotentPut.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodIdempotentPut.bend -import Base -import ../laws/MethodIdempotentPut.bend as Laws - -def Laws.method_idempotent_put(): - {==} diff --git a/proofs/MethodSafeDelete.bend b/proofs/MethodSafeDelete.bend deleted file mode 100644 index 85dde17..0000000 --- a/proofs/MethodSafeDelete.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodSafeDelete.bend -import Base -import ../laws/MethodSafeDelete.bend as Laws - -def Laws.method_safe_delete(): - {==} diff --git a/proofs/MethodSafeGet.bend b/proofs/MethodSafeGet.bend deleted file mode 100644 index 51751a9..0000000 --- a/proofs/MethodSafeGet.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodSafeGet.bend -import Base -import ../laws/MethodSafeGet.bend as Laws - -def Laws.method_safe_get(): - {==} diff --git a/proofs/MethodSafeGetLower.bend b/proofs/MethodSafeGetLower.bend deleted file mode 100644 index 3f9d0d4..0000000 --- a/proofs/MethodSafeGetLower.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodSafeGetLower.bend -import Base -import ../laws/MethodSafeGetLower.bend as Laws - -def Laws.method_safe_get_lower(): - {==} diff --git a/proofs/MethodSafeHead.bend b/proofs/MethodSafeHead.bend deleted file mode 100644 index aebfa3c..0000000 --- a/proofs/MethodSafeHead.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodSafeHead.bend -import Base -import ../laws/MethodSafeHead.bend as Laws - -def Laws.method_safe_head(): - {==} diff --git a/proofs/MethodSafeOptions.bend b/proofs/MethodSafeOptions.bend deleted file mode 100644 index f8fe2f8..0000000 --- a/proofs/MethodSafeOptions.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodSafeOptions.bend -import Base -import ../laws/MethodSafeOptions.bend as Laws - -def Laws.method_safe_options(): - {==} diff --git a/proofs/MethodSafePost.bend b/proofs/MethodSafePost.bend deleted file mode 100644 index d4e37c2..0000000 --- a/proofs/MethodSafePost.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodSafePost.bend -import Base -import ../laws/MethodSafePost.bend as Laws - -def Laws.method_safe_post(): - {==} diff --git a/proofs/MethodSafePut.bend b/proofs/MethodSafePut.bend deleted file mode 100644 index a3369b4..0000000 --- a/proofs/MethodSafePut.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/MethodSafePut.bend -import Base -import ../laws/MethodSafePut.bend as Laws - -def Laws.method_safe_put(): - {==} 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/FreshNoCacheAny.bend b/proofs/NoCacheNeverFresh.bend similarity index 73% rename from proofs/FreshNoCacheAny.bend rename to proofs/NoCacheNeverFresh.bend index bbeddcc..f9a38df 100644 --- a/proofs/FreshNoCacheAny.bend +++ b/proofs/NoCacheNeverFresh.bend @@ -1,6 +1,6 @@ -# proofs of laws/FreshNoCacheAny.bend +# proofs of laws/NoCacheNeverFresh.bend import Base -import ../laws/FreshNoCacheAny.bend as Laws +import ../laws/NoCacheNeverFresh.bend as Laws # fresh_no_cache_any: no-store either way, then no-cache def Laws.fresh_no_cache_any( 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/FreshNoStoreAny.bend b/proofs/NoStoreNeverFresh.bend similarity index 66% rename from proofs/FreshNoStoreAny.bend rename to proofs/NoStoreNeverFresh.bend index 3265d9f..624a85e 100644 --- a/proofs/FreshNoStoreAny.bend +++ b/proofs/NoStoreNeverFresh.bend @@ -1,6 +1,6 @@ -# proofs of laws/FreshNoStoreAny.bend +# proofs of laws/NoStoreNeverFresh.bend import Base -import ../laws/FreshNoStoreAny.bend as Laws +import ../laws/NoStoreNeverFresh.bend as Laws # fresh_no_store_any: fresh matches no-store first def Laws.fresh_no_store_any( 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/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/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/OptionsReqNamesPath.bend b/proofs/OptionsRequestLine.bend similarity index 74% rename from proofs/OptionsReqNamesPath.bend rename to proofs/OptionsRequestLine.bend index 3f16fed..0cda2d4 100644 --- a/proofs/OptionsReqNamesPath.bend +++ b/proofs/OptionsRequestLine.bend @@ -1,7 +1,7 @@ -# proofs of laws/OptionsReqNamesPath.bend +# proofs of laws/OptionsRequestLine.bend import Base import ../src/http.bend as Http -import ../laws/OptionsReqNamesPath.bend as Laws +import ../laws/OptionsRequestLine.bend as Laws import ./lemmas/BaseEqualities.bend as M1 def Laws.options_req_names_path(host, path, hs): 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/DfltPlain.bend b/proofs/OtherSchemesDefaultTo80.bend similarity index 50% rename from proofs/DfltPlain.bend rename to proofs/OtherSchemesDefaultTo80.bend index da65893..61c782f 100644 --- a/proofs/DfltPlain.bend +++ b/proofs/OtherSchemesDefaultTo80.bend @@ -1,6 +1,6 @@ -# proofs of laws/DfltPlain.bend +# proofs of laws/OtherSchemesDefaultTo80.bend import Base -import ../laws/DfltPlain.bend as Laws +import ../laws/OtherSchemesDefaultTo80.bend as Laws def Laws.dflt_plain(_scheme, e): %e : {Bool.pick(U32, _, 443, 80) == 80 : U32} diff --git a/proofs/ParseChunkedBody.bend b/proofs/ParseChunkedBody.bend deleted file mode 100644 index c15274f..0000000 --- a/proofs/ParseChunkedBody.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/ParseChunkedBody.bend -import Base -import ../laws/ParseChunkedBody.bend as Laws - -def Laws.parse_chunked_body(): - {==} diff --git a/proofs/ParseChunkedIgnoresCase.bend b/proofs/ParseChunkedIgnoresCase.bend deleted file mode 100644 index e0f9a02..0000000 --- a/proofs/ParseChunkedIgnoresCase.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/ParseChunkedIgnoresCase.bend -import Base -import ../laws/ParseChunkedIgnoresCase.bend as Laws - -def Laws.parse_chunked_ignores_case(): - {==} diff --git a/proofs/ParseClenTakesPrefix.bend b/proofs/ParseClenTakesPrefix.bend deleted file mode 100644 index da399cf..0000000 --- a/proofs/ParseClenTakesPrefix.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/ParseClenTakesPrefix.bend -import Base -import ../laws/ParseClenTakesPrefix.bend as Laws - -def Laws.parse_clen_takes_prefix(): - {==} diff --git a/proofs/ParseEqualContentLengths.bend b/proofs/ParseEqualContentLengths.bend deleted file mode 100644 index 2b4f815..0000000 --- a/proofs/ParseEqualContentLengths.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/ParseEqualContentLengths.bend -import Base -import ../laws/ParseEqualContentLengths.bend as Laws - -def Laws.parse_equal_content_lengths(): - {==} diff --git a/proofs/ParseRejectsClAndTe.bend b/proofs/ParseRejectsClAndTe.bend deleted file mode 100644 index a4523f5..0000000 --- a/proofs/ParseRejectsClAndTe.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/ParseRejectsClAndTe.bend -import Base -import ../laws/ParseRejectsClAndTe.bend as Laws - -def Laws.parse_rejects_cl_and_te(): - {==} diff --git a/proofs/ParseRejectsDisagreeingCl.bend b/proofs/ParseRejectsDisagreeingCl.bend deleted file mode 100644 index 363887e..0000000 --- a/proofs/ParseRejectsDisagreeingCl.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/ParseRejectsDisagreeingCl.bend -import Base -import ../laws/ParseRejectsDisagreeingCl.bend as Laws - -def Laws.parse_rejects_disagreeing_cl(): - {==} diff --git a/proofs/ParseRejectsNonHexChunk.bend b/proofs/ParseRejectsNonHexChunk.bend deleted file mode 100644 index c05e393..0000000 --- a/proofs/ParseRejectsNonHexChunk.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/ParseRejectsNonHexChunk.bend -import Base -import ../laws/ParseRejectsNonHexChunk.bend as Laws - -def Laws.parse_rejects_non_hex_chunk(): - {==} 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/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/PostReqNamesPath.bend b/proofs/PostRequestLine.bend similarity index 78% rename from proofs/PostReqNamesPath.bend rename to proofs/PostRequestLine.bend index 4f4cc18..134dab0 100644 --- a/proofs/PostReqNamesPath.bend +++ b/proofs/PostRequestLine.bend @@ -1,7 +1,7 @@ -# proofs of laws/PostReqNamesPath.bend +# proofs of laws/PostRequestLine.bend import Base import ../src/http.bend as Http -import ../laws/PostReqNamesPath.bend as Laws +import ../laws/PostRequestLine.bend as Laws import ./lemmas/BaseEqualities.bend as M1 def Laws.post_req_names_path(host, path, hs, body): 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/PutReqNamesPath.bend b/proofs/PutRequestLine.bend similarity index 78% rename from proofs/PutReqNamesPath.bend rename to proofs/PutRequestLine.bend index 885ccb2..900da85 100644 --- a/proofs/PutReqNamesPath.bend +++ b/proofs/PutRequestLine.bend @@ -1,7 +1,7 @@ -# proofs of laws/PutReqNamesPath.bend +# proofs of laws/PutRequestLine.bend import Base import ../src/http.bend as Http -import ../laws/PutReqNamesPath.bend as Laws +import ../laws/PutRequestLine.bend as Laws import ./lemmas/BaseEqualities.bend as M1 def Laws.put_req_names_path(host, path, hs, body): diff --git a/proofs/Reason200.bend b/proofs/Reason200.bend deleted file mode 100644 index 61fc9fa..0000000 --- a/proofs/Reason200.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/Reason200.bend -import Base -import ../laws/Reason200.bend as Laws - -def Laws.reason_200(): - {==} 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/Reason201.bend b/proofs/Reason201.bend deleted file mode 100644 index 3e397ce..0000000 --- a/proofs/Reason201.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/Reason201.bend -import Base -import ../laws/Reason201.bend as Laws - -def Laws.reason_201(): - {==} 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/Reason204.bend b/proofs/Reason204.bend deleted file mode 100644 index 2091555..0000000 --- a/proofs/Reason204.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/Reason204.bend -import Base -import ../laws/Reason204.bend as Laws - -def Laws.reason_204(): - {==} 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/Reason400.bend b/proofs/Reason400.bend deleted file mode 100644 index 908cd54..0000000 --- a/proofs/Reason400.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/Reason400.bend -import Base -import ../laws/Reason400.bend as Laws - -def Laws.reason_400(): - {==} 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/Reason404.bend b/proofs/Reason404.bend deleted file mode 100644 index 0e50812..0000000 --- a/proofs/Reason404.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/Reason404.bend -import Base -import ../laws/Reason404.bend as Laws - -def Laws.reason_404(): - {==} 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/Reason405.bend b/proofs/Reason405.bend deleted file mode 100644 index cc2694c..0000000 --- a/proofs/Reason405.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/Reason405.bend -import Base -import ../laws/Reason405.bend as Laws - -def Laws.reason_405(): - {==} 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/Reason500.bend b/proofs/Reason500.bend deleted file mode 100644 index db9c54e..0000000 --- a/proofs/Reason500.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/Reason500.bend -import Base -import ../laws/Reason500.bend as Laws - -def Laws.reason_500(): - {==} 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/ReasonUnknownEmpty.bend b/proofs/ReasonUnknownEmpty.bend deleted file mode 100644 index d3ec62a..0000000 --- a/proofs/ReasonUnknownEmpty.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/ReasonUnknownEmpty.bend -import Base -import ../laws/ReasonUnknownEmpty.bend as Laws - -def Laws.reason_unknown_empty(): - {==} 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/AskClenExact.bend b/proofs/RequestBodyOfContentLengthKept.bend similarity index 86% rename from proofs/AskClenExact.bend rename to proofs/RequestBodyOfContentLengthKept.bend index f68f4a7..26b6393 100644 --- a/proofs/AskClenExact.bend +++ b/proofs/RequestBodyOfContentLengthKept.bend @@ -1,7 +1,7 @@ -# proofs of laws/AskClenExact.bend +# proofs of laws/RequestBodyOfContentLengthKept.bend import Base import ../src/http.bend as Http -import ../laws/AskClenExact.bend as Laws +import ../laws/RequestBodyOfContentLengthKept.bend as Laws import ./lemmas/NatOrder.bend as M1 def Laws.ask_clen_exact(method, target, hs, 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/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/RequestNamesMethod.bend b/proofs/RequestLineOpensWithMethod.bend similarity index 74% rename from proofs/RequestNamesMethod.bend rename to proofs/RequestLineOpensWithMethod.bend index 6e82c28..abd89c8 100644 --- a/proofs/RequestNamesMethod.bend +++ b/proofs/RequestLineOpensWithMethod.bend @@ -1,7 +1,7 @@ -# proofs of laws/RequestNamesMethod.bend +# proofs of laws/RequestLineOpensWithMethod.bend import Base import ../src/http.bend as Http -import ../laws/RequestNamesMethod.bend as Laws +import ../laws/RequestLineOpensWithMethod.bend as Laws import ./lemmas/BaseEqualities.bend as M1 def Laws.request_names_method(method, host, path, hs, body): diff --git a/proofs/AskNeedsBlankLine.bend b/proofs/RequestNeedsBlankLine.bend similarity index 67% rename from proofs/AskNeedsBlankLine.bend rename to proofs/RequestNeedsBlankLine.bend index ee7e13f..8dfc335 100644 --- a/proofs/AskNeedsBlankLine.bend +++ b/proofs/RequestNeedsBlankLine.bend @@ -1,7 +1,7 @@ -# proofs of laws/AskNeedsBlankLine.bend +# proofs of laws/RequestNeedsBlankLine.bend import Base import ../src/http.bend as Http -import ../laws/AskNeedsBlankLine.bend as Laws +import ../laws/RequestNeedsBlankLine.bend as Laws def Laws.ask_needs_blank_line(_raw, e): %e : {Http.ask.cut(_) 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/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/ParseNeedsBlankLine.bend b/proofs/ResponseNeedsBlankLine.bend similarity index 67% rename from proofs/ParseNeedsBlankLine.bend rename to proofs/ResponseNeedsBlankLine.bend index e77d2cd..e055fd3 100644 --- a/proofs/ParseNeedsBlankLine.bend +++ b/proofs/ResponseNeedsBlankLine.bend @@ -1,7 +1,7 @@ -# proofs of laws/ParseNeedsBlankLine.bend +# proofs of laws/ResponseNeedsBlankLine.bend import Base import ../src/http.bend as Http -import ../laws/ParseNeedsBlankLine.bend as Laws +import ../laws/ResponseNeedsBlankLine.bend as Laws def Laws.parse_needs_blank_line(_raw, e): %e : {Http.parse.cut(_) diff --git a/proofs/ParseNeedsStatus.bend b/proofs/ResponseNeedsStatus.bend similarity index 69% rename from proofs/ParseNeedsStatus.bend rename to proofs/ResponseNeedsStatus.bend index 8c2bc1b..f386808 100644 --- a/proofs/ParseNeedsStatus.bend +++ b/proofs/ResponseNeedsStatus.bend @@ -1,7 +1,7 @@ -# proofs of laws/ParseNeedsStatus.bend +# proofs of laws/ResponseNeedsStatus.bend import Base import ../src/http.bend as Http -import ../laws/ParseNeedsStatus.bend as Laws +import ../laws/ResponseNeedsStatus.bend as Laws def Laws.parse_needs_status(ls, body, e): %e : {Http.parse.status(_, ls, body) 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/ParseSchemeFirst.bend b/proofs/SchemeBeforeFirstColon.bend similarity index 67% rename from proofs/ParseSchemeFirst.bend rename to proofs/SchemeBeforeFirstColon.bend index a4a8ba3..c967bb5 100644 --- a/proofs/ParseSchemeFirst.bend +++ b/proofs/SchemeBeforeFirstColon.bend @@ -1,7 +1,7 @@ -# proofs of laws/ParseSchemeFirst.bend +# proofs of laws/SchemeBeforeFirstColon.bend import Base import ../src/url.bend as Url -import ../laws/ParseSchemeFirst.bend as Laws +import ../laws/SchemeBeforeFirstColon.bend as Laws def Laws.parse_scheme_first(_url, h, t, e): %e : {Url.parse.scheme(_) 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/AskClenShort.bend b/proofs/ShortRequestBodyIsBad.bend similarity index 72% rename from proofs/AskClenShort.bend rename to proofs/ShortRequestBodyIsBad.bend index a78212f..11f0ed6 100644 --- a/proofs/AskClenShort.bend +++ b/proofs/ShortRequestBodyIsBad.bend @@ -1,7 +1,7 @@ -# proofs of laws/AskClenShort.bend +# proofs of laws/ShortRequestBodyIsBad.bend import Base import ../src/http.bend as Http -import ../laws/AskClenShort.bend as Laws +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, _) 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/Utf8Split.bend b/proofs/TakeDropSplitAtOneOffset.bend similarity index 90% rename from proofs/Utf8Split.bend rename to proofs/TakeDropSplitAtOneOffset.bend index 2077712..bff05d0 100644 --- a/proofs/Utf8Split.bend +++ b/proofs/TakeDropSplitAtOneOffset.bend @@ -1,7 +1,7 @@ -# proofs of laws/Utf8Split.bend +# proofs of laws/TakeDropSplitAtOneOffset.bend import Base import ../src/http.bend as Http -import ../laws/Utf8Split.bend as Laws +import ../laws/TakeDropSplitAtOneOffset.bend as Laws # LAW: one step of the UTF-8 partition law utf8_split.arm: diff --git a/proofs/TokenOkNewline.bend b/proofs/TokenOkNewline.bend deleted file mode 100644 index f50fcb7..0000000 --- a/proofs/TokenOkNewline.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/TokenOkNewline.bend -import Base -import ../laws/TokenOkNewline.bend as Laws - -def Laws.token_ok_newline(): - {==} diff --git a/proofs/TokenOkPlain.bend b/proofs/TokenOkPlain.bend deleted file mode 100644 index 9ba4f2a..0000000 --- a/proofs/TokenOkPlain.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/TokenOkPlain.bend -import Base -import ../laws/TokenOkPlain.bend as Laws - -def Laws.token_ok_plain(): - {==} diff --git a/proofs/TokenOkSpace.bend b/proofs/TokenOkSpace.bend deleted file mode 100644 index 81073f9..0000000 --- a/proofs/TokenOkSpace.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/TokenOkSpace.bend -import Base -import ../laws/TokenOkSpace.bend as Laws - -def Laws.token_ok_space(): - {==} diff --git a/proofs/TokenOkTab.bend b/proofs/TokenOkTab.bend deleted file mode 100644 index bac4429..0000000 --- a/proofs/TokenOkTab.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/TokenOkTab.bend -import Base -import ../laws/TokenOkTab.bend as Laws - -def Laws.token_ok_tab(): - {==} 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/UnframedIsRest.bend b/proofs/UnframedIsRest.bend deleted file mode 100644 index f2c3298..0000000 --- a/proofs/UnframedIsRest.bend +++ /dev/null @@ -1,6 +0,0 @@ -# proofs of laws/UnframedIsRest.bend -import Base -import ../laws/UnframedIsRest.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/ParseNeedsSlashes.bend b/proofs/UrlNeedsSlashes.bend similarity index 70% rename from proofs/ParseNeedsSlashes.bend rename to proofs/UrlNeedsSlashes.bend index a32b677..231a41a 100644 --- a/proofs/ParseNeedsSlashes.bend +++ b/proofs/UrlNeedsSlashes.bend @@ -1,7 +1,7 @@ -# proofs of laws/ParseNeedsSlashes.bend +# proofs of laws/UrlNeedsSlashes.bend import Base import ../src/url.bend as Url -import ../laws/ParseNeedsSlashes.bend as Laws +import ../laws/UrlNeedsSlashes.bend as Laws def Laws.parse_needs_slashes(scheme, rest, e): %e : {Url.parse.body(scheme, rest, _) diff --git a/proofs/PathAbsolute.bend b/proofs/UrlPathIsAbsolute.bend similarity index 67% rename from proofs/PathAbsolute.bend rename to proofs/UrlPathIsAbsolute.bend index 65dce93..a32ec02 100644 --- a/proofs/PathAbsolute.bend +++ b/proofs/UrlPathIsAbsolute.bend @@ -1,6 +1,6 @@ -# proofs of laws/PathAbsolute.bend +# proofs of laws/UrlPathIsAbsolute.bend import Base -import ../laws/PathAbsolute.bend as Laws +import ../laws/UrlPathIsAbsolute.bend as Laws import ./lemmas/BaseEqualities.bend as M1 def Laws.path_absolute(ps): diff --git a/src/entities.bend b/src/entities.bend index f2e57c1..e017ed7 100644 --- a/src/entities.bend +++ b/src/entities.bend @@ -4,12 +4,13 @@ # # 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. It is data transcribed from another -# document (the same table as Python's `html.entities.html5`) by -# tools/entities.py, which no law reaches: a law can say how the table is -# read (laws/NamedReferencesLongestMatch.bend), not that its rows are the -# standard's. The differential test against `html.unescape` reads every name -# with and without its `;`. +# 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/tools/entities.py b/tools/entities.py index b83c650..6c5df77 100644 --- a/tools/entities.py +++ b/tools/entities.py @@ -36,12 +36,13 @@ def main(path): "#\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. It is data transcribed from another\n" - "# document (the same table as Python's `html.entities.html5`) by\n" - "# tools/entities.py, which no law reaches: a law can say how the table is\n" - "# read (laws/NamedReferencesLongestMatch.bend), not that its rows are the\n" - "# standard's. The differential test against `html.unescape` reads every name\n" - "# with and without its `;`.\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, " From 67665e0c525abe1b496875afa8f231ade861fe12 Mon Sep 17 00:00:00 2001 From: Noah Gardner Date: Sat, 31 Oct 2026 23:39:18 -0400 Subject: [PATCH 3/3] build: bolt v1.22.0; drop the noqa comments it finds silence nothing bolt v1.22.0 reads law and proof files by what LAWS.bend and PROOF.bend import, which this layout needs. Rules changed since the old pin, so some noqa comments no longer silence anything (S005); they are removed. Co-Authored-By: Claude Opus 5.5 --- bench/main.bend | 6 +++--- ez.lock.toml | 6 +++--- ez.toml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bench/main.bend b/bench/main.bend index 807b53d..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 @@ -636,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/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"