diff --git a/.ci-scripts/release/arm64-apple-darwin-nightly.bash b/.ci-scripts/release/arm64-apple-darwin-nightly.bash index 6954821..d413950 100644 --- a/.ci-scripts/release/arm64-apple-darwin-nightly.bash +++ b/.ci-scripts/release/arm64-apple-darwin-nightly.bash @@ -88,7 +88,7 @@ ASSET_DESCRIPTION="https://github.com/${GITHUB_REPOSITORY}" # Build application installation echo -e "\e[34mBuilding ${APPLICATION_NAME}...\e[0m" make install prefix="${BUILD_DIR}" arch=${CPU} \ - version="${APPLICATION_VERSION}" + version="${APPLICATION_VERSION}" ssl=libressl # Package it all up echo -e "\e[34mCreating .tar.gz of ${APPLICATION_NAME}...\e[0m" diff --git a/.ci-scripts/release/arm64-apple-darwin-release.bash b/.ci-scripts/release/arm64-apple-darwin-release.bash index 9f51282..2a9671b 100644 --- a/.ci-scripts/release/arm64-apple-darwin-release.bash +++ b/.ci-scripts/release/arm64-apple-darwin-release.bash @@ -86,7 +86,7 @@ ASSET_DESCRIPTION="https://github.com/${GITHUB_REPOSITORY}" # Build application installation echo -e "\e[34mBuilding ${APPLICATION_NAME}...\e[0m" make install prefix="${BUILD_DIR}" arch=${CPU} \ - version="${APPLICATION_VERSION}" + version="${APPLICATION_VERSION}" ssl=libressl # Package it all up echo -e "\e[34mCreating .tar.gz of ${APPLICATION_NAME}...\e[0m" diff --git a/.ci-scripts/release/arm64-unknown-linux-nightly.bash b/.ci-scripts/release/arm64-unknown-linux-nightly.bash index 4ab12f7..212dce4 100755 --- a/.ci-scripts/release/arm64-unknown-linux-nightly.bash +++ b/.ci-scripts/release/arm64-unknown-linux-nightly.bash @@ -88,7 +88,7 @@ ASSET_DESCRIPTION="https://github.com/${GITHUB_REPOSITORY}" # Build application installation echo -e "\e[34mBuilding ${APPLICATION_NAME}...\e[0m" make install prefix="${BUILD_DIR}" arch=${CPU} \ - version="${APPLICATION_VERSION}" static=true linker=bfd + version="${APPLICATION_VERSION}" static=true linker=bfd ssl=libressl # Package it all up echo -e "\e[34mCreating .tar.gz of ${APPLICATION_NAME}...\e[0m" diff --git a/.ci-scripts/release/arm64-unknown-linux-release.bash b/.ci-scripts/release/arm64-unknown-linux-release.bash index 3e06b70..6fbd14a 100755 --- a/.ci-scripts/release/arm64-unknown-linux-release.bash +++ b/.ci-scripts/release/arm64-unknown-linux-release.bash @@ -86,7 +86,7 @@ ASSET_DESCRIPTION="https://github.com/${GITHUB_REPOSITORY}" # Build application installation echo -e "\e[34mBuilding ${APPLICATION_NAME}...\e[0m" make install prefix="${BUILD_DIR}" arch=${CPU} \ - version="${APPLICATION_VERSION}" static=true linker=bfd + version="${APPLICATION_VERSION}" static=true linker=bfd ssl=libressl # Package it all up echo -e "\e[34mCreating .tar.gz of ${APPLICATION_NAME}...\e[0m" diff --git a/.ci-scripts/release/x86-64-apple-darwin-nightly.bash b/.ci-scripts/release/x86-64-apple-darwin-nightly.bash index 44568a9..08f1317 100644 --- a/.ci-scripts/release/x86-64-apple-darwin-nightly.bash +++ b/.ci-scripts/release/x86-64-apple-darwin-nightly.bash @@ -85,7 +85,7 @@ ASSET_DESCRIPTION="https://github.com/${GITHUB_REPOSITORY}" # Build application installation echo -e "\e[34mBuilding ${APPLICATION_NAME}...\e[0m" make install prefix="${BUILD_DIR}" arch=${ARCH} \ - version="${APPLICATION_VERSION}" + version="${APPLICATION_VERSION}" ssl=libressl # Package it all up echo -e "\e[34mCreating .tar.gz of ${APPLICATION_NAME}...\e[0m" diff --git a/.ci-scripts/release/x86-64-apple-darwin-release.bash b/.ci-scripts/release/x86-64-apple-darwin-release.bash index d115092..8730d75 100644 --- a/.ci-scripts/release/x86-64-apple-darwin-release.bash +++ b/.ci-scripts/release/x86-64-apple-darwin-release.bash @@ -83,7 +83,7 @@ ASSET_DESCRIPTION="https://github.com/${GITHUB_REPOSITORY}" # Build application installation echo -e "\e[34mBuilding ${APPLICATION_NAME}...\e[0m" make install prefix="${BUILD_DIR}" arch=${ARCH} \ - version="${APPLICATION_VERSION}" + version="${APPLICATION_VERSION}" ssl=libressl # Package it all up echo -e "\e[34mCreating .tar.gz of ${APPLICATION_NAME}...\e[0m" diff --git a/.ci-scripts/release/x86-64-unknown-linux-nightly.bash b/.ci-scripts/release/x86-64-unknown-linux-nightly.bash index 166cd33..3e95e0f 100755 --- a/.ci-scripts/release/x86-64-unknown-linux-nightly.bash +++ b/.ci-scripts/release/x86-64-unknown-linux-nightly.bash @@ -87,7 +87,7 @@ ASSET_DESCRIPTION="https://github.com/${GITHUB_REPOSITORY}" # Build application installation echo -e "\e[34mBuilding ${APPLICATION_NAME}...\e[0m" make install prefix="${BUILD_DIR}" arch=${ARCH} \ - version="${APPLICATION_VERSION}" static=true linker=bfd + version="${APPLICATION_VERSION}" static=true linker=bfd ssl=libressl # Package it all up echo -e "\e[34mCreating .tar.gz of ${APPLICATION_NAME}...\e[0m" diff --git a/.ci-scripts/release/x86-64-unknown-linux-release.bash b/.ci-scripts/release/x86-64-unknown-linux-release.bash index c276fd4..50db943 100755 --- a/.ci-scripts/release/x86-64-unknown-linux-release.bash +++ b/.ci-scripts/release/x86-64-unknown-linux-release.bash @@ -85,7 +85,7 @@ ASSET_DESCRIPTION="https://github.com/${GITHUB_REPOSITORY}" # Build application installation echo -e "\e[34mBuilding ${APPLICATION_NAME}...\e[0m" make install prefix="${BUILD_DIR}" arch=${ARCH} \ - version="${APPLICATION_VERSION}" static=true linker=bfd + version="${APPLICATION_VERSION}" static=true linker=bfd ssl=libressl # Package it all up echo -e "\e[34mCreating .tar.gz of ${APPLICATION_NAME}...\e[0m" diff --git a/.ci-scripts/windows-install-libressl.ps1 b/.ci-scripts/windows-install-libressl.ps1 new file mode 100644 index 0000000..b10ed5c --- /dev/null +++ b/.ci-scripts/windows-install-libressl.ps1 @@ -0,0 +1,19 @@ +$version = "3.9.1" +$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture +if ($arch -ieq "X64") { $zipArch = "x64" } +elseif ($arch -ieq "Arm64") { $zipArch = "ARM64" } +else { throw "Unsupported architecture: $arch" } + +$zipName = "libressl_v${version}_windows_${zipArch}.zip" +$url = "https://github.com/libressl/portable/releases/download/v${version}/${zipName}" + +$tempDir = Join-Path $env:TEMP "libressl" +if (-not (Test-Path $tempDir)) { New-Item -ItemType Directory -Path $tempDir | Out-Null } + +Invoke-WebRequest $url -OutFile "$tempDir\$zipName" +Expand-Archive -Force -Path "$tempDir\$zipName" -DestinationPath "$tempDir\libressl" + +$repoRoot = Split-Path $PSScriptRoot +Copy-Item -Force "$tempDir\libressl\lib\ssl.lib" "$repoRoot\ssl.lib" +Copy-Item -Force "$tempDir\libressl\lib\crypto.lib" "$repoRoot\crypto.lib" +Copy-Item -Force "$tempDir\libressl\lib\tls.lib" "$repoRoot\tls.lib" diff --git a/.github/workflows/breakage-against-linux-ponyc-latest.yml b/.github/workflows/breakage-against-linux-ponyc-latest.yml index 8d6bac9..3b38a65 100644 --- a/.github/workflows/breakage-against-linux-ponyc-latest.yml +++ b/.github/workflows/breakage-against-linux-ponyc-latest.yml @@ -13,11 +13,11 @@ jobs: name: Verify main against the latest ponyc on Linux runs-on: ubuntu-latest container: - image: ghcr.io/ponylang/shared-docker-ci-standard-builder:nightly + image: ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:nightly steps: - uses: actions/checkout@v6.0.2 - name: Test - run: make unit-tests config=debug + run: make unit-tests config=debug ssl=libressl - name: Send alert on failure if: ${{ failure() }} uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 diff --git a/.github/workflows/breakage-against-macos-arm64-ponyc-latest.yml b/.github/workflows/breakage-against-macos-arm64-ponyc-latest.yml index 95e7f7a..c49da9f 100644 --- a/.github/workflows/breakage-against-macos-arm64-ponyc-latest.yml +++ b/.github/workflows/breakage-against-macos-arm64-ponyc-latest.yml @@ -17,10 +17,13 @@ jobs: run: bash .ci-scripts/macos-arm64-install-pony-tools.bash nightly - name: brew install dependencies run: brew install coreutils + - name: Install LibreSSL + continue-on-error: true + run: brew install libressl - name: Test with most recent ponyc release run: | export PATH="/tmp/ponyc/bin/:$PATH" - make unit-tests config=debug + make unit-tests config=debug ssl=libressl - name: Send alert on failure if: ${{ failure() }} uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 diff --git a/.github/workflows/breakage-against-macos-x86-ponyc-latest.yml b/.github/workflows/breakage-against-macos-x86-ponyc-latest.yml index 2346fed..ce4521d 100644 --- a/.github/workflows/breakage-against-macos-x86-ponyc-latest.yml +++ b/.github/workflows/breakage-against-macos-x86-ponyc-latest.yml @@ -17,10 +17,13 @@ jobs: run: bash .ci-scripts/macos-x86-install-pony-tools.bash nightly - name: brew install dependencies run: brew install coreutils + - name: Install LibreSSL + continue-on-error: true + run: brew install libressl - name: Test with most recent ponyc release run: | export PATH="/tmp/ponyc/bin/:$PATH" - make unit-tests config=debug + make unit-tests config=debug ssl=libressl - name: Send alert on failure if: ${{ failure() }} uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5 diff --git a/.github/workflows/breakage-against-windows-ponyc-latest.yml b/.github/workflows/breakage-against-windows-ponyc-latest.yml index 7e43dce..d0c9e82 100644 --- a/.github/workflows/breakage-against-windows-ponyc-latest.yml +++ b/.github/workflows/breakage-against-windows-ponyc-latest.yml @@ -15,6 +15,8 @@ jobs: - name: Disable Windows Defender run: Set-MpPreference -DisableRealtimeMonitoring $true - uses: actions/checkout@v6.0.2 + - name: Install LibreSSL + run: .ci-scripts\windows-install-libressl.ps1 - name: Test with most recent ponyc nightly run: | Invoke-WebRequest https://dl.cloudsmith.io/public/ponylang/nightlies/raw/versions/latest/ponyc-x86-64-pc-windows-msvc.zip -OutFile C:\ponyc.zip; diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index ee80383..7c77be0 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -24,7 +24,7 @@ jobs: - name: Checkout uses: actions/checkout@v6.0.2 - name: Pull Docker image - run: docker pull ghcr.io/ponylang/shared-docker-ci-standard-builder:release + run: docker pull ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:release - name: Build and upload run: | docker run --rm \ @@ -32,7 +32,7 @@ jobs: -w /root/project \ -e CLOUDSMITH_API_KEY=${{ secrets.CLOUDSMITH_API_KEY }} \ -e GITHUB_REPOSITORY=${{ github.repository }} \ - ghcr.io/ponylang/shared-docker-ci-standard-builder:release \ + ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:release \ bash .ci-scripts/release/arm64-unknown-linux-nightly.bash - name: Send alert on failure if: ${{ failure() }} @@ -50,7 +50,7 @@ jobs: name: Build and upload x86-64-unknown-linux-nightly to Cloudsmith runs-on: ubuntu-latest container: - image: ghcr.io/ponylang/shared-docker-ci-standard-builder:release + image: ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:release steps: - uses: actions/checkout@v6.0.2 - name: Build and upload @@ -82,6 +82,7 @@ jobs: Invoke-WebRequest https://dl.cloudsmith.io/public/ponylang/releases/raw/versions/latest/ponyc-x86-64-pc-windows-msvc.zip -OutFile C:\ponyc.zip; Expand-Archive -Path C:\ponyc.zip -DestinationPath C:\ponyc; $env:PATH = 'C:\ponyc\bin;' + $env:PATH; + .ci-scripts\windows-install-libressl.ps1; .\make.ps1 -Command build; .\make.ps1 -Command test; .\make.ps1 -Command install; @@ -114,6 +115,7 @@ jobs: Invoke-WebRequest https://dl.cloudsmith.io/public/ponylang/releases/raw/versions/latest/ponyc-arm64-pc-windows-msvc.zip -OutFile C:\ponyc.zip; Expand-Archive -Path C:\ponyc.zip -DestinationPath C:\ponyc; $env:PATH = 'C:\ponyc\bin;' + $env:PATH; + .ci-scripts\windows-install-libressl.ps1; .\make.ps1 -Command build; .\make.ps1 -Command test; .\make.ps1 -Command install; @@ -142,6 +144,9 @@ jobs: run: bash .ci-scripts/macos-x86-install-pony-tools.bash release - name: brew install dependencies run: brew install coreutils + - name: Install LibreSSL + continue-on-error: true + run: brew install libressl - name: pip install dependencies run: pip3 install --break-system-packages --upgrade cloudsmith-cli - name: Build and upload @@ -171,6 +176,9 @@ jobs: run: bash .ci-scripts/macos-arm64-install-pony-tools.bash release - name: brew install dependencies run: brew install coreutils + - name: Install LibreSSL + continue-on-error: true + run: brew install libressl - name: pip install dependencies run: pip3 install --upgrade --break-system-packages cloudsmith-cli - name: Build and upload diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8e62b5f..e996b9d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -21,11 +21,11 @@ jobs: name: x86_64 Linux runs-on: ubuntu-latest container: - image: ghcr.io/ponylang/shared-docker-ci-standard-builder:release + image: ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:release steps: - uses: actions/checkout@v6.0.2 - name: Test with most recent ponyc release - run: make test + run: make test ssl=libressl # Currently, GitHub actions supplied by GH like checkout and cache do not work # in musl libc environments on arm64. We can work around this by running @@ -43,14 +43,14 @@ jobs: - name: Checkout uses: actions/checkout@v6.0.2 - name: Pull Docker image - run: docker pull ghcr.io/ponylang/shared-docker-ci-standard-builder:release + run: docker pull ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:release - name: Test with most recent ponyc release run: | docker run --rm \ -v ${{ github.workspace }}:/root/project \ -w /root/project \ - ghcr.io/ponylang/shared-docker-ci-standard-builder:release \ - make test + ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:release \ + make test ssl=libressl x86_windows: name: x86_64 Windows @@ -64,6 +64,7 @@ jobs: Invoke-WebRequest https://dl.cloudsmith.io/public/ponylang/releases/raw/versions/latest/ponyc-x86-64-pc-windows-msvc.zip -OutFile C:\ponyc.zip; Expand-Archive -Path C:\ponyc.zip -DestinationPath C:\ponyc; $env:PATH = 'C:\ponyc\bin;' + $env:PATH; + .ci-scripts\windows-install-libressl.ps1; .\make.ps1 -Command test 2>&1 arm64_windows: @@ -78,6 +79,7 @@ jobs: Invoke-WebRequest https://dl.cloudsmith.io/public/ponylang/releases/raw/versions/latest/ponyc-arm64-pc-windows-msvc.zip -OutFile C:\ponyc.zip; Expand-Archive -Path C:\ponyc.zip -DestinationPath C:\ponyc; $env:PATH = 'C:\ponyc\bin;' + $env:PATH; + .ci-scripts\windows-install-libressl.ps1; .\make.ps1 -Command test 2>&1 x86_macos: @@ -89,10 +91,13 @@ jobs: run: bash .ci-scripts/macos-x86-install-pony-tools.bash release - name: brew install dependencies run: brew install coreutils + - name: Install LibreSSL + continue-on-error: true + run: brew install libressl - name: Test with most recent ponyc release run: | export PATH="/tmp/ponyc/bin/:$PATH" - make test + make test ssl=libressl arm64_macos: name: arm64 MacOS @@ -103,7 +108,10 @@ jobs: run: bash .ci-scripts/macos-arm64-install-pony-tools.bash release - name: brew install dependencies run: brew install coreutils + - name: Install LibreSSL + continue-on-error: true + run: brew install libressl - name: Test with most recent ponyc release run: | export PATH="/tmp/ponyc/bin/:$PATH" - make test + make test ssl=libressl diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c367a9f..2f56359 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: - name: Checkout uses: actions/checkout@v6.0.2 - name: Pull Docker image - run: docker pull ghcr.io/ponylang/shared-docker-ci-standard-builder:release + run: docker pull ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:release - name: Build and upload run: | docker run --rm \ @@ -55,7 +55,7 @@ jobs: -w /root/project \ -e CLOUDSMITH_API_KEY=${{ secrets.CLOUDSMITH_API_KEY }} \ -e GITHUB_REPOSITORY=${{ github.repository }} \ - ghcr.io/ponylang/shared-docker-ci-standard-builder:release \ + ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:release \ bash .ci-scripts/release/arm64-unknown-linux-release.bash x86-64-unknown-linux-release: @@ -64,7 +64,7 @@ jobs: needs: - pre-artefact-creation container: - image: ghcr.io/ponylang/shared-docker-ci-standard-builder:release + image: ghcr.io/ponylang/shared-docker-ci-standard-builder-with-libressl-4.2.0:release steps: - uses: actions/checkout@v6.0.2 - name: Build and upload @@ -83,6 +83,9 @@ jobs: run: bash .ci-scripts/macos-x86-install-pony-tools.bash release - name: brew install dependencies run: brew install coreutils + - name: Install LibreSSL + continue-on-error: true + run: brew install libressl - name: pip install dependencies run: pip3 install --break-system-packages --upgrade cloudsmith-cli - name: Build and upload @@ -103,6 +106,9 @@ jobs: run: bash .ci-scripts/macos-arm64-install-pony-tools.bash release - name: brew install dependencies run: brew install coreutils + - name: Install LibreSSL + continue-on-error: true + run: brew install libressl - name: pip install dependencies run: pip3 install --upgrade --break-system-packages cloudsmith-cli - name: Build and upload @@ -127,6 +133,7 @@ jobs: Invoke-WebRequest https://dl.cloudsmith.io/public/ponylang/releases/raw/versions/latest/ponyc-x86-64-pc-windows-msvc.zip -OutFile C:\ponyc.zip; Expand-Archive -Path C:\ponyc.zip -DestinationPath C:\ponyc; $env:PATH = 'C:\ponyc\bin;' + $env:PATH; + .ci-scripts\windows-install-libressl.ps1; .\make.ps1 -Command build; .\make.ps1 -Command install; .\make.ps1 -Command package; @@ -149,6 +156,7 @@ jobs: Invoke-WebRequest https://dl.cloudsmith.io/public/ponylang/releases/raw/versions/latest/ponyc-arm64-pc-windows-msvc.zip -OutFile C:\ponyc.zip; Expand-Archive -Path C:\ponyc.zip -DestinationPath C:\ponyc; $env:PATH = 'C:\ponyc\bin;' + $env:PATH; + .ci-scripts\windows-install-libressl.ps1; .\make.ps1 -Command build; .\make.ps1 -Command install; .\make.ps1 -Command package; diff --git a/.gitignore b/.gitignore index fa4d11a..9f02a15 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ corral/version.pony **/_corral .vscode *.code-workspace +*.lib .claude CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md index 7364c69..59211a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,15 +5,17 @@ Pony dependency manager. Manages `corral.json` (deps/packages/info) and `lock.js ## Building and Testing ``` -make # build + test (unit + integration) -make unit-tests # unit tests only -make test-one t=TestName # run a single test by name -make integration # integration tests only (requires built binary) -make test # both unit and integration -make clean # remove build artifacts -make config=debug # debug build +make ssl=3.0.x # build + test (unit + integration) +make unit-tests ssl=3.0.x # unit tests only +make test-one ssl=3.0.x t=TestName # run a single test by name +make integration ssl=3.0.x # integration tests only (requires built binary) +make test ssl=3.0.x # both unit and integration +make clean # remove build artifacts (no ssl= needed) +make config=debug ssl=3.0.x # debug build ``` +The `ssl=` parameter is required for all targets except `clean`. Values: `3.0.x` (OpenSSL 3.x), `1.1.x` (OpenSSL 1.1), `libressl` (LibreSSL). CI uses `ssl=libressl`; use whichever matches your local installation. + Tests run via `ponyc` directly (no corral dependencies needed — it bootstraps itself). The Makefile generates `version.pony` from `version.pony.in` using the VERSION file. Integration tests use the `CORRAL_BIN` env var (set by Makefile) to locate the built binary. Unit tests run in-process. @@ -42,6 +44,14 @@ corral/ vcs.pony -- VCS interface, Repo class, RepoOperation interface vcs_builder.pony -- VCSBuilder interface + CorralVCSBuilder git.pony -- GitVCS (the only fully-featured one) + _vendor/ -- Vendored dependencies (ssl, lori, courier) + ssl/ -- SSL wrappers (crypto/, net/) + lori/ -- TCP networking + courier/ -- HTTP client + VERSIONS -- Pinned version info + git/ -- Pure Pony git internals + inflate/ -- RFC 1951 DEFLATE decompression + sha1/ -- SHA-1 hashing (wraps ssl/crypto) semver/ -- Semantic versioning (parsing, ranges, constraint solving) json/ -- Custom JSON handling (not stdlib) util/ -- Action (Program/Action/ActionResult/Runner), Copy, Log @@ -65,7 +75,7 @@ corral/ - **Unit tests** (`cmd/_test_cmd_update.pony`): Use fake VCS (`_RecordedVCS`) to verify operation counts (syncs, tag queries, checkouts) without network. Test data from `test/testdata/`. - **Integration tests** (`test/integration/`): Run the actual corral binary via `Execute` helper (uses `ProcessMonitor`). `DataClone` copies test fixtures to temp dirs. Tests use `h.long_test()` with 30s timeouts. -- **Test registration**: `test/_test.pony` is the test Main. Unit tests listed directly; cmd tests delegated via `cmd.Main.make().tests(test)`. +- **Test registration**: `test/_test.pony` is the test Main. Unit tests listed directly; cmd tests delegated via `cmd.Main.make().tests(test)`, git tests via `git.Main.make().tests(test)`. - **Naming**: Integration tests named `"integration/..."`, unit tests named `"cmd/update/..."` etc. `\nodoc\` annotation on test classes. ## Conventions @@ -77,3 +87,7 @@ corral/ - Error returns as union types: `(SuccessType | ErrorType)` rather than exceptions, except `?` for simple lookup failures. - `iso` bundles passed between actors via `consume`. - Capabilities: `Bundle` created as `iso`, consumed into `ref` by actors. `Context`, `Project`, `Locator`, `VCS`, `Repo`, `Action`, `Program` are all `val`. + +## Vendored Dependencies + +Corral vendors ssl, lori, and courier for the pure Pony git implementation. These live in `corral/_vendor/` and are compiled via `-p` flag in the Makefile. Version info is in `corral/_vendor/VERSIONS`. Do not modify vendored code directly -- update the upstream package and re-vendor. diff --git a/Makefile b/Makefile index 1cae07b..1441e00 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ config ?= release arch ?= static ?= false linker ?= +ssl ?= BUNDLE := corral @@ -46,6 +47,18 @@ ifneq ($(linker),) LINKER += --link-ldcmd=$(linker) endif +ifeq (,$(filter $(MAKECMDGOALS),clean)) + ifeq ($(ssl), 3.0.x) + SSL = -Dopenssl_3.0.x + else ifeq ($(ssl), 1.1.x) + SSL = -Dopenssl_1.1.x + else ifeq ($(ssl), libressl) + SSL = -Dlibressl + else + $(error Unknown SSL version "$(ssl)". Must set using 'ssl=FOO') + endif +endif + # Default to version from `VERSION` file but allowing overridding on the # make command line like: # make version="nightly-19710702" @@ -74,7 +87,7 @@ GEN_FILES = $(patsubst %.pony.in, %.pony, $(GEN_FILES_IN)) sed s/%%VERSION%%/$(version)/ $< > $@ $(binary): $(GEN_FILES) $(SOURCE_FILES) | $(BUILD_DIR) - ${PONYC} $(arch_arg) $(LINKER) $(SRC_DIR) -o ${BUILD_DIR} + ${PONYC} $(SSL) -p $(SRC_DIR)/_vendor $(arch_arg) $(LINKER) $(SRC_DIR) -o ${BUILD_DIR} install: $(binary) @echo "install" @@ -82,7 +95,7 @@ install: $(binary) cp $^ $(DESTDIR)$(prefix)/bin $(tests_binary): $(GEN_FILES) $(SOURCE_FILES) $(TEST_FILES) | $(BUILD_DIR) - ${PONYC} $(arch_arg) $(LINKER) --debug -o ${BUILD_DIR} $(SRC_DIR)/test + ${PONYC} $(SSL) -p $(SRC_DIR)/_vendor $(arch_arg) $(LINKER) --debug -o ${BUILD_DIR} $(SRC_DIR)/test unit-tests: $(tests_binary) $^ --exclude=integration @@ -101,7 +114,7 @@ clean: $(docs_dir): $(SOURCE_FILES) rm -rf $(docs_dir) - $(BUILD_DOCS_WITH) --output build $(SRC_DIR) + $(BUILD_DOCS_WITH) $(SSL) -p $(SRC_DIR)/_vendor --output build $(SRC_DIR) docs: $(docs_dir) diff --git a/corral.json b/corral.json index 7330677..0ff7a4a 100644 --- a/corral.json +++ b/corral.json @@ -4,6 +4,9 @@ "corral/archive", "corral/bundle", "corral/cmd", + "corral/git", + "corral/git/inflate", + "corral/git/sha1", "corral/json", "corral/mort", "corral/semver/range", diff --git a/corral/_vendor/VERSIONS b/corral/_vendor/VERSIONS new file mode 100644 index 0000000..b116100 --- /dev/null +++ b/corral/_vendor/VERSIONS @@ -0,0 +1,3 @@ +ssl 2.0.1 7208e18 +lori 0.12.0 4b61809 +courier 0.1.3 0e0453f diff --git a/corral/_vendor/courier/_connection_failure_reason.pony b/corral/_vendor/courier/_connection_failure_reason.pony new file mode 100644 index 0000000..67f8975 --- /dev/null +++ b/corral/_vendor/courier/_connection_failure_reason.pony @@ -0,0 +1,25 @@ +interface val _ConnectionFailureReason is Stringable + +primitive ConnectionFailedDNS is _ConnectionFailureReason + """ + DNS resolution failed. No TCP connection was attempted. + """ + fun string(): String iso^ => "ConnectionFailedDNS".clone() + +primitive ConnectionFailedTCP is _ConnectionFailureReason + """ + TCP connection failed after DNS resolution succeeded. + """ + fun string(): String iso^ => "ConnectionFailedTCP".clone() + +primitive ConnectionFailedSSL is _ConnectionFailureReason + """ + SSL handshake failed after TCP connection succeeded. + """ + fun string(): String iso^ => "ConnectionFailedSSL".clone() + +primitive ConnectionFailedTimeout is _ConnectionFailureReason + """ + Connection attempt timed out before completing. + """ + fun string(): String iso^ => "ConnectionFailedTimeout".clone() diff --git a/corral/_vendor/courier/_connection_state.pony b/corral/_vendor/courier/_connection_state.pony new file mode 100644 index 0000000..c37becd --- /dev/null +++ b/corral/_vendor/courier/_connection_state.pony @@ -0,0 +1,87 @@ +use lori = "lori" + +trait ref _ConnectionState + """ + Connection lifecycle state. + + Dispatches lori events to the appropriate client methods based on + what operations are valid in each state. Two states: `_Active` + (connection is open for requests and responses) and `_Closed` + (all operations are no-ops). + """ + + fun ref on_received(client: HTTPClientConnection ref, data: Array[U8] iso) + """ + Handle incoming data from the TCP connection. + """ + + fun ref on_closed(client: HTTPClientConnection ref) + """ + Handle connection close notification. + """ + + fun ref on_throttled(client: HTTPClientConnection ref) + """ + Handle backpressure applied notification. + """ + + fun ref on_unthrottled(client: HTTPClientConnection ref) + """ + Handle backpressure released notification. + """ + + fun ref on_idle_timeout(client: HTTPClientConnection ref) + """ + Handle connection going idle. + """ + + fun ref on_timer(client: HTTPClientConnection ref, token: lori.TimerToken) + """ + Handle one-shot timer firing. + """ + +class ref _Active is _ConnectionState + """ + Connection is active — sending requests and receiving responses. + """ + + fun ref on_received(client: HTTPClientConnection ref, data: Array[U8] iso) => + client._feed_parser(consume data) + + fun ref on_closed(client: HTTPClientConnection ref) => + client._handle_closed() + + fun ref on_throttled(client: HTTPClientConnection ref) => + client._handle_throttled() + + fun ref on_unthrottled(client: HTTPClientConnection ref) => + client._handle_unthrottled() + + fun ref on_idle_timeout(client: HTTPClientConnection ref) => + client._handle_idle_timeout() + + fun ref on_timer(client: HTTPClientConnection ref, token: lori.TimerToken) => + client._handle_timer(token) + +class ref _Closed is _ConnectionState + """ + Connection is closed — all operations are no-ops. + """ + + fun ref on_received(client: HTTPClientConnection ref, data: Array[U8] iso) => + None + + fun ref on_closed(client: HTTPClientConnection ref) => + None + + fun ref on_throttled(client: HTTPClientConnection ref) => + None + + fun ref on_unthrottled(client: HTTPClientConnection ref) => + None + + fun ref on_idle_timeout(client: HTTPClientConnection ref) => + None + + fun ref on_timer(client: HTTPClientConnection ref, token: lori.TimerToken) => + None diff --git a/corral/_vendor/courier/_mort.pony b/corral/_vendor/courier/_mort.pony new file mode 100644 index 0000000..8b9078d --- /dev/null +++ b/corral/_vendor/courier/_mort.pony @@ -0,0 +1,35 @@ +use @exit[None](status: I32) +use @fprintf[I32](stream: Pointer[None] tag, fmt: Pointer[U8] tag, ...) +use @pony_os_stderr[Pointer[None]]() + +primitive _IllegalState + """ + To be used to exit early if we called a function that shouldn't be possible + in our current state. + """ + fun apply(loc: SourceLoc = __loc) => + @fprintf( + @pony_os_stderr(), + ("An illegal state was encountered in %s at line %s\n" + + "Please open an issue at " + + "https://github.com/ponylang/courier/issues") + .cstring(), + loc.file().cstring(), + loc.line().string().cstring()) + @exit(1) + +primitive _Unreachable + """ + To be used in places that the compiler can't prove is unreachable but we are + certain is unreachable and if we reach it, we'd be silently hiding a bug. + """ + fun apply(loc: SourceLoc = __loc) => + @fprintf( + @pony_os_stderr(), + ("The unreachable was reached in %s at line %s\n" + + "Please open an issue at " + + "https://github.com/ponylang/courier/issues") + .cstring(), + loc.file().cstring(), + loc.line().string().cstring()) + @exit(1) diff --git a/corral/_vendor/courier/_multipart_part.pony b/corral/_vendor/courier/_multipart_part.pony new file mode 100644 index 0000000..68456c2 --- /dev/null +++ b/corral/_vendor/courier/_multipart_part.pony @@ -0,0 +1,33 @@ +class val _MultipartField + """ + A text field in a multipart form. + """ + let name: String + let value: String + + new val create(name': String, value': String) => + name = name' + value = value' + +class val _MultipartFile + """ + A file attachment in a multipart form. + """ + let name: String + let filename: String + let content_type: String + let data: Array[U8] val + + new val create( + name': String, + filename': String, + content_type': String, + data': Array[U8] val) + => + name = name' + filename = filename' + content_type = content_type' + data = data' + +type _MultipartPart is (_MultipartField | _MultipartFile) + """A single part in a multipart form: either a text field or a file.""" diff --git a/corral/_vendor/courier/_parse_error.pony b/corral/_vendor/courier/_parse_error.pony new file mode 100644 index 0000000..9f5abec --- /dev/null +++ b/corral/_vendor/courier/_parse_error.pony @@ -0,0 +1,43 @@ +interface val _ParseError is Stringable + +primitive TooLarge is _ParseError + """ + Status line or headers exceed the configured size limit. + """ + fun string(): String iso^ => "TooLarge".clone() + +primitive InvalidStatusLine is _ParseError + """ + Response status line is malformed. + """ + fun string(): String iso^ => "InvalidStatusLine".clone() + +primitive InvalidVersion is _ParseError + """ + HTTP version is not HTTP/1.0 or HTTP/1.1. + """ + fun string(): String iso^ => "InvalidVersion".clone() + +primitive MalformedHeaders is _ParseError + """ + Header syntax is invalid (missing colon, obs-fold continuation line). + """ + fun string(): String iso^ => "MalformedHeaders".clone() + +primitive InvalidContentLength is _ParseError + """ + Content-Length is non-numeric, negative, or has conflicting values. + """ + fun string(): String iso^ => "InvalidContentLength".clone() + +primitive InvalidChunk is _ParseError + """ + Chunked transfer encoding error: bad chunk size or missing CRLF. + """ + fun string(): String iso^ => "InvalidChunk".clone() + +primitive BodyTooLarge is _ParseError + """ + Response body exceeds the configured maximum body size. + """ + fun string(): String iso^ => "BodyTooLarge".clone() diff --git a/corral/_vendor/courier/_parser_config.pony b/corral/_vendor/courier/_parser_config.pony new file mode 100644 index 0000000..d9f48ed --- /dev/null +++ b/corral/_vendor/courier/_parser_config.pony @@ -0,0 +1,21 @@ +class val _ParserConfig + """ + Configuration for HTTP response parser size limits. + + All limits are in bytes. Responses exceeding any limit produce a parse error. + """ + let max_status_line_size: USize + let max_header_size: USize + let max_chunk_header_size: USize + let max_body_size: USize + + new val create( + max_status_line_size': USize = 8192, + max_header_size': USize = 8192, + max_chunk_header_size': USize = 128, + max_body_size': USize = 10_485_760) + => + max_status_line_size = max_status_line_size' + max_header_size = max_header_size' + max_chunk_header_size = max_chunk_header_size' + max_body_size = max_body_size' diff --git a/corral/_vendor/courier/_parser_state.pony b/corral/_vendor/courier/_parser_state.pony new file mode 100644 index 0000000..4893cce --- /dev/null +++ b/corral/_vendor/courier/_parser_state.pony @@ -0,0 +1,635 @@ +primitive _ParseContinue + """ + More data may be parseable in the current buffer. + """ + +primitive _ParseNeedMore + """ + Need more data from the network before parsing can continue. + """ + +type _ParseResult is (_ParseContinue | _ParseNeedMore | ParseError) + """ + Result of a single parse step. + """ + +interface ref _ParserState + """ + A state in the HTTP response parser state machine. + + Each state is a class that owns its per-state data (buffers, + accumulators). State transitions are explicit assignments to + `p.state`. Per-state data is automatically cleaned up when + the state transitions out. + """ + fun ref parse(p: _ResponseParser ref): _ParseResult + +// --------------------------------------------------------------------------- +// Buffer scanning utilities +// --------------------------------------------------------------------------- +primitive _BufferScan + """ + Byte-level scanning utilities for the parser buffer. + """ + + fun find_crlf(buf: Array[U8] box, from: USize = 0): (USize | None) => + """ + Find the position of \\r\\n in buf starting from `from`. + Returns the index of \\r, or None if not found. + """ + if buf.size() < (from + 2) then return None end + var i = from + let limit = buf.size() - 1 + try + while i < limit do + if (buf(i)? == '\r') and (buf(i + 1)? == '\n') then + return i + end + i = i + 1 + end + else + _Unreachable() + end + None + + fun find_byte( + buf: Array[U8] box, + byte: U8, + from: USize, + to: USize = USize.max_value()) + : (USize | None) + => + """ + Find the first occurrence of `byte` in buf[from, to). + """ + var i = from + let limit = to.min(buf.size()) + try + while i < limit do + if buf(i)? == byte then return i end + i = i + 1 + end + else + _Unreachable() + end + None + +// --------------------------------------------------------------------------- +// Parser states +// --------------------------------------------------------------------------- +class _ExpectStatusLine is _ParserState + """ + Waiting for a complete HTTP status line. + + Format: HTTP-version SP status-code SP reason-phrase CRLF + """ + let _config: _ParserConfig + + new create(config: _ParserConfig) => + _config = config + + fun ref parse(p: _ResponseParser ref): _ParseResult => + let available = p.buf.size() - p.pos + + match \exhaustive\ _BufferScan.find_crlf(p.buf, p.pos) + | let crlf: USize => + let line_len = crlf - p.pos + if line_len > _config.max_status_line_size then + return TooLarge + end + + // Parse version: must start with "HTTP/1.0" or "HTTP/1.1" + if line_len < 12 then + // Minimum: "HTTP/1.x NNN" = 12 chars + return InvalidStatusLine + end + + let version = + try + if (p.buf(p.pos)? == 'H') + and (p.buf(p.pos + 1)? == 'T') + and (p.buf(p.pos + 2)? == 'T') + and (p.buf(p.pos + 3)? == 'P') + and (p.buf(p.pos + 4)? == '/') + and (p.buf(p.pos + 5)? == '1') + and (p.buf(p.pos + 6)? == '.') + then + let minor = p.buf(p.pos + 7)? + if minor == '1' then + HTTP11 + elseif minor == '0' then + HTTP10 + else + return InvalidVersion + end + else + return InvalidVersion + end + else + _Unreachable() + return InvalidVersion + end + + // Expect space after version + try + if p.buf(p.pos + 8)? != ' ' then + return InvalidStatusLine + end + else + _Unreachable() + return InvalidStatusLine + end + + // Parse 3-digit status code at pos+9 + let status_start = p.pos + 9 + if (status_start + 3) > crlf then + return InvalidStatusLine + end + + let status: U16 = + try + let d1 = p.buf(status_start)? + let d2 = p.buf(status_start + 1)? + let d3 = p.buf(status_start + 2)? + if (d1 < '0') or (d1 > '9') + or (d2 < '0') or (d2 > '9') + or (d3 < '0') or (d3 > '9') + then + return InvalidStatusLine + end + (((d1 - '0').u16() * 100) + + ((d2 - '0').u16() * 10) + + (d3 - '0').u16()) + else + _Unreachable() + return InvalidStatusLine + end + + // Reason phrase: everything after status code to CRLF + // May be empty. If present, a space separates status from reason. + let after_status = status_start + 3 + let reason: String val = + if after_status < crlf then + // Skip the space between status and reason + let reason_start = + if + try + p.buf(after_status)? == ' ' + else + _Unreachable() + false + end + then + after_status + 1 + else + after_status + end + p.extract_string(reason_start, crlf) + else + "" + end + + // Advance past the status line + p.pos = crlf + 2 + + // Transition to header parsing + p.state = _ExpectHeaders(status, reason, version, _config) + _ParseContinue + + | None => + // No complete line yet — check size limit + if available > _config.max_status_line_size then + TooLarge + else + _ParseNeedMore + end + end + +class _ExpectHeaders is _ParserState + """ + Parsing HTTP headers after the status line. + + Loops through header lines until an empty line (CRLF) marks the end + of headers. Tracks Content-Length and Transfer-Encoding for body handling. + Determines body framing per RFC 7230 §3.3.3 for responses. + """ + let _status: U16 + let _reason: String val + let _version: Version + let _config: _ParserConfig + var _headers: Headers iso = recover iso Headers end + var _content_length: (USize | None) = None + var _chunked: Bool = false + var _total_header_bytes: USize = 0 + + new create( + status: U16, + reason: String val, + version: Version, + config: _ParserConfig) + => + _status = status + _reason = reason + _version = version + _config = config + + fun ref parse(p: _ResponseParser ref): _ParseResult => + while true do + match \exhaustive\ _BufferScan.find_crlf(p.buf, p.pos) + | let crlf: USize => + if crlf == p.pos then + // Empty line: end of headers + p.pos = crlf + 2 + + // 1xx informational: discard and await the final response + if (_status >= 100) and (_status < 200) then + p.state = _ExpectStatusLine(_config) + return _ParseContinue + end + + // Deliver the response metadata + let headers: Headers val = + (_headers = recover iso Headers end) + p.handler.response_received(_status, _reason, _version, headers) + + // Body determination per RFC 7230 §3.3.3: + // HEAD responses and 204/304 have no body regardless of headers + if (p.method is HEAD) or (_status == 204) or (_status == 304) then + p.handler.response_complete() + p.state = _ExpectStatusLine(_config) + return _ParseContinue + end + + // Transfer-Encoding: chunked takes precedence over Content-Length + if _chunked then + p.state = _ExpectChunkHeader(0, _config) + return _ParseContinue + end + + match \exhaustive\ _content_length + | let cl: USize if cl > 0 => + // Check body size limit before entering body state + if cl > _config.max_body_size then + return BodyTooLarge + end + p.state = _ExpectFixedBody(cl) + return _ParseContinue + | let _: USize => + // Content-Length: 0 + p.handler.response_complete() + p.state = _ExpectStatusLine(_config) + return _ParseContinue + else + // No Content-Length, no chunked: close-delimited body + p.state = _ExpectCloseDelimitedBody(_config) + return _ParseContinue + end + end + + // Track header size + let line_len = (crlf - p.pos) + 2 + _total_header_bytes = _total_header_bytes + line_len + if _total_header_bytes > _config.max_header_size then + return TooLarge + end + + // Check for obs-fold (continuation line): reject per RFC 7230 + try + let first_byte = p.buf(p.pos)? + if (first_byte == ' ') or (first_byte == '\t') then + return MalformedHeaders + end + else + _Unreachable() + end + + // Find colon separator + let colon_pos = + match \exhaustive\ _BufferScan.find_byte(p.buf, ':', p.pos, crlf) + | let i: USize => i + | None => return MalformedHeaders + end + + // Header name must not be empty + if colon_pos == p.pos then + return MalformedHeaders + end + + // Extract header name (lowercasing happens in Headers.add) + let name: String val = p.extract_string(p.pos, colon_pos) + + // Extract header value, skipping optional whitespace (OWS) + var val_start = colon_pos + 1 + try + while val_start < crlf do + let ch = p.buf(val_start)? + if (ch != ' ') and (ch != '\t') then break end + val_start = val_start + 1 + end + else + _Unreachable() + end + + // Trim trailing OWS from value + var val_end = crlf + try + while val_end > val_start do + let ch = p.buf(val_end - 1)? + if (ch != ' ') and (ch != '\t') then break end + val_end = val_end - 1 + end + else + _Unreachable() + end + + let value: String val = p.extract_string(val_start, val_end) + + // Detect special headers + let lower_name: String val = name.lower() + if lower_name == "content-length" then + match \exhaustive\ _parse_content_length(value) + | let cl: USize => + match \exhaustive\ _content_length + | let existing: USize => + if existing != cl then + return InvalidContentLength + end + | None => + _content_length = cl + end + | InvalidContentLength => return InvalidContentLength + end + elseif lower_name == "transfer-encoding" then + if value.lower().contains("chunked") then + _chunked = true + end + end + + _headers.add(name, value) + + // Advance past this header line + p.pos = crlf + 2 + | None => + // No complete line yet — check size limit + let pending = p.buf.size() - p.pos + if (pending + _total_header_bytes) > _config.max_header_size then + return TooLarge + end + return _ParseNeedMore + end + end + _Unreachable() + _ParseNeedMore + + fun _parse_content_length(value: String val) + : (USize | InvalidContentLength) + => + """ + Parse a Content-Length value as a non-negative integer. + """ + if value.size() == 0 then + return InvalidContentLength + end + try + var i: USize = 0 + while i < value.size() do + let ch = value(i)? + if (ch < '0') or (ch > '9') then + return InvalidContentLength + end + i = i + 1 + end + value.read_int[USize]()?._1 + else + InvalidContentLength + end + +class _ExpectFixedBody is _ParserState + """ + Reading a fixed-length response body (Content-Length). + + Delivers body data incrementally as it becomes available in the buffer. + """ + var _remaining: USize + + new create(remaining: USize) => + _remaining = remaining + + fun ref parse(p: _ResponseParser ref): _ParseResult => + let available = (p.buf.size() - p.pos).min(_remaining) + if available > 0 then + let chunk: Array[U8] val = + p.extract_bytes(p.pos, p.pos + available) + p.handler.body_chunk(chunk) + p.pos = p.pos + available + _remaining = _remaining - available + end + + if _remaining == 0 then + p.handler.response_complete() + p.state = _ExpectStatusLine(p.config) + _ParseContinue + else + _ParseNeedMore + end + +class _ExpectChunkHeader is _ParserState + """ + Expecting a chunk size line in chunked transfer encoding. + + Format: chunk-size [ chunk-ext ] CRLF + """ + var _total_body_received: USize + let _config: _ParserConfig + + new create(total_body_received: USize, config: _ParserConfig) => + _total_body_received = total_body_received + _config = config + + fun ref parse(p: _ResponseParser ref): _ParseResult => + match \exhaustive\ _BufferScan.find_crlf(p.buf, p.pos) + | let crlf: USize => + let line_len = crlf - p.pos + if line_len > _config.max_chunk_header_size then + return InvalidChunk + end + if line_len == 0 then + return InvalidChunk + end + + // Find optional chunk extension (semicolon) + let size_end = + match \exhaustive\ _BufferScan.find_byte(p.buf, ';', p.pos, crlf) + | let i: USize => i + | None => crlf + end + + // Parse hex chunk size — must consume entire string + let size_str: String val = p.extract_string(p.pos, size_end) + let chunk_size = + try + (let cs, let consumed) = size_str.read_int[USize](0, 16)? + if consumed.usize() != size_str.size() then + return InvalidChunk + end + cs + else + return InvalidChunk + end + + p.pos = crlf + 2 + + if chunk_size == 0 then + // Last chunk — expect trailers or final CRLF + p.state = _ExpectChunkTrailer(0, _config) + _ParseContinue + else + // Check body size limit + if (_total_body_received + chunk_size) > _config.max_body_size then + return BodyTooLarge + end + p.state = + _ExpectChunkData( + chunk_size, _total_body_received, _config) + _ParseContinue + end + | None => + // No complete line — check size limit + let pending = p.buf.size() - p.pos + if pending > _config.max_chunk_header_size then + InvalidChunk + else + _ParseNeedMore + end + end + +class _ExpectChunkData is _ParserState + """ + Reading chunk data in chunked transfer encoding. + + Delivers data incrementally, then expects CRLF after the chunk data. + """ + var _remaining: USize + var _total_body_received: USize + let _config: _ParserConfig + + new create( + remaining: USize, + total_body_received: USize, + config: _ParserConfig) + => + _remaining = remaining + _total_body_received = total_body_received + _config = config + + fun ref parse(p: _ResponseParser ref): _ParseResult => + if _remaining > 0 then + let available = (p.buf.size() - p.pos).min(_remaining) + if available > 0 then + let chunk: Array[U8] val = + p.extract_bytes(p.pos, p.pos + available) + p.handler.body_chunk(chunk) + p.pos = p.pos + available + _remaining = _remaining - available + _total_body_received = _total_body_received + available + end + if _remaining > 0 then + return _ParseNeedMore + end + end + + // Chunk data consumed — expect CRLF + let bytes_available = p.buf.size() - p.pos + if bytes_available < 2 then + return _ParseNeedMore + end + + try + if (p.buf(p.pos)? == '\r') and (p.buf(p.pos + 1)? == '\n') then + p.pos = p.pos + 2 + p.state = _ExpectChunkHeader(_total_body_received, _config) + _ParseContinue + else + InvalidChunk + end + else + _Unreachable() + InvalidChunk + end + +class _ExpectChunkTrailer is _ParserState + """ + Reading optional trailer headers after the last (zero-size) chunk. + + Trailers are skipped (not delivered to the receiver). The response is + complete when an empty line is found. + """ + var _total_trailer_bytes: USize + let _config: _ParserConfig + + new create(total_trailer_bytes: USize, config: _ParserConfig) => + _total_trailer_bytes = total_trailer_bytes + _config = config + + fun ref parse(p: _ResponseParser ref): _ParseResult => + while true do + match \exhaustive\ _BufferScan.find_crlf(p.buf, p.pos) + | let crlf: USize => + if crlf == p.pos then + // Empty line: end of chunked message + p.pos = crlf + 2 + p.handler.response_complete() + p.state = _ExpectStatusLine(p.config) + return _ParseContinue + end + + // Skip this trailer header line + let line_len = (crlf - p.pos) + 2 + _total_trailer_bytes = _total_trailer_bytes + line_len + if _total_trailer_bytes > _config.max_header_size then + return TooLarge + end + + p.pos = crlf + 2 + | None => + let pending = p.buf.size() - p.pos + if (pending + _total_trailer_bytes) > _config.max_header_size then + return TooLarge + end + return _ParseNeedMore + end + end + _Unreachable() + _ParseNeedMore + +class _ExpectCloseDelimitedBody is _ParserState + """ + Reading a response body delimited by connection close. + + Used when the response has neither Content-Length nor Transfer-Encoding: + chunked. Delivers all available data via `body_chunk()` and tracks total + bytes received. Returns `_ParseNeedMore` to request more data. Completion + is triggered externally by `_ResponseParser.connection_closed()`, not by + `parse()`. + """ + let _config: _ParserConfig + var _total_received: USize = 0 + + new create(config: _ParserConfig) => + _config = config + + fun ref parse(p: _ResponseParser ref): _ParseResult => + let available = p.buf.size() - p.pos + if available > 0 then + let chunk: Array[U8] val = + p.extract_bytes(p.pos, p.pos + available) + p.pos = p.pos + available + _total_received = _total_received + available + + if _total_received > _config.max_body_size then + return BodyTooLarge + end + + p.handler.body_chunk(chunk) + end + _ParseNeedMore diff --git a/corral/_vendor/courier/_percent_encoder.pony b/corral/_vendor/courier/_percent_encoder.pony new file mode 100644 index 0000000..3c988f4 --- /dev/null +++ b/corral/_vendor/courier/_percent_encoder.pony @@ -0,0 +1,68 @@ +primitive _PercentEncoder + """ + RFC 3986 and WHATWG percent-encoding for query strings and form data. + + Two encoding modes: + - `query()`: RFC 3986 unreserved characters pass through, everything else + is `%XX`, spaces become `%20`. + - `form()`: WHATWG form encoding, spaces become `+`, unreserved characters + pass through, everything else is `%XX`. + """ + + fun query(input: String): String iso^ => + """ + Encode a string using RFC 3986 percent-encoding for query components. + + Unreserved characters (`A-Z a-z 0-9 - . _ ~`) pass through unchanged. + All other bytes are encoded as `%XX`. + """ + let buf = recover iso String(input.size()) end + for byte in input.values() do + if _is_rfc3986_unreserved(byte) then + buf.push(byte) + else + buf .> push('%') + .> push(_hex_digit(byte >> 4)) + .push(_hex_digit(byte and 0x0F)) + end + end + consume buf + + fun form(input: String): String iso^ => + """ + Encode a string using WHATWG application/x-www-form-urlencoded encoding. + + Characters `A-Z a-z 0-9 * - . _` pass through unchanged. Spaces become + `+`. All other bytes are encoded as `%XX`. + """ + let buf = recover iso String(input.size()) end + for byte in input.values() do + if byte == ' ' then + buf.push('+') + elseif _is_form_unreserved(byte) then + buf.push(byte) + else + buf .> push('%') + .> push(_hex_digit(byte >> 4)) + .push(_hex_digit(byte and 0x0F)) + end + end + consume buf + + fun _is_rfc3986_unreserved(byte: U8): Bool => + ((byte >= 'A') and (byte <= 'Z')) + or ((byte >= 'a') and (byte <= 'z')) + or ((byte >= '0') and (byte <= '9')) + or (byte == '-') or (byte == '.') or (byte == '_') or (byte == '~') + + fun _is_form_unreserved(byte: U8): Bool => + ((byte >= 'A') and (byte <= 'Z')) + or ((byte >= 'a') and (byte <= 'z')) + or ((byte >= '0') and (byte <= '9')) + or (byte == '*') or (byte == '-') or (byte == '.') or (byte == '_') + + fun _hex_digit(nibble: U8): U8 => + let n = nibble and 0x0F + if n < 10 then '0' + n + else ('A' - 10) + n + end diff --git a/corral/_vendor/courier/_request_builder.pony b/corral/_vendor/courier/_request_builder.pony new file mode 100644 index 0000000..53aaef3 --- /dev/null +++ b/corral/_vendor/courier/_request_builder.pony @@ -0,0 +1,82 @@ +class ref _RequestBuilder + """ + Internal implementation of the request builder. + + Implements all methods from both `RequestOptions` and + `RequestOptionsWithBody`. The `Request` factory controls which interface + the caller sees, providing compile-time body restriction for GET/HEAD. + """ + let _method: Method + let _path: String + let _headers: Headers ref + embed _query_params: Array[(String, String)] + var _body: (Array[U8] val | None) + + new ref create(method': Method, path': String) => + _method = method' + _path = path' + _headers = Headers + _query_params = Array[(String, String)] + _body = None + + fun ref header(hdr_name: String, hdr_value: String): _RequestBuilder ref => + _headers.set(hdr_name, hdr_value) + this + + fun ref query(key: String, value: String): _RequestBuilder ref => + _query_params.push((key, value)) + this + + fun ref basic_auth(username: String, password: String) + : _RequestBuilder ref + => + (let hdr_name, let hdr_value) = BasicAuth(username, password) + _headers.set(hdr_name, hdr_value) + this + + fun ref bearer_auth(token: String): _RequestBuilder ref => + (let hdr_name, let hdr_value) = BearerAuth(token) + _headers.set(hdr_name, hdr_value) + this + + fun ref body(data: Array[U8] val): _RequestBuilder ref => + _body = data + this + + fun ref json_body(data: String): _RequestBuilder ref => + _body = data.array() + _headers.set("Content-Type", "application/json") + this + + fun ref form_body(params: Array[(String, String)] val) + : _RequestBuilder ref + => + _body = FormEncoder(params) + _headers.set("Content-Type", "application/x-www-form-urlencoded") + this + + fun ref multipart_body(form: MultipartFormData): _RequestBuilder ref => + _body = form.body() + _headers.set("Content-Type", form.content_type()) + this + + fun ref build(): HTTPRequest val => + let full_path = _build_path() + var hdrs: Headers iso = recover iso Headers end + for (n, v) in _headers.values() do + hdrs.set(n, v) + end + HTTPRequest(_method, full_path, consume hdrs, _body) + + fun _build_path(): String => + if _query_params.size() == 0 then + _path + else + let qsize = _query_params.size() + var params: Array[(String, String)] iso = + recover iso Array[(String, String)](qsize) end + for (k, v) in _query_params.values() do + params.push((k, v)) + end + _path + "?" + QueryParams(consume params) + end diff --git a/corral/_vendor/courier/_request_serializer.pony b/corral/_vendor/courier/_request_serializer.pony new file mode 100644 index 0000000..f2f5795 --- /dev/null +++ b/corral/_vendor/courier/_request_serializer.pony @@ -0,0 +1,64 @@ +primitive _RequestSerializer + """ + Serialize an `HTTPRequest` into HTTP/1.1 wire format. + + Produces: `METHOD SP PATH SP HTTP/1.1\r\n` + headers + `\r\n` + body. + + Auto-sets `Host` from the connection's host/port if not already present. + Port is omitted for "80" and "443" (standard ports); included otherwise. + Auto-sets `Content-Length` from body size if body is present and not already + in the request headers. User-explicit headers take precedence. + """ + + fun apply( + request: HTTPRequest val, + host: String, + port: String) + : Array[U8] iso^ + => + let buf = recover iso Array[U8] end + + // Request line: METHOD SP PATH SP HTTP/1.1\r\n + buf .> append(request.method.string()) + .> push(' ') + .> append(request.path) + .> append(" HTTP/1.1\r\n") + + // Host header (auto-set if not present) + if request.headers.get("host") is None then + buf.append("Host: ") + buf.append(host) + if (port != "80") and (port != "443") and (port != "") then + buf .> push(':') + .> append(port) + end + buf.append("\r\n") + end + + // Content-Length header (auto-set if body present and not already set) + match request.body + | let b: Array[U8] val => + if request.headers.get("content-length") is None then + buf .> append("Content-Length: ") + .> append(b.size().string()) + .> append("\r\n") + end + end + + // User headers + for (name, value) in request.headers.values() do + buf .> append(name) + .> append(": ") + .> append(value) + .> append("\r\n") + end + + // End of headers + buf.append("\r\n") + + // Body + match request.body + | let b: Array[U8] val => buf.append(b) + end + + buf diff --git a/corral/_vendor/courier/_response_parser.pony b/corral/_vendor/courier/_response_parser.pony new file mode 100644 index 0000000..4ab24d5 --- /dev/null +++ b/corral/_vendor/courier/_response_parser.pony @@ -0,0 +1,125 @@ +class _ResponseParser + """ + HTTP/1.1 response parser. + + Data is fed in as chunks via `parse()` (matching lori's delivery model). + Parsed responses are delivered via the `_ResponseParserNotify` callback + interface. The parser handles arbitrary chunk boundaries, connection reuse + (multiple responses on the same connection), and both fixed-length, chunked, + and close-delimited transfer encoding. + + The `method` field stores the request method for the response currently + being parsed. HEAD responses and 204/304 responses have no body regardless + of headers — the method is needed to detect HEAD. + + Fields are public so that state classes (in the same package) can access + them for buffer reading, position tracking, state transitions, and handler + callbacks. + """ + var state: _ParserState + let handler: _ResponseParserNotify ref + let config: _ParserConfig + var buf: Array[U8] ref = Array[U8] + var pos: USize = 0 + var method: Method = GET + var _failed: Bool = false + + new create( + handler': _ResponseParserNotify ref, + config': _ParserConfig = _ParserConfig) + => + handler = handler' + config = config' + state = _ExpectStatusLine(config) + + fun ref expect_response(method': Method) => + """ + Prepare the parser to receive a response for the given request method. + + Sets the method (needed for HEAD body suppression) and resets state to + `_ExpectStatusLine`. Called by `HTTPClientConnection.send_request()`. + """ + method = method' + state = _ExpectStatusLine(config) + + fun ref parse(data: Array[U8] iso) => + """ + Feed data to the parser. + + The parser processes as much data as possible in a single call, + delivering callbacks for each complete response (or response component) + found. Remaining partial data is buffered for the next call. + """ + if _failed then return end + + buf.append(consume data) + + var continue_parsing = true + while continue_parsing do + match \exhaustive\ state.parse(this) + | _ParseContinue => + if _failed then break end + | _ParseNeedMore => continue_parsing = false + | let err: ParseError => + handler.parse_error(err) + _failed = true + continue_parsing = false + end + end + + // Compact consumed data + if pos > 0 then + buf.trim_in_place(pos) + pos = 0 + end + + fun ref connection_closed() => + """ + Signal that the remote end closed the connection. + + If the parser is currently in `_ExpectCloseDelimitedBody` state, this + completes the response by calling `handler.response_complete()`. For + all other states, this is a no-op — the connection class handles + `on_closed()` separately. + """ + if _failed then return end + match state + | let _: _ExpectCloseDelimitedBody => + handler.response_complete() + _failed = true + end + + fun ref stop() => + """ + Stop the parser. All subsequent `parse()` calls become no-ops. + + Safe to call from within a handler callback during parsing — the parse + loop checks the failed flag after each state transition. + """ + _failed = true + + fun ref extract_bytes(from: USize, to: USize): Array[U8] iso^ => + """ + Copy bytes from buf[from..to) into a new iso array. + """ + let len = to - from + let out = recover Array[U8].create(len) end + var i = from + while i < to do + try out.push(buf(i)?) else _Unreachable() end + i = i + 1 + end + out + + fun ref extract_string(from: USize, to: USize): String iso^ => + """ + Copy bytes from buf[from..to) into a new iso String. + """ + let len = to - from + let out = recover String.create(len) end + var i = from + while i < to do + try out.push(buf(i)?) else _Unreachable() end + i = i + 1 + end + out diff --git a/corral/_vendor/courier/_response_parser_notify.pony b/corral/_vendor/courier/_response_parser_notify.pony new file mode 100644 index 0000000..cca46f8 --- /dev/null +++ b/corral/_vendor/courier/_response_parser_notify.pony @@ -0,0 +1,37 @@ +trait ref _ResponseParserNotify + """ + Callback interface for the HTTP response parser. + """ + + fun ref response_received( + status: U16, + reason: String val, + version: Version, + headers: Headers val) + """ + Called when the status line and all headers have been parsed. + + For responses with a body (Content-Length, chunked, or close-delimited), + `body_chunk` calls follow. For responses without a body (HEAD, 204, 304), + `response_complete` is called immediately after. + """ + fun ref body_chunk(data: Array[U8] val) + """ + Called for each chunk of response body data as it becomes available. + + Body data is delivered incrementally -- not accumulated. + """ + fun ref response_complete() + """ + Called when the entire response (including any body) has been received. + + After this call, the parser is ready to parse the next response on the + same connection (keep-alive). + """ + fun ref parse_error(err: ParseError) + """ + Called when a parse error is encountered. + + After this call, the parser enters a terminal failed state and will not + produce any further callbacks. The connection should be closed. + """ diff --git a/corral/_vendor/courier/_scheme.pony b/corral/_vendor/courier/_scheme.pony new file mode 100644 index 0000000..ce15f83 --- /dev/null +++ b/corral/_vendor/courier/_scheme.pony @@ -0,0 +1,15 @@ +interface val _Scheme is (Equatable[Scheme] & Stringable) + +primitive SchemeHTTP is _Scheme + """ + HTTP scheme (unencrypted). + """ + fun string(): String iso^ => "http".clone() + fun eq(that: Scheme): Bool => that is this + +primitive SchemeHTTPS is _Scheme + """ + HTTPS scheme (TLS-encrypted). + """ + fun string(): String iso^ => "https".clone() + fun eq(that: Scheme): Bool => that is this diff --git a/corral/_vendor/courier/_url_parse_error.pony b/corral/_vendor/courier/_url_parse_error.pony new file mode 100644 index 0000000..864125a --- /dev/null +++ b/corral/_vendor/courier/_url_parse_error.pony @@ -0,0 +1,31 @@ +interface val _URLParseError is Stringable + +primitive MissingScheme is _URLParseError + """ + URL has no `://` separator or the scheme portion is empty. + """ + fun string(): String iso^ => "MissingScheme".clone() + +primitive UnsupportedScheme is _URLParseError + """ + URL scheme is not `http` or `https`. + """ + fun string(): String iso^ => "UnsupportedScheme".clone() + +primitive MissingHost is _URLParseError + """ + URL has an empty host component. + """ + fun string(): String iso^ => "MissingHost".clone() + +primitive InvalidPort is _URLParseError + """ + Port is non-numeric, zero, or exceeds 65535. + """ + fun string(): String iso^ => "InvalidPort".clone() + +primitive UserInfoNotSupported is _URLParseError + """ + URL contains userinfo (`user@` or `user:pass@`), which is not supported. + """ + fun string(): String iso^ => "UserInfoNotSupported".clone() diff --git a/corral/_vendor/courier/_version.pony b/corral/_vendor/courier/_version.pony new file mode 100644 index 0000000..545a430 --- /dev/null +++ b/corral/_vendor/courier/_version.pony @@ -0,0 +1,15 @@ +interface val _Version is (Equatable[Version] & Stringable) + +primitive HTTP10 is _Version + """ + HTTP/1.0 protocol version. + """ + fun string(): String iso^ => "HTTP/1.0".clone() + fun eq(that: Version): Bool => that is this + +primitive HTTP11 is _Version + """ + HTTP/1.1 protocol version. + """ + fun string(): String iso^ => "HTTP/1.1".clone() + fun eq(that: Version): Bool => that is this diff --git a/corral/_vendor/courier/basic_auth.pony b/corral/_vendor/courier/basic_auth.pony new file mode 100644 index 0000000..976d86a --- /dev/null +++ b/corral/_vendor/courier/basic_auth.pony @@ -0,0 +1,20 @@ +use "encode/base64" + +primitive BasicAuth + """ + Construct an HTTP Basic Authentication header value. + + Returns a `(name, value)` tuple suitable for passing directly to + `Headers.set()` or the request builder's `header()` method: + `("authorization", "Basic ")`. + """ + + fun apply(username: String, password: String): (String, String) => + """ + Build the Basic auth header from `username` and `password`. + + The credentials are encoded as `base64(username:password)` per RFC 7617. + """ + let credentials: String val = username + ":" + password + let encoded: String val = Base64.encode(credentials) + ("authorization", "Basic " + encoded) diff --git a/corral/_vendor/courier/bearer_auth.pony b/corral/_vendor/courier/bearer_auth.pony new file mode 100644 index 0000000..4845263 --- /dev/null +++ b/corral/_vendor/courier/bearer_auth.pony @@ -0,0 +1,14 @@ +primitive BearerAuth + """ + Construct an HTTP Bearer token authentication header value. + + Returns a `(name, value)` tuple suitable for passing directly to + `Headers.set()` or the request builder's `header()` method: + `("authorization", "Bearer ")`. + """ + + fun apply(token: String): (String, String) => + """ + Build the Bearer auth header from `token`. + """ + ("authorization", "Bearer " + token) diff --git a/corral/_vendor/courier/client_connection_config.pony b/corral/_vendor/courier/client_connection_config.pony new file mode 100644 index 0000000..956ef73 --- /dev/null +++ b/corral/_vendor/courier/client_connection_config.pony @@ -0,0 +1,90 @@ +use lori = "lori" + +primitive _DefaultIdleTimeout + """ + 60-second idle timeout, the default for HTTP client connections. + """ + fun apply(): (lori.IdleTimeout | None) => + match lori.MakeIdleTimeout(60_000) + | let t: lori.IdleTimeout => t + else + _Unreachable() + None + end + +class val ClientConnectionConfig + """ + Configuration for an HTTP client connection. + + Parser limits control the maximum size of response components. Idle timeout + controls how long the connection can sit without I/O activity before the + library closes it. Connection timeout bounds how long the initial connection + handshake is allowed to take. `from` specifies the local bind address (empty + string means any interface). + + ```pony + // All defaults (60-second idle timeout, 10 MB max body) + ClientConnectionConfig + + // Custom idle timeout via MakeIdleTimeout (milliseconds) + let timeout = match lori.MakeIdleTimeout(30_000) + | let t: lori.IdleTimeout => t + end + ClientConnectionConfig(where + max_body_size' = 52_428_800, // 50 MB + idle_timeout' = timeout) + + // Disable idle timeout + ClientConnectionConfig(where idle_timeout' = None) + + // Set a 5-second connection timeout + let ct = match lori.MakeConnectionTimeout(5_000) + | let t: lori.ConnectionTimeout => t + end + ClientConnectionConfig(where connection_timeout' = ct) + ``` + """ + let max_status_line_size: USize + let max_header_size: USize + let max_chunk_header_size: USize + let max_body_size: USize + let idle_timeout: (lori.IdleTimeout | None) + let connection_timeout: (lori.ConnectionTimeout | None) + let from: String + + new val create( + max_status_line_size': USize = 8192, + max_header_size': USize = 8192, + max_chunk_header_size': USize = 128, + max_body_size': USize = 10_485_760, + idle_timeout': (lori.IdleTimeout | None) = _DefaultIdleTimeout(), + connection_timeout': (lori.ConnectionTimeout | None) = None, + from': String = "") + => + """ + Create client connection configuration. + + Parser limits default to sensible values. `idle_timeout'` is an + `IdleTimeout` (milliseconds) or `None` to disable idle timeout. Defaults + to 60 seconds. `connection_timeout'` is a `ConnectionTimeout` + (milliseconds) or `None` to disable connection timeout. Defaults to + `None`. `from'` specifies the local bind address (empty string means any + interface). + """ + max_status_line_size = max_status_line_size' + max_header_size = max_header_size' + max_chunk_header_size = max_chunk_header_size' + max_body_size = max_body_size' + idle_timeout = idle_timeout' + connection_timeout = connection_timeout' + from = from' + + fun _parser_config(): _ParserConfig val => + """ + Create a parser config from the parser limit fields. + """ + _ParserConfig( + max_status_line_size, + max_header_size, + max_chunk_header_size, + max_body_size) diff --git a/corral/_vendor/courier/connection_failure_reason.pony b/corral/_vendor/courier/connection_failure_reason.pony new file mode 100644 index 0000000..0fcc72b --- /dev/null +++ b/corral/_vendor/courier/connection_failure_reason.pony @@ -0,0 +1,8 @@ +type ConnectionFailureReason is + ( ConnectionFailedDNS + | ConnectionFailedTCP + | ConnectionFailedSSL + | ConnectionFailedTimeout ) + """ + Reason a connection attempt failed, delivered via `on_connection_failure()`. + """ diff --git a/corral/_vendor/courier/courier.pony b/corral/_vendor/courier/courier.pony new file mode 100644 index 0000000..cbab701 --- /dev/null +++ b/corral/_vendor/courier/courier.pony @@ -0,0 +1,143 @@ +""" +courier — HTTP client for Pony. + +Courier is an HTTP/1.1 client library built on +[lori](https://github.com/ponylang/lori). It follows the same architectural +pattern as lori and [stallion](https://github.com/ponylang/stallion): a +protocol handler class (`HTTPClientConnection`) owned by the user's actor, +with synchronous `fun ref` callbacks. No hidden actors. + +## Getting Started + +Implement `HTTPClientConnectionActor` on your actor, store an +`HTTPClientConnection` as a field, and override the lifecycle callbacks +you need: + +```pony +use "courier" +use lori = "lori" + +actor MyClient is HTTPClientConnectionActor + var _http: HTTPClientConnection = HTTPClientConnection.none() + let _out: OutStream + + new create(auth: lori.TCPConnectAuth, host: String, port: String, + out: OutStream) + => + _out = out + _http = HTTPClientConnection(auth, host, port, this, + ClientConnectionConfig) + + fun ref _http_client_connection(): HTTPClientConnection => _http + + fun ref on_connected() => + _http.send_request(HTTPRequest(GET, "/")) + + fun ref on_response(response: Response val) => + _out.print(response.status.string() + " " + response.reason) + + fun ref on_body_chunk(data: Array[U8] val) => + _out.write(data) + + fun ref on_response_complete() => + _out.print("") + _http.close() +``` + +For HTTPS, use `HTTPClientConnection.ssl()` instead of +`HTTPClientConnection()`. + +## One-Shot Timers + +For response deadlines or application-level timeouts, use +`HTTPClientConnection.set_timer()`. Unlike idle timeout, this timer fires +unconditionally — I/O activity does not reset it. Only one timer can be active +per connection at a time. The typical pattern is a response deadline: set a +timer +after sending a request, cancel it when the response completes, close the +connection if the timer fires: + +```pony +actor MyClient is HTTPClientConnectionActor + var _http: HTTPClientConnection = HTTPClientConnection.none() + var _timer: (lori.TimerToken | None) = None + let _out: OutStream + + // ... constructor ... + + fun ref _http_client_connection(): HTTPClientConnection => _http + + fun ref on_connected() => + _http.send_request(Request.get("/slow-endpoint").build()) + match lori.MakeTimerDuration(5_000) + | let d: lori.TimerDuration => + match _http.set_timer(d) + | let t: lori.TimerToken => _timer = t + | let err: lori.SetTimerError => None + end + end + + fun ref on_response_complete() => + match _timer + | let t: lori.TimerToken => + _http.cancel_timer(t) + _timer = None + end + // process response... + _http.close() + + fun ref on_timer(token: lori.TimerToken) => + match _timer + | let t: lori.TimerToken if t == token => + _timer = None + _out.print("Response timed out") + _http.close() + end +``` + +## Key Types + +- `HTTPClientConnectionActor` — trait for your actor +- `HTTPClientConnection` — protocol handler class (stored as actor field) +- `HTTPClientLifecycleEventReceiver` — callback trait (default no-ops) +- `HTTPRequest` — request data (method, path, headers, body) +- `Response` — parsed response metadata (version, status, reason, headers) +- `ClientConnectionConfig` — parser limits, idle timeout, connection timeout, + bind address +- `SendRequestResult` — result of `send_request()` (success or error) +- `ConnectionFailureReason` — reason a connection attempt failed + (`ConnectionFailedDNS`, `ConnectionFailedTCP`, `ConnectionFailedSSL`, + `ConnectionFailedTimeout`) +- `lori.TimerToken` — opaque token for timer cancellation and matching +- `lori.TimerDuration` — validated timer duration (use + `lori.MakeTimerDuration(milliseconds)` to create) +- `lori.SetTimerError` — timer setup failure (`lori.SetTimerAlreadyActive`, + `lori.SetTimerNotOpen`) +- `HTTPResponse` — buffered response with complete body + (from `ResponseCollector`) +- `ResponseCollector` — accumulates streaming callbacks into `HTTPResponse` +- `QueryParams` — RFC 3986 query string encoding +- `FormEncoder` — `application/x-www-form-urlencoded` body encoding +- `BasicAuth` — HTTP Basic authentication header +- `BearerAuth` — HTTP Bearer token authentication header +- `Request` — factory for typed step-builder request construction +- `RequestOptions` — builder interface for all methods (headers, query, auth) +- `RequestOptionsWithBody` — builder interface with body methods (POST, etc.) +- `MultipartFormData` — `multipart/form-data` builder for file uploads and + mixed form submissions; use with `multipart_body()` on the request builder. + For simple key-value form data without files, use `FormEncoder`/`form_body()` + instead. +- `URL` — parse URL strings into `ParsedURL` components +- `ParsedURL` — parsed URL with scheme, host, port, path, and optional query +- `Scheme` — URL scheme (`SchemeHTTP` or `SchemeHTTPS`) +- `URLParseError` — error encountered during URL parsing +- `ResponseJSON` — parse `HTTPResponse` body as JSON +- `JSONDecoder` — interface for typed JSON decoders +- `JSONDecodeError` — decode failure with descriptive message +- `DecodeJSON` — parse and decode an HTTP response body in one step + +## Design + +See [Discussion #2](https://github.com/ponylang/courier/discussions/2) for +the full design rationale. +""" diff --git a/corral/_vendor/courier/decode_json.pony b/corral/_vendor/courier/decode_json.pony new file mode 100644 index 0000000..ae7a0a6 --- /dev/null +++ b/corral/_vendor/courier/decode_json.pony @@ -0,0 +1,48 @@ +use json = "json" + +primitive DecodeJSON[A: Any val] + """ + Parse and decode an HTTP response body as a typed domain object in one step. + + Combines `ResponseJSON` (JSON parsing) with a `JSONDecoder` (structural + decoding) into a single call. The three-way return type distinguishes + between success, parse failure, and decode failure: + + - `A` — the response body was valid JSON and matched the decoder's expected + structure + - `JsonParseError` — the response body was not valid JSON syntax + - `JSONDecodeError` — the JSON was valid but didn't match the decoder's + expected structure (missing fields, wrong types, etc.) + + ```pony + use "courier" + use json = "json" + + // In on_response_complete(): + match DecodeJSON[User](response, UserDecoder) + | let user: User => + env.out.print("Hello, " + user.name) + | let err: json.JsonParseError => + env.out.print("Invalid JSON: " + err.string()) + | let err: JSONDecodeError => + env.out.print("Unexpected structure: " + err.string()) + end + ``` + """ + + fun apply( + response: HTTPResponse, + decoder: JSONDecoder[A]) + : (A | json.JsonParseError | JSONDecodeError) + => + """ + Parse `response.body` as JSON, then decode with `decoder`. + + Returns `JsonParseError` if the body isn't valid JSON, `JSONDecodeError` if + the JSON doesn't match the decoder's expected structure, or the decoded + value on success. + """ + match \exhaustive\ ResponseJSON(response) + | let value: json.JsonValue => decoder(value) + | let err: json.JsonParseError => err + end diff --git a/corral/_vendor/courier/form_encoder.pony b/corral/_vendor/courier/form_encoder.pony new file mode 100644 index 0000000..350fac9 --- /dev/null +++ b/corral/_vendor/courier/form_encoder.pony @@ -0,0 +1,31 @@ +primitive FormEncoder + """ + Encode key-value pairs as `application/x-www-form-urlencoded` body data. + + Uses WHATWG form encoding: spaces become `+`, unreserved characters + (`A-Z a-z 0-9 * - . _`) pass through, everything else is `%XX`. + + Returns `Array[U8] val` because the output is used as a request body, + matching `HTTPRequest.body`'s type. Use `QueryParams` instead when building + URL query strings (which use RFC 3986 encoding and return `String`). + """ + + fun apply(params: Array[(String, String)] val): Array[U8] val => + """ + Encode `params` as form-urlencoded body data. + + Keys and values are encoded per WHATWG spec. Pairs are joined with `&`. + Returns an empty array if `params` is empty. + """ + if params.size() == 0 then return recover val Array[U8] end end + + var buf = recover iso String end + var first = true + for (key, value) in params.values() do + if not first then buf.push('&') end + first = false + buf.append(_PercentEncoder.form(key)) + buf.push('=') + buf.append(_PercentEncoder.form(value)) + end + (consume buf).iso_array() diff --git a/corral/_vendor/courier/headers.pony b/corral/_vendor/courier/headers.pony new file mode 100644 index 0000000..9164740 --- /dev/null +++ b/corral/_vendor/courier/headers.pony @@ -0,0 +1,70 @@ +class Headers + """ + A collection of HTTP headers with case-insensitive name lookup. + + Names are lowercased on storage. Use `set()` to replace all values for a + name, or `add()` to append an additional value (appropriate for multi-value + headers like Set-Cookie). + """ + embed _headers: Array[(String, String)] + + new create() => + """ + Create an empty header collection. + """ + _headers = Array[(String, String)] + + fun ref set(name: String, value: String) => + """ + Set a header, removing any existing entries with the same name. + + After this call, `get(name)` returns `value` and there is exactly one + entry for this name. + """ + let lower_name: String val = name.lower() + var i: USize = 0 + while i < _headers.size() do + try + if _headers(i)?._1 == lower_name then + _headers.delete(i)? + else + i = i + 1 + end + else + i = i + 1 + end + end + _headers.push((lower_name, value)) + + fun ref add(name: String, value: String) => + """ + Add a header entry without removing existing entries with the same name. + + This is appropriate for headers that can appear multiple times + (e.g., Set-Cookie). Use `set()` when you want to replace. + """ + _headers.push((name.lower(), value)) + + fun get(name: String): (String | None) => + """ + Get the first value for the given header name (case-insensitive). + + Returns `None` if no header with that name exists. + """ + let lower_name: String val = name.lower() + for (n, v) in _headers.values() do + if n == lower_name then return v end + end + None + + fun size(): USize => + """ + Return the number of header entries. + """ + _headers.size() + + fun values(): ArrayValues[(String, String), this->Array[(String, String)]] => + """ + Iterate over all header entries as (name, value) pairs. + """ + _headers.values() diff --git a/corral/_vendor/courier/http_client_connection.pony b/corral/_vendor/courier/http_client_connection.pony new file mode 100644 index 0000000..ec0a984 --- /dev/null +++ b/corral/_vendor/courier/http_client_connection.pony @@ -0,0 +1,376 @@ +use lori = "lori" +use ssl_net = "ssl/net" + +primitive _Idle +primitive _AwaitingResponse + +class HTTPClientConnection is + (lori.ClientLifecycleEventReceiver & _ResponseParserNotify) + """ + HTTP protocol handler that manages request serialization, response parsing, + and connection lifecycle for a single HTTP client connection. + + Stored as a field inside an `HTTPClientConnectionActor`. Handles all + HTTP-level concerns — serializing outgoing requests, parsing incoming + responses, idle timeout scheduling, and backpressure — and delivers + HTTP events to the actor via `HTTPClientLifecycleEventReceiver` callbacks. + + The protocol class implements lori's `ClientLifecycleEventReceiver` + to receive TCP-level events from the connection, and + `_ResponseParserNotify` to receive parser callbacks. It forwards + HTTP-level events to the owning actor. + + Use `none()` as the field default so that `this` is `ref` in the + actor constructor body, then replace with `create()` or `ssl()`: + + ```pony + actor MyClient is HTTPClientConnectionActor + var _http: HTTPClientConnection = HTTPClientConnection.none() + + new create(auth: lori.TCPConnectAuth, host: String, port: String, + config: ClientConnectionConfig) + => + _http = HTTPClientConnection(auth, host, port, this, config) + ``` + """ + let _lifecycle_event_receiver: + (HTTPClientLifecycleEventReceiver ref | None) + let _config: (ClientConnectionConfig | None) + let _host: String + let _port: String + var _tcp_connection: lori.TCPConnection = lori.TCPConnection.none() + var _state: _ConnectionState = _Active + var _request_state: (_Idle | _AwaitingResponse) = _Idle + var _parser: (_ResponseParser | None) = None + + new none() => + """ + Create a placeholder protocol instance. + + Used as the default value for the `_http` field in + `HTTPClientConnectionActor` implementations, allowing `this` to be `ref` + in the actor constructor body. The placeholder is immediately replaced + by `create()` or `ssl()` — its methods must never be called. + """ + _lifecycle_event_receiver = None + _config = None + _host = "" + _port = "" + + new create( + auth: lori.TCPConnectAuth, + host: String, + port: String, + client_actor: HTTPClientConnectionActor ref, + config: ClientConnectionConfig) + => + """ + Create the protocol handler for a plain HTTP connection. + + Called inside the `HTTPClientConnectionActor` constructor. The + `client_actor` parameter must be the actor's `this` — it provides the + `HTTPClientLifecycleEventReceiver ref` for synchronous HTTP callbacks. + """ + _lifecycle_event_receiver = client_actor + _config = config + _host = host + _port = port + _parser = _ResponseParser(this, config._parser_config()) + _tcp_connection = + lori.TCPConnection.client( + auth, + host, + port, + config.from, + client_actor, + this + where connection_timeout = config.connection_timeout) + + new ssl( + auth: lori.TCPConnectAuth, + ssl_ctx: ssl_net.SSLContext val, + host: String, + port: String, + client_actor: HTTPClientConnectionActor ref, + config: ClientConnectionConfig) + => + """ + Create the protocol handler for an HTTPS connection. + + Like `create`, but wraps the TCP connection in SSL using the provided + `SSLContext`. Called inside the `HTTPClientConnectionActor` constructor + for HTTPS connections. + """ + _lifecycle_event_receiver = client_actor + _config = config + _host = host + _port = port + _parser = _ResponseParser(this, config._parser_config()) + _tcp_connection = + lori.TCPConnection.ssl_client( + auth, + ssl_ctx, + host, + port, + config.from, + client_actor, + this + where connection_timeout = config.connection_timeout) + + fun ref _connection(): lori.TCPConnection => + """ + Return the underlying TCP connection. + """ + _tcp_connection + + fun ref send_request(request: HTTPRequest val): SendRequestResult => + """ + Serialize and send an HTTP request. + + Returns `SendRequestOK` on success, `ConnectionClosed` if the connection + is not open, or `ResponsePending` if a response to a previous request + is still in progress. + + Auto-sets `Host` and `Content-Length` headers during serialization if + they are not already present in the request. + """ + match _state + | let _: _Closed => return ConnectionClosed + end + + match _request_state + | let _: _AwaitingResponse => return ResponsePending + end + + match _parser + | let p: _ResponseParser => p.expect_response(request.method) + end + + let serialized = _RequestSerializer(request, _host, _port) + match _tcp_connection.send(consume serialized) + | let _: lori.SendError => + _close_connection() + return ConnectionClosed + end + + _request_state = _AwaitingResponse + SendRequestOK + + fun ref close() => + """ + Close the connection. + + Safe to call at any time; idempotent due to the `_Active` state guard + in `_close_connection()`. + """ + _close_connection() + + fun ref yield_read() => + """ + Exit the read loop after the current callback, giving other actors a + chance to run. Reading resumes automatically on the next scheduler turn. + + Intended for use inside `on_body_chunk()` to prevent a single large + response from starving other actors. Granularity is per-TCP-read, not + per-HTTP-chunk — one TCP read may contain multiple chunks, and they will + all be parsed before yielding. This is a one-shot flag; there is no + corresponding "unmute" needed. + + No state guard is needed — if the connection is closed, the read loop + is not running and the flag is harmless. + """ + _tcp_connection.yield_read() + + fun ref set_timer(duration: lori.TimerDuration) + : (lori.TimerToken | lori.SetTimerError) + => + """ + Create a one-shot timer that fires `on_timer()` after the configured + duration. Returns a `TimerToken` on success, or a `SetTimerError` on + failure. + + Unlike idle timeout, this timer has no I/O-reset behavior — it fires + unconditionally after the duration elapses, regardless of send/receive + activity. There is no automatic re-arming; call `set_timer()` again from + `on_timer()` for repetition. + + Only one timer can be active at a time. Setting a timer while one is + already active returns `SetTimerAlreadyActive` — call `cancel_timer()` + first. Requires the connection to be open; returns `SetTimerNotOpen` if + not. + + Use `lori.MakeTimerDuration(milliseconds)` to create the duration value. + `MakeTimerDuration` returns `(TimerDuration | ValidationFailure)`, so + match on the result before passing it here. + """ + _tcp_connection.set_timer(duration) + + fun ref cancel_timer(token: lori.TimerToken) => + """ + Cancel an active timer. No-op if the token doesn't match the active timer + (already fired, already cancelled, wrong token). Safe to call with stale + tokens. + """ + _tcp_connection.cancel_timer(token) + + // + // ClientLifecycleEventReceiver + // + fun ref _on_connected() => + match _config + | let c: ClientConnectionConfig => + _tcp_connection.idle_timeout(c.idle_timeout) + end + match _lifecycle_event_receiver + | let r: HTTPClientLifecycleEventReceiver ref => r.on_connected() + end + + fun ref _on_connection_failure(reason: lori.ConnectionFailureReason) => + _state = _Closed + let courier_reason: ConnectionFailureReason = + match \exhaustive\ reason + | lori.ConnectionFailedDNS => ConnectionFailedDNS + | lori.ConnectionFailedTCP => ConnectionFailedTCP + | lori.ConnectionFailedSSL => ConnectionFailedSSL + | lori.ConnectionFailedTimeout => ConnectionFailedTimeout + end + match _lifecycle_event_receiver + | let r: HTTPClientLifecycleEventReceiver ref => + r.on_connection_failure(courier_reason) + end + + fun ref _on_received(data: Array[U8] iso) => + _state.on_received(this, consume data) + + fun ref _on_closed() => + _state.on_closed(this) + + fun ref _on_throttled() => + _state.on_throttled(this) + + fun ref _on_unthrottled() => + _state.on_unthrottled(this) + + fun ref _on_idle_timeout() => + _state.on_idle_timeout(this) + + fun ref _on_timer(token: lori.TimerToken) => + _state.on_timer(this, token) + + // + // _ResponseParserNotify — forwarding parser events to receiver + // + fun ref response_received( + status: U16, + reason: String val, + version: Version, + headers: Headers val) + => + match _lifecycle_event_receiver + | let r: HTTPClientLifecycleEventReceiver ref => + r.on_response(Response(version, status, reason, headers)) + end + + fun ref body_chunk(data: Array[U8] val) => + match _lifecycle_event_receiver + | let r: HTTPClientLifecycleEventReceiver ref => + r.on_body_chunk(data) + end + + fun ref response_complete() => + _request_state = _Idle + match _lifecycle_event_receiver + | let r: HTTPClientLifecycleEventReceiver ref => + r.on_response_complete() + end + + fun ref parse_error(err: ParseError) => + match _lifecycle_event_receiver + | let r: HTTPClientLifecycleEventReceiver ref => + r.on_parse_error(err) + end + _close_connection() + + // + // Internal methods called by state classes + // + fun ref _feed_parser(data: Array[U8] iso) => + """ + Feed incoming data to the response parser. + """ + match _parser + | let p: _ResponseParser => p.parse(consume data) + end + + fun ref _handle_closed() => + """ + Handle remote connection close. + + For close-delimited bodies, `parser.connection_closed()` completes the + response. For all other states, this just cleans up. + """ + match _parser + | let p: _ResponseParser => + p.connection_closed() + p.stop() + end + match _lifecycle_event_receiver + | let r: HTTPClientLifecycleEventReceiver ref => r.on_closed() + end + _state = _Closed + + fun ref _handle_throttled() => + """ + Apply backpressure: mute the TCP connection and notify the receiver. + """ + _tcp_connection.mute() + match _lifecycle_event_receiver + | let r: HTTPClientLifecycleEventReceiver ref => r.on_throttled() + end + + fun ref _handle_unthrottled() => + """ + Release backpressure: unmute the TCP connection and notify. + """ + _tcp_connection.unmute() + match _lifecycle_event_receiver + | let r: HTTPClientLifecycleEventReceiver ref => r.on_unthrottled() + end + + fun ref _handle_idle_timeout() => + """ + Close the connection on idle timeout. + """ + _close_connection() + + fun ref _handle_timer(token: lori.TimerToken) => + """ + Forward one-shot timer firing to the receiver. + """ + match \exhaustive\ _lifecycle_event_receiver + | let r: HTTPClientLifecycleEventReceiver ref => r.on_timer(token) + | None => _Unreachable() + end + + fun ref _close_connection() => + """ + Close the connection and clean up all resources. + + User-initiated close does NOT call `parser.connection_closed()` — the + user knows the response is abandoned. Remote close goes through + `_handle_closed()` which does call `parser.connection_closed()`. + + Safe to call from within parser callbacks — the `_Active` state guard + prevents double-close. + """ + match _state + | let _: _Active => + match _parser + | let p: _ResponseParser => p.stop() + end + match _lifecycle_event_receiver + | let r: HTTPClientLifecycleEventReceiver ref => r.on_closed() + end + _tcp_connection.close() + _state = _Closed + end diff --git a/corral/_vendor/courier/http_client_connection_actor.pony b/corral/_vendor/courier/http_client_connection_actor.pony new file mode 100644 index 0000000..6fb25d2 --- /dev/null +++ b/corral/_vendor/courier/http_client_connection_actor.pony @@ -0,0 +1,58 @@ +use lori = "lori" + +trait tag HTTPClientConnectionActor is + (lori.TCPConnectionActor & HTTPClientLifecycleEventReceiver) + """ + Trait for actors that make HTTP client connections. + + Extends `TCPConnectionActor` (for lori ASIO plumbing) and + `HTTPClientLifecycleEventReceiver` (for HTTP-level callbacks). The + actor stores an `HTTPClientConnection` as a field and implements + `_http_client_connection()` to return it. All other required methods have + default implementations that delegate to the protocol. + + Minimal implementation: + + ```pony + actor MyClient is HTTPClientConnectionActor + var _http: HTTPClientConnection = HTTPClientConnection.none() + + new create(auth: lori.TCPConnectAuth, host: String, port: String, + config: ClientConnectionConfig) + => + _http = HTTPClientConnection(auth, host, port, this, config) + + fun ref _http_client_connection(): HTTPClientConnection => _http + + fun ref on_connected() => + let request = HTTPRequest(GET, "/") + _http.send_request(request) + + fun ref on_response(version: Version, status: U16, + reason: String val, headers: Headers val) + => + // process response + None + ``` + + For HTTPS, use `HTTPClientConnection.ssl(auth, ssl_ctx, host, port, + this, config)` instead of `HTTPClientConnection(auth, host, port, + this, config)`. + + The `none()` default ensures all fields are initialized before the + constructor body runs, so `this` is `ref` when passed to + `HTTPClientConnection.create()` or `HTTPClientConnection.ssl()`. + """ + + fun ref _http_client_connection(): HTTPClientConnection + """ + Return the protocol instance owned by this actor. + + Called by the default implementation of `_connection()`. Must return + the same instance every time. + """ + fun ref _connection(): lori.TCPConnection => + """ + Delegates to the protocol's TCP connection. + """ + _http_client_connection()._connection() diff --git a/corral/_vendor/courier/http_client_lifecycle_event_receiver.pony b/corral/_vendor/courier/http_client_lifecycle_event_receiver.pony new file mode 100644 index 0000000..7a0183e --- /dev/null +++ b/corral/_vendor/courier/http_client_lifecycle_event_receiver.pony @@ -0,0 +1,114 @@ +use lori = "lori" + +trait ref HTTPClientLifecycleEventReceiver + """ + HTTP response lifecycle callbacks delivered to the client actor. + + All callbacks have default no-op implementations. Override only the + callbacks your actor needs. Callbacks are invoked synchronously inside + the actor that owns the `HTTPClientConnection`. + + Typical usage: override `on_connected()` to send the first request, + `on_response()` and `on_body_chunk()` to process the response, and + `on_response_complete()` to send follow-up requests or close. + """ + + fun ref on_connected() => + """ + Called when the connection is ready for application data. + + For plain TCP connections, this fires after TCP connect. For SSL + connections, this fires after the TLS handshake completes. Safe to call + `send_request()` from this callback. + """ + None + + fun ref on_connection_failure(reason: ConnectionFailureReason) => + """ + Called when a connection attempt fails. + + The `reason` indicates which stage failed: DNS resolution, TCP connect, + SSL handshake, or connection timeout. The connection is unusable after + this callback. + """ + None + + fun ref on_response(response: Response val) => + """ + Called when the response status line and all headers have been parsed. + + For responses with a body, `on_body_chunk()` calls follow. For responses + without a body (HEAD, 204, 304), `on_response_complete()` is called + immediately after. + """ + None + + fun ref on_body_chunk(data: Array[U8] val) => + """ + Called for each chunk of response body data as it arrives. + + Body data is delivered incrementally. Accumulate chunks manually if + you need the complete body before processing. + """ + None + + fun ref on_response_complete() => + """ + Called when the entire response (including any body) has been received. + + After this call, the connection is ready for another `send_request()` + (connection reuse / keep-alive). + """ + None + + fun ref on_parse_error(err: ParseError) => + """ + Called when a response parse error is encountered. + + The connection is closed after this callback. No further callbacks + will be delivered except `on_closed()`. + """ + None + + fun ref on_closed() => + """ + Called when the connection closes. + + Fires on remote disconnect, local close, idle timeout, or any other + reason. Not called if the connection fails before connecting (use + `on_connection_failure` for that case). + """ + None + + fun ref on_throttled() => + """ + Called when backpressure is applied on the connection. + + The TCP send buffer is full — avoid sending more requests until + `on_unthrottled()` is called. + """ + None + + fun ref on_unthrottled() => + """ + Called when backpressure is released on the connection. + + The TCP send buffer has drained — request sending may resume. + """ + None + + fun ref on_timer(token: lori.TimerToken) => + """ + Called when a one-shot timer created by `HTTPClientConnection.set_timer()` + fires. + + The `token` matches the one returned by `set_timer()`. Fires once per + `set_timer()` call. The timer is consumed before the callback, so it is + safe to call `set_timer()` from within `on_timer()` to re-arm. No + automatic re-arming occurs. + + Unlike idle timeout, this timer has no I/O-reset behavior — it fires + unconditionally after the configured duration, regardless of send/receive + activity. + """ + None diff --git a/corral/_vendor/courier/http_request.pony b/corral/_vendor/courier/http_request.pony new file mode 100644 index 0000000..9acad8a --- /dev/null +++ b/corral/_vendor/courier/http_request.pony @@ -0,0 +1,30 @@ +class val HTTPRequest + """ + An HTTP request to be sent by `HTTPClientConnection`. + + Holds the method, path, headers, and optional body. The connection layer + auto-sets `Host` and `Content-Length` headers during serialization if they + are not already present — callers only need to set them explicitly when + overriding the defaults. + + No validation is performed on the request — the client sends whatever the + caller asks for. + """ + let method: Method + let path: String + let headers: Headers val + let body: (Array[U8] val | None) + + new val create( + method': Method, + path': String, + headers': Headers val = recover val Headers end, + body': (Array[U8] val | None) = None) + => + """ + Create an HTTP request with the given method, path, headers, and body. + """ + method = method' + path = path' + headers = headers' + body = body' diff --git a/corral/_vendor/courier/http_response.pony b/corral/_vendor/courier/http_response.pony new file mode 100644 index 0000000..39aed1f --- /dev/null +++ b/corral/_vendor/courier/http_response.pony @@ -0,0 +1,33 @@ +class val HTTPResponse + """ + A buffered HTTP response containing status, headers, and the complete body. + + `HTTPResponse` is the result of collecting streaming response callbacks into a + single object. Use `ResponseCollector` to accumulate `on_response()` and + `on_body_chunk()` data, then call `build()` to produce an `HTTPResponse`. + + The entire body is held in memory as a contiguous `Array[U8] val`. For large + responses where memory is a concern, use the raw `on_body_chunk()` callbacks + directly instead of collecting. + """ + let version: Version + let status: U16 + let reason: String val + let headers: Headers val + let body: Array[U8] val + + new val create( + version': Version, + status': U16, + reason': String val, + headers': Headers val, + body': Array[U8] val) + => + """ + Create a buffered HTTP response with all fields. + """ + version = version' + status = status' + reason = reason' + headers = headers' + body = body' diff --git a/corral/_vendor/courier/json_decode_error.pony b/corral/_vendor/courier/json_decode_error.pony new file mode 100644 index 0000000..b9e618f --- /dev/null +++ b/corral/_vendor/courier/json_decode_error.pony @@ -0,0 +1,24 @@ +class val JSONDecodeError is Stringable + """ + A structural mismatch when decoding parsed JSON into a domain type. + + `JSONDecodeError` is distinct from `JsonParseError`: + `JsonParseError` means the response body was not valid JSON syntax, while + `JSONDecodeError` means the JSON was syntactically valid but its structure + doesn't match what the decoder expected — a missing field, a field with the + wrong type, or any other shape mismatch. The message should describe what was + expected versus what was found. + """ + let message: String + + new val create(message': String) => + """ + Create a decode error with a descriptive message. + """ + message = message' + + fun string(): String iso^ => + """ + Return the error message. + """ + message.clone() diff --git a/corral/_vendor/courier/json_decoder.pony b/corral/_vendor/courier/json_decoder.pony new file mode 100644 index 0000000..286e9cb --- /dev/null +++ b/corral/_vendor/courier/json_decoder.pony @@ -0,0 +1,38 @@ +use json = "json" + +interface val JSONDecoder[A: Any val] + """ + Interface for converting a parsed `JsonValue` into a typed domain object. + + Implement this interface to define how a specific JSON structure maps to your + application type. Return `JSONDecodeError` when the JSON doesn't match the + expected structure. + + ```pony + use "courier" + use json = "json" + + class val User + let name: String + let age: I64 + + new val create(name': String, age': I64) => + name = name' + age = age' + + primitive UserDecoder is JSONDecoder[User] + fun apply(value: json.JsonValue): (User | JSONDecodeError) => + let nav = json.JsonNav(value) + try + User(nav("name").as_string()?, nav("age").as_i64()?) + else + JSONDecodeError("expected object with string 'name' and integer 'age'") + end + ``` + """ + + fun apply(value: json.JsonValue): (A | JSONDecodeError) + """ + Decode a parsed JSON value into a domain object, or return an error if + the JSON structure doesn't match. + """ diff --git a/corral/_vendor/courier/method.pony b/corral/_vendor/courier/method.pony new file mode 100644 index 0000000..03b2693 --- /dev/null +++ b/corral/_vendor/courier/method.pony @@ -0,0 +1,96 @@ +interface val Method is (Equatable[Method] & Stringable) + """ + An HTTP request method (RFC 7231). + """ + +primitive GET is Method + """ + HTTP GET method. + """ + fun string(): String iso^ => "GET".clone() + fun eq(that: Method): Bool => that is this + +primitive HEAD is Method + """ + HTTP HEAD method. + """ + fun string(): String iso^ => "HEAD".clone() + fun eq(that: Method): Bool => that is this + +primitive POST is Method + """ + HTTP POST method. + """ + fun string(): String iso^ => "POST".clone() + fun eq(that: Method): Bool => that is this + +primitive PUT is Method + """ + HTTP PUT method. + """ + fun string(): String iso^ => "PUT".clone() + fun eq(that: Method): Bool => that is this + +primitive DELETE is Method + """ + HTTP DELETE method. + """ + fun string(): String iso^ => "DELETE".clone() + fun eq(that: Method): Bool => that is this + +primitive CONNECT is Method + """ + HTTP CONNECT method. + """ + fun string(): String iso^ => "CONNECT".clone() + fun eq(that: Method): Bool => that is this + +primitive OPTIONS is Method + """ + HTTP OPTIONS method. + """ + fun string(): String iso^ => "OPTIONS".clone() + fun eq(that: Method): Bool => that is this + +primitive TRACE is Method + """ + HTTP TRACE method. + """ + fun string(): String iso^ => "TRACE".clone() + fun eq(that: Method): Bool => that is this + +primitive PATCH is Method + """ + HTTP PATCH method. + """ + fun string(): String iso^ => "PATCH".clone() + fun eq(that: Method): Bool => that is this + +primitive Methods + """ + Parse HTTP method strings and enumerate known methods. + """ + + fun parse(data: String): (Method | None) => + """ + Parse a string into an HTTP method, or None if not recognized. + """ + match data + | "GET" => GET + | "HEAD" => HEAD + | "POST" => POST + | "PUT" => PUT + | "DELETE" => DELETE + | "CONNECT" => CONNECT + | "OPTIONS" => OPTIONS + | "TRACE" => TRACE + | "PATCH" => PATCH + else + None + end + + fun valid(): Array[Method] val => + """ + Return all standard HTTP methods. + """ + [GET; HEAD; POST; PUT; DELETE; CONNECT; OPTIONS; TRACE; PATCH] diff --git a/corral/_vendor/courier/multipart_form_data.pony b/corral/_vendor/courier/multipart_form_data.pony new file mode 100644 index 0000000..f71f145 --- /dev/null +++ b/corral/_vendor/courier/multipart_form_data.pony @@ -0,0 +1,127 @@ +use "format" +use "random" +use "time" + +class ref MultipartFormData + """ + Build a `multipart/form-data` body for file uploads and mixed form + submissions (RFC 7578). + + Use `field()` for plain text values and `file()` for file attachments. + After adding all parts, pass the builder to `multipart_body()` on the + request builder, which sets both the body and `Content-Type` header. + + For simple key-value form data without files, use `FormEncoder` with + `form_body()` instead — it produces the more compact + `application/x-www-form-urlencoded` format. + + ```pony + let form = MultipartFormData + .> field("username", "alice") + .> file("avatar", "photo.jpg", "image/jpeg", image_data) + let req = Request.post("/upload") + .multipart_body(form) + .build() + ``` + """ + let _boundary: String + embed _parts: Array[_MultipartPart] + + new ref create() => + """ + Create a new builder with a randomly generated boundary string. + """ + _parts = Array[_MultipartPart] + (let secs, let nanos) = Time.now() + let rand = Rand(secs.u64(), nanos.u64()) + let a = rand.next() + let b = rand.next() + _boundary = "----courier" + + Format.int[U64](a, FormatHexSmallBare where width = 16, fill = '0') + + Format.int[U64](b, FormatHexSmallBare where width = 16, fill = '0') + + fun ref field(name: String, value: String): MultipartFormData ref => + """ + Add a text field to the form. + + `"` and `\` in the name are automatically backslash-escaped in the + serialized `Content-Disposition` quoted-string. + """ + _parts.push(_MultipartField(name, value)) + this + + fun ref file( + name: String, + filename: String, + file_content_type: String, + data: Array[U8] val) + : MultipartFormData ref + => + """ + Add a file attachment to the form. + + `"` and `\` in the name and filename are automatically backslash-escaped + in the serialized `Content-Disposition` quoted-string. + """ + _parts.push(_MultipartFile(name, filename, file_content_type, data)) + this + + fun content_type(): String => + """ + Return the `Content-Type` header value including the boundary parameter. + + Pass this to the request's `Content-Type` header so the server knows how + to parse the body. The `multipart_body()` method on the request builder + does this automatically. + """ + "multipart/form-data; boundary=" + _boundary + + fun body(): Array[U8] val => + """ + Serialize all parts into the `multipart/form-data` wire format. + + Each part is delimited by the boundary string. Field parts include a + `Content-Disposition` header; file parts additionally include a `filename` + parameter and a `Content-Type` header. The body ends with a closing + boundary. + + Field names and filenames are backslash-escaped (`"` becomes `\"`, `\` + becomes `\\`) within `Content-Disposition` quoted-strings. + """ + let buf = recover iso Array[U8] end + for part in _parts.values() do + buf.append("--") + buf.append(_boundary) + buf.append("\r\n") + match \exhaustive\ part + | let f: _MultipartField val => + buf.append("Content-Disposition: form-data; name=\"") + buf.append(_escape_quoted(f.name)) + buf.append("\"\r\n\r\n") + buf.append(f.value) + | let f: _MultipartFile val => + buf.append("Content-Disposition: form-data; name=\"") + buf.append(_escape_quoted(f.name)) + buf.append("\"; filename=\"") + buf.append(_escape_quoted(f.filename)) + buf.append("\"\r\nContent-Type: ") + buf.append(f.content_type) + buf.append("\r\n\r\n") + buf.append(f.data) + end + buf.append("\r\n") + end + buf.append("--") + buf.append(_boundary) + buf.append("--\r\n") + consume buf + + fun _escape_quoted(input: String): String iso^ => + let buf = recover iso String(input.size()) end + for byte in input.values() do + if (byte == '"') or (byte == '\\') then + buf.push('\\') + end + buf.push(byte) + end + consume buf diff --git a/corral/_vendor/courier/parse_error.pony b/corral/_vendor/courier/parse_error.pony new file mode 100644 index 0000000..18cf57c --- /dev/null +++ b/corral/_vendor/courier/parse_error.pony @@ -0,0 +1,11 @@ +type ParseError is + ( TooLarge + | InvalidStatusLine + | InvalidVersion + | MalformedHeaders + | InvalidContentLength + | InvalidChunk + | BodyTooLarge ) + """ + Parse error encountered during HTTP response parsing. + """ diff --git a/corral/_vendor/courier/parsed_url.pony b/corral/_vendor/courier/parsed_url.pony new file mode 100644 index 0000000..4600296 --- /dev/null +++ b/corral/_vendor/courier/parsed_url.pony @@ -0,0 +1,47 @@ +class val ParsedURL + """ + A parsed URL with scheme, host, port, path, and optional query string. + + Produced by `URL.parse()`. Fields are validated and normalized: the scheme + is lowercase, the host has IPv6 brackets stripped, the port is a decimal + string (defaulting to `"80"` for HTTP or `"443"` for HTTPS when omitted), + and the path defaults to `"/"` when absent. + + Use `request_path()` to get the combined path and query string for the + HTTP request target. Use `is_ssl()` to determine whether TLS is needed. + """ + let scheme: Scheme + let host: String + let port: String + let path: String + let query: (String | None) + + new val _create( + scheme': Scheme, + host': String, + port': String, + path': String, + query': (String | None)) + => + scheme = scheme' + host = host' + port = port' + path = path' + query = query' + + fun request_path(): String => + """ + The HTTP request target: path with query string appended if present. + + Always starts with `/`. For example, `/api/v1?key=value`. + """ + match \exhaustive\ query + | let q: String => path + "?" + q + | None => path + end + + fun is_ssl(): Bool => + """ + True if the scheme is HTTPS. + """ + scheme is SchemeHTTPS diff --git a/corral/_vendor/courier/query_params.pony b/corral/_vendor/courier/query_params.pony new file mode 100644 index 0000000..d24e5a2 --- /dev/null +++ b/corral/_vendor/courier/query_params.pony @@ -0,0 +1,30 @@ +primitive QueryParams + """ + Encode key-value pairs as a URL query string using RFC 3986 percent-encoding. + + Returns a string like `key1=value1&key2=value2` with all keys and values + percent-encoded. No leading `?` — the caller or request builder prepends + that when appending to a path. + + Returns an empty string for empty params. + """ + + fun apply(params: Array[(String, String)] val): String => + """ + Encode `params` as a query string. + + Keys and values are percent-encoded per RFC 3986. Pairs are joined with + `&`. Returns an empty string if `params` is empty. + """ + if params.size() == 0 then return "" end + + var buf = recover iso String end + var first = true + for (key, value) in params.values() do + if not first then buf.push('&') end + first = false + buf.append(_PercentEncoder.query(key)) + buf.push('=') + buf.append(_PercentEncoder.query(value)) + end + consume buf diff --git a/corral/_vendor/courier/request.pony b/corral/_vendor/courier/request.pony new file mode 100644 index 0000000..a8d9703 --- /dev/null +++ b/corral/_vendor/courier/request.pony @@ -0,0 +1,90 @@ +primitive Request + """ + Factory for building HTTP requests with a typed step-builder pattern. + + Each factory method returns a builder typed according to whether the HTTP + method supports a body: + + - `get()` and `head()` return `RequestOptions` — no body methods available. + - `post()`, `put()`, `patch()`, `delete()`, and `options()` return + `RequestOptionsWithBody` — body methods are available, but optional. + + After calling a body method, the return type narrows to `RequestOptions`, + preventing the body from being set twice. + + CONNECT and TRACE are intentionally omitted. CONNECT is for proxy tunneling + (not standard request/response) and TRACE is a diagnostic verb rarely used + by application code. Both exist as `Method` primitives and can be used + directly: `HTTPRequest(CONNECT, path)` / `HTTPRequest(TRACE, path)`. + + ```pony + // Simple GET + let req = Request.get("/users").build() + + // GET with query params and auth + let req = Request.get("/search") + .query("q", "pony lang") + .bearer_auth("my-token") + .build() + + // POST with JSON body + let req = Request.post("/users") + .json_body("{\"name\": \"Alice\"}") + .build() + + // POST with form body + let req = Request.post("/login") + .form_body(recover val [("user", "alice"); ("pass", "secret")] end) + .build() + + // POST with multipart form data + let form = MultipartFormData + .> field("username", "alice") + .> file("avatar", "photo.jpg", "image/jpeg", image_data) + let req = Request.post("/upload") + .multipart_body(form) + .build() + ``` + """ + + fun get(path: String): RequestOptions ref^ => + """ + Create a GET request builder. + """ + _RequestBuilder(GET, path) + + fun head(path: String): RequestOptions ref^ => + """ + Create a HEAD request builder. + """ + _RequestBuilder(HEAD, path) + + fun delete(path: String): RequestOptionsWithBody ref^ => + """ + Create a DELETE request builder. + """ + _RequestBuilder(DELETE, path) + + fun options(path: String): RequestOptionsWithBody ref^ => + """ + Create an OPTIONS request builder. + """ + _RequestBuilder(OPTIONS, path) + + fun post(path: String): RequestOptionsWithBody ref^ => + """ + Create a POST request builder. + """ + _RequestBuilder(POST, path) + + fun put(path: String): RequestOptionsWithBody ref^ => + """ + Create a PUT request builder. + """ + _RequestBuilder(PUT, path) + + fun patch(path: String): RequestOptionsWithBody ref^ => + """ + Create a PATCH request builder. + """ + _RequestBuilder(PATCH, path) diff --git a/corral/_vendor/courier/request_options.pony b/corral/_vendor/courier/request_options.pony new file mode 100644 index 0000000..03c7c59 --- /dev/null +++ b/corral/_vendor/courier/request_options.pony @@ -0,0 +1,36 @@ +interface ref RequestOptions + """ + Builder options available for all HTTP request methods. + + Provides methods for setting headers, query parameters, and authentication. + Use `build()` to produce the final `HTTPRequest val`. + + Methods that accept a body (POST, PUT, PATCH, DELETE, OPTIONS) return + `RequestOptionsWithBody` instead, which extends this interface with + body-setting methods. + """ + + fun ref header(hdr_name: String, hdr_value: String): RequestOptions ref + """ + Add a header to the request. + """ + + fun ref query(key: String, value: String): RequestOptions ref + """ + Add a query parameter. Parameters are percent-encoded in `build()`. + """ + + fun ref basic_auth(username: String, password: String): RequestOptions ref + """ + Set the Authorization header using HTTP Basic authentication. + """ + + fun ref bearer_auth(token: String): RequestOptions ref + """ + Set the Authorization header using a Bearer token. + """ + + fun ref build(): HTTPRequest val + """ + Build the final `HTTPRequest val`. + """ diff --git a/corral/_vendor/courier/request_options_with_body.pony b/corral/_vendor/courier/request_options_with_body.pony new file mode 100644 index 0000000..df3bdf3 --- /dev/null +++ b/corral/_vendor/courier/request_options_with_body.pony @@ -0,0 +1,62 @@ +interface ref RequestOptionsWithBody + """ + Builder options for HTTP methods that support a request body. + + Extends the common options (headers, query params, auth) with methods for + setting the request body. Available for POST, PUT, PATCH, DELETE, and + OPTIONS. + + After calling a body method, the return type narrows to `RequestOptions` + which does not expose body methods — this prevents accidentally setting the + body twice. The body is always optional: calling `build()` without setting + a body produces a request with no body. + """ + + fun ref header(hdr_name: String, hdr_value: String): + RequestOptionsWithBody ref + """ + Add a header to the request. + """ + + fun ref query(key: String, value: String): RequestOptionsWithBody ref + """ + Add a query parameter. Parameters are percent-encoded in `build()`. + """ + + fun ref basic_auth(username: String, password: String): + RequestOptionsWithBody ref + """ + Set the Authorization header using HTTP Basic authentication. + """ + + fun ref bearer_auth(token: String): RequestOptionsWithBody ref + """ + Set the Authorization header using a Bearer token. + """ + + fun ref body(data: Array[U8] val): RequestOptions ref + """ + Set the request body as raw bytes. + """ + + fun ref json_body(data: String): RequestOptions ref + """ + Set the request body to `data` and add `Content-Type: application/json`. + """ + + fun ref form_body(params: Array[(String, String)] val): RequestOptions ref + """ + URL-encode `params` via `FormEncoder`, set as body, and add + `Content-Type: application/x-www-form-urlencoded`. + """ + + fun ref multipart_body(form: MultipartFormData): RequestOptions ref + """ + Set the request body from a `MultipartFormData` builder. + + Sets `Content-Type` to `multipart/form-data` with the boundary. + """ + fun ref build(): HTTPRequest val + """ + Build the final `HTTPRequest val`. + """ diff --git a/corral/_vendor/courier/response.pony b/corral/_vendor/courier/response.pony new file mode 100644 index 0000000..61db481 --- /dev/null +++ b/corral/_vendor/courier/response.pony @@ -0,0 +1,24 @@ +class val Response + """ + Parsed HTTP response metadata (status line and headers). + + Delivered via `on_response()` after the status line and all headers have + been parsed. For responses with a body, `on_body_chunk()` calls follow. + For responses without a body (HEAD, 204, 304), `on_response_complete()` + is called immediately after. + """ + let version: Version + let status: U16 + let reason: String val + let headers: Headers val + + new val create( + version': Version, + status': U16, + reason': String val, + headers': Headers val) + => + version = version' + status = status' + reason = reason' + headers = headers' diff --git a/corral/_vendor/courier/response_collector.pony b/corral/_vendor/courier/response_collector.pony new file mode 100644 index 0000000..7dcca02 --- /dev/null +++ b/corral/_vendor/courier/response_collector.pony @@ -0,0 +1,82 @@ +class ref ResponseCollector + """ + Accumulates streaming response callbacks into a buffered `HTTPResponse`. + + Create a fresh `ResponseCollector` for each request/response cycle. On + keep-alive connections where multiple requests are sent sequentially, use a + new collector per request rather than reusing one. + + Typical usage in `HTTPClientLifecycleEventReceiver` callbacks: + + ```pony + var _collector: ResponseCollector = ResponseCollector + + fun ref on_response(response: Response val) => + _collector = ResponseCollector + _collector.set_response(response) + + fun ref on_body_chunk(data: Array[U8] val) => + _collector.add_chunk(data) + + fun ref on_response_complete() => + try + let response = _collector.build()? + // use response.status, response.body, etc. + end + ``` + + The collector concatenates all chunks into a single contiguous `Array[U8] + val`. For large responses, this means the full body is held in memory. Users + who need streaming for large downloads should use the raw `on_body_chunk()` + callbacks directly. + """ + var _response: (Response val | None) + embed _chunks: Array[Array[U8] val] + var _total_size: USize + + new create() => + """ + Create an empty response collector. + """ + _response = None + _chunks = Array[Array[U8] val] + _total_size = 0 + + fun ref set_response(response: Response val) => + """ + Store the response metadata (version, status, reason, headers). + + Must be called before `build()`. Typically called from `on_response()`. + """ + _response = response + + fun ref add_chunk(data: Array[U8] val) => + """ + Append a body chunk. Typically called from `on_body_chunk()`. + + Chunks are concatenated in order when `build()` is called. + """ + _total_size = _total_size + data.size() + _chunks.push(data) + + fun build(): HTTPResponse val ? => + """ + Build the final `HTTPResponse` from the stored response and chunks. + + Returns the buffered response with all chunks concatenated into a single + `Array[U8] val`. + + Partial: errors if `set_response()` was never called, since the collector + would have no status, version, or headers to populate. + """ + let response = _response as Response val + var body: Array[U8] iso = recover iso Array[U8](_total_size) end + for chunk in _chunks.values() do + body.append(chunk) + end + HTTPResponse( + response.version, + response.status, + response.reason, + response.headers, + consume body) diff --git a/corral/_vendor/courier/response_json.pony b/corral/_vendor/courier/response_json.pony new file mode 100644 index 0000000..0b12f9e --- /dev/null +++ b/corral/_vendor/courier/response_json.pony @@ -0,0 +1,32 @@ +use json = "json" + +primitive ResponseJSON + """ + Parse the body of an `HTTPResponse` as JSON. + + Returns the parsed `JsonValue` on success, or `JsonParseError` if the body + is not valid JSON. This is deliberately minimal — users then use + `JsonNav`, `JsonLens`, pattern matching, or whatever access pattern + they prefer. + + ```pony + use json = "json" + + match ResponseJSON(response) + | let value: json.JsonValue => + // work with the parsed JSON + | let err: json.JsonParseError => + env.out.print("Parse error: " + err.string()) + end + ``` + """ + + fun apply(response: HTTPResponse): (json.JsonValue | json.JsonParseError) => + """ + Parse `response.body` as JSON. + + Converts the body bytes to a `String` and parses with `JsonParser.parse()`. + Returns `JsonParseError` if the body is empty or contains invalid JSON. + """ + let body_str = String.from_array(response.body) + json.JsonParser.parse(body_str) diff --git a/corral/_vendor/courier/scheme.pony b/corral/_vendor/courier/scheme.pony new file mode 100644 index 0000000..850d249 --- /dev/null +++ b/corral/_vendor/courier/scheme.pony @@ -0,0 +1 @@ +type Scheme is ((SchemeHTTP | SchemeHTTPS) & _Scheme) diff --git a/corral/_vendor/courier/send_request_result.pony b/corral/_vendor/courier/send_request_result.pony new file mode 100644 index 0000000..9f08255 --- /dev/null +++ b/corral/_vendor/courier/send_request_result.pony @@ -0,0 +1,20 @@ +primitive SendRequestOK + """ + Request was serialized and sent successfully. + """ + +primitive ConnectionClosed + """ + Connection is not open, or a send failed and the connection was closed. + """ + +primitive ResponsePending + """ + A response to a previous request is still in progress. + """ + +type SendRequestError is (ConnectionClosed | ResponsePending) + """Error returned by `send_request()` when the request cannot be sent.""" + +type SendRequestResult is (SendRequestOK | SendRequestError) + """Result of `send_request()`: either success or an error.""" diff --git a/corral/_vendor/courier/url.pony b/corral/_vendor/courier/url.pony new file mode 100644 index 0000000..a4b53c1 --- /dev/null +++ b/corral/_vendor/courier/url.pony @@ -0,0 +1,205 @@ +primitive URL + """ + Parse URL strings into their components. + + Supports `http` and `https` schemes. Validates the scheme, host, and port; + rejects URLs with userinfo (`user:pass@host`). Fragments are silently + discarded. + + ```pony + match URL.parse("https://example.com:8443/api/v1?key=value") + | let url: ParsedURL => + // url.scheme == SchemeHTTPS + // url.host == "example.com" + // url.port == "8443" + // url.path == "/api/v1" + // url.query == "key=value" + // url.request_path() == "/api/v1?key=value" + // url.is_ssl() == true + | let err: URLParseError => + // handle error + end + ``` + """ + + fun parse(url: String): (ParsedURL val | URLParseError) => + """ + Parse a URL string into its components, or return an error. + """ + // Find :// separator + let sep: ISize = try url.find("://")? else return MissingScheme end + if sep == 0 then return MissingScheme end + + // Match scheme (case-insensitive) + let scheme: Scheme = + if (sep == 4) + and (url.compare_sub("http", 4 where ignore_case = true) is Equal) + then + SchemeHTTP + elseif (sep == 5) + and (url.compare_sub("https", 5 where ignore_case = true) is Equal) + then + SchemeHTTPS + else + return UnsupportedScheme + end + + // Authority starts after :// + var pos: ISize = sep + 3 + let url_size: ISize = url.size().isize() + + // Find end of authority (first /, ?, #, or end of string) + var authority_end: ISize = url_size + var scan: ISize = pos + while scan < url_size do + try + let ch = url(scan.usize())? + if (ch == '/') or (ch == '?') or (ch == '#') then + authority_end = scan + break + end + else + _Unreachable() + end + scan = scan + 1 + end + + // Check for userinfo (@ in authority) + try + let at_pos = url.find("@", pos)? + if at_pos < authority_end then + return UserInfoNotSupported + end + end + + // Parse host + var host: String val = "" + var host_end: ISize = pos + + if pos >= url_size then return MissingHost end + + try + if url(pos.usize())? == '[' then + // IPv6: extract between brackets + host_end = try url.find("]", pos)? else return MissingHost end + if host_end >= authority_end then return MissingHost end + host = url.substring(pos + 1, host_end) + host_end = host_end + 1 + else + // Regular host: ends at : or authority_end + host_end = pos + while host_end < authority_end do + try + if url(host_end.usize())? == ':' then break end + else + _Unreachable() + end + host_end = host_end + 1 + end + host = url.substring(pos, host_end) + end + else + _Unreachable() + end + + if host.size() == 0 then return MissingHost end + + // Parse port + var port: String val = + if scheme is SchemeHTTP then "80" else "443" end + + if host_end < authority_end then + try + if url(host_end.usize())? == ':' then + let port_str: String val = + url.substring(host_end + 1, authority_end) + if port_str.size() > 0 then + // RFC 3986: port = *DIGIT (decimal only, no hex/binary/underscores) + if not _all_digits(port_str) then return InvalidPort end + try + (let port_num, let consumed) = + port_str.read_int[U32](where base = 10)? + if (consumed == 0) or (consumed != port_str.size()) + or (port_num == 0) or (port_num > 65535) + then + return InvalidPort + end + port = port_num.string() + else + return InvalidPort + end + end + end + else + _Unreachable() + end + end + + // Parse path + var path: String val = "/" + var query: (String | None) = None + pos = authority_end + + if pos < url_size then + try + let pos_ch = url(pos.usize())? + if pos_ch == '/' then + // Find end of path (? or # or end) + var path_end: ISize = url_size + scan = pos + while scan < url_size do + try + let pc = url(scan.usize())? + if (pc == '?') or (pc == '#') then + path_end = scan + break + end + else + _Unreachable() + end + scan = scan + 1 + end + path = url.substring(pos, path_end) + pos = path_end + elseif pos_ch == '#' then + pos = url_size + end + else + _Unreachable() + end + end + + // Parse query (from ? to # or end) + if pos < url_size then + try + if url(pos.usize())? == '?' then + var query_end: ISize = url_size + scan = pos + 1 + while scan < url_size do + try + if url(scan.usize())? == '#' then + query_end = scan + break + end + else + _Unreachable() + end + scan = scan + 1 + end + query = url.substring(pos + 1, query_end) + end + else + _Unreachable() + end + end + + ParsedURL._create(scheme, host, port, path, query) + + fun _all_digits(s: String box): Bool => + """ + True if every byte in `s` is an ASCII digit. + """ + for byte in s.values() do + if (byte < '0') or (byte > '9') then return false end + end + true diff --git a/corral/_vendor/courier/url_parse_error.pony b/corral/_vendor/courier/url_parse_error.pony new file mode 100644 index 0000000..18c9c84 --- /dev/null +++ b/corral/_vendor/courier/url_parse_error.pony @@ -0,0 +1,6 @@ +type URLParseError is + ( MissingScheme + | UnsupportedScheme + | MissingHost + | InvalidPort + | UserInfoNotSupported ) diff --git a/corral/_vendor/courier/version.pony b/corral/_vendor/courier/version.pony new file mode 100644 index 0000000..b3f925c --- /dev/null +++ b/corral/_vendor/courier/version.pony @@ -0,0 +1,4 @@ +type Version is ((HTTP10 | HTTP11) & _Version) + """ + HTTP protocol version, either HTTP/1.0 or HTTP/1.1. + """ diff --git a/corral/_vendor/lori/_connection_state.pony b/corral/_vendor/lori/_connection_state.pony new file mode 100644 index 0000000..20a54d0 --- /dev/null +++ b/corral/_vendor/lori/_connection_state.pony @@ -0,0 +1,426 @@ +use "ssl/net" + +trait _ConnectionState + fun ref own_event(conn: TCPConnection ref, flags: U32, arg: U32) + fun ref foreign_event(conn: TCPConnection ref, event: AsioEventID, + flags: U32, arg: U32) + fun ref send(conn: TCPConnection ref, + data: (ByteSeq | ByteSeqIter)): (SendToken | SendError) + fun ref close(conn: TCPConnection ref) + fun ref hard_close(conn: TCPConnection ref) + fun ref start_tls(conn: TCPConnection ref, ssl_ctx: SSLContext val, + host: String): (None | StartTLSError) + fun ref read_again(conn: TCPConnection ref) + fun ref ssl_handshake_complete(conn: TCPConnection ref, + s: EitherLifecycleEventReceiver ref) + fun is_open(): Bool + fun is_closed(): Bool + fun sends_allowed(): Bool + +class _ConnectionNone is _ConnectionState + fun ref own_event(conn: TCPConnection ref, flags: U32, arg: U32) => + _Unreachable() + + fun ref foreign_event(conn: TCPConnection ref, event: AsioEventID, + flags: U32, arg: U32) + => + if not (AsioEvent.writeable(flags) or AsioEvent.readable(flags)) then + return + end + + // The message flags and the event struct's disposable status can + // disagree: a stale message may carry writeable/readable flags while + // the event struct has already been marked disposable by a prior + // unsubscribe. Check the struct before unsubscribing. + if not PonyAsio.get_disposable(event) then + PonyAsio.unsubscribe(event) + end + PonyTCP.close(PonyAsio.event_fd(event)) + + fun ref send(conn: TCPConnection ref, + data: (ByteSeq | ByteSeqIter)): (SendToken | SendError) + => + _Unreachable() + SendErrorNotConnected + + fun ref close(conn: TCPConnection ref) => + _Unreachable() + + fun ref hard_close(conn: TCPConnection ref) => + _Unreachable() + + fun ref start_tls(conn: TCPConnection ref, ssl_ctx: SSLContext val, + host: String): (None | StartTLSError) + => + _Unreachable() + StartTLSNotConnected + + fun ref read_again(conn: TCPConnection ref) => + _Unreachable() + + fun ref ssl_handshake_complete(conn: TCPConnection ref, + s: EitherLifecycleEventReceiver ref) + => + _Unreachable() + + fun is_open(): Bool => false + fun is_closed(): Bool => false + fun sends_allowed(): Bool => false + +class _ClientConnecting is _ConnectionState + fun ref own_event(conn: TCPConnection ref, flags: U32, arg: U32) => + _Unreachable() + + fun ref foreign_event(conn: TCPConnection ref, event: AsioEventID, + flags: U32, arg: U32) + => + if not (AsioEvent.writeable(flags) or AsioEvent.readable(flags)) then + return + end + + let fd = PonyAsio.event_fd(event) + conn._decrement_inflight() + + if conn._is_socket_connected(fd) then + conn._establish_connection(event, fd) + else + conn._connecting_event_failed(event, fd) + end + + fun ref send(conn: TCPConnection ref, + data: (ByteSeq | ByteSeqIter)): (SendToken | SendError) + => + SendErrorNotConnected + + fun ref close(conn: TCPConnection ref) => + conn._set_state(_UnconnectedClosing) + + fun ref hard_close(conn: TCPConnection ref) => + conn._set_state(_Closed) + conn._hard_close_connecting() + + fun ref start_tls(conn: TCPConnection ref, ssl_ctx: SSLContext val, + host: String): (None | StartTLSError) + => + StartTLSNotConnected + + fun ref read_again(conn: TCPConnection ref) => + None + + fun ref ssl_handshake_complete(conn: TCPConnection ref, + s: EitherLifecycleEventReceiver ref) + => + _Unreachable() + + fun is_open(): Bool => false + fun is_closed(): Bool => false + fun sends_allowed(): Bool => false + +class _Open is _ConnectionState + fun ref own_event(conn: TCPConnection ref, flags: U32, arg: U32) => + conn._dispatch_io_event(flags, arg) + + fun ref foreign_event(conn: TCPConnection ref, event: AsioEventID, + flags: U32, arg: U32) + => + // Removing this guard causes the test suite to hang. + if PonyAsio.get_disposable(event) then return end + if not (AsioEvent.writeable(flags) or AsioEvent.readable(flags)) then + return + end + + // Happy Eyeballs straggler — clean up + conn._decrement_inflight() + conn._straggler_cleanup(event) + + fun ref send(conn: TCPConnection ref, + data: (ByteSeq | ByteSeqIter)): (SendToken | SendError) + => + conn._do_send(data) + + fun ref close(conn: TCPConnection ref) => + conn._set_state(_Closing) + conn._initiate_shutdown() + + fun ref hard_close(conn: TCPConnection ref) => + conn._set_state(_Closed) + conn._hard_close_connected() + + fun ref start_tls(conn: TCPConnection ref, ssl_ctx: SSLContext val, + host: String): (None | StartTLSError) + => + conn._do_start_tls(ssl_ctx, host) + + fun ref read_again(conn: TCPConnection ref) => + conn._do_read_again() + + fun ref ssl_handshake_complete(conn: TCPConnection ref, + s: EitherLifecycleEventReceiver ref) + => + _Unreachable() + + fun is_open(): Bool => true + fun is_closed(): Bool => false + fun sends_allowed(): Bool => true + +class _Closing is _ConnectionState + fun ref own_event(conn: TCPConnection ref, flags: U32, arg: U32) => + conn._dispatch_io_event(flags, arg) + + fun ref foreign_event(conn: TCPConnection ref, event: AsioEventID, + flags: U32, arg: U32) + => + // Removing this guard causes the test suite to hang. + if PonyAsio.get_disposable(event) then return end + if not (AsioEvent.writeable(flags) or AsioEvent.readable(flags)) then + return + end + + // Happy Eyeballs straggler — clean up + conn._decrement_inflight() + conn._straggler_cleanup(event) + + // Inflight drained — can now send FIN + conn._initiate_shutdown() + conn._check_shutdown_complete() + + fun ref send(conn: TCPConnection ref, + data: (ByteSeq | ByteSeqIter)): (SendToken | SendError) + => + SendErrorNotConnected + + fun ref close(conn: TCPConnection ref) => + None + + fun ref hard_close(conn: TCPConnection ref) => + conn._set_state(_Closed) + conn._hard_close_connected() + + fun ref start_tls(conn: TCPConnection ref, ssl_ctx: SSLContext val, + host: String): (None | StartTLSError) + => + StartTLSNotConnected + + fun ref read_again(conn: TCPConnection ref) => + conn._do_read_again() + + fun ref ssl_handshake_complete(conn: TCPConnection ref, + s: EitherLifecycleEventReceiver ref) + => + _Unreachable() + + fun is_open(): Bool => false + fun is_closed(): Bool => true + fun sends_allowed(): Bool => false + +class _UnconnectedClosing is _ConnectionState + """ + Draining inflight Happy Eyeballs connections after close() during the + connecting phase. The failure callback is deferred until all inflight + connections drain. hard_close() can interrupt this drain (e.g., connection + timeout fires during drain), transitioning to _Closed immediately. + """ + fun ref own_event(conn: TCPConnection ref, flags: U32, arg: U32) => + _Unreachable() + + fun ref foreign_event(conn: TCPConnection ref, event: AsioEventID, + flags: U32, arg: U32) + => + if not (AsioEvent.writeable(flags) or AsioEvent.readable(flags)) then + return + end + + let remaining = conn._decrement_inflight() + conn._straggler_cleanup(event) + + if remaining == 0 then + conn._set_state(_Closed) + conn._hard_close_connecting() + end + + fun ref send(conn: TCPConnection ref, + data: (ByteSeq | ByteSeqIter)): (SendToken | SendError) + => + SendErrorNotConnected + + fun ref close(conn: TCPConnection ref) => + None + + fun ref hard_close(conn: TCPConnection ref) => + conn._set_state(_Closed) + conn._hard_close_connecting() + + fun ref start_tls(conn: TCPConnection ref, ssl_ctx: SSLContext val, + host: String): (None | StartTLSError) + => + StartTLSNotConnected + + fun ref read_again(conn: TCPConnection ref) => + None + + fun ref ssl_handshake_complete(conn: TCPConnection ref, + s: EitherLifecycleEventReceiver ref) + => + _Unreachable() + + fun is_open(): Bool => false + fun is_closed(): Bool => true + fun sends_allowed(): Bool => false + +class _Closed is _ConnectionState + fun ref own_event(conn: TCPConnection ref, flags: U32, arg: U32) => + None + + fun ref foreign_event(conn: TCPConnection ref, event: AsioEventID, + flags: U32, arg: U32) + => + if not (AsioEvent.writeable(flags) or AsioEvent.readable(flags)) then + return + end + + // Happy Eyeballs straggler — clean up + conn._decrement_inflight() + conn._straggler_cleanup(event) + + fun ref send(conn: TCPConnection ref, + data: (ByteSeq | ByteSeqIter)): (SendToken | SendError) + => + SendErrorNotConnected + + fun ref close(conn: TCPConnection ref) => + None + + fun ref hard_close(conn: TCPConnection ref) => + None + + fun ref start_tls(conn: TCPConnection ref, ssl_ctx: SSLContext val, + host: String): (None | StartTLSError) + => + StartTLSNotConnected + + fun ref read_again(conn: TCPConnection ref) => + None + + fun ref ssl_handshake_complete(conn: TCPConnection ref, + s: EitherLifecycleEventReceiver ref) + => + _Unreachable() + + fun is_open(): Bool => false + fun is_closed(): Bool => true + fun sends_allowed(): Bool => false + +class _SSLHandshaking is _ConnectionState + """ + TCP connected, initial SSL handshake in progress. The application has not + been notified yet — `_on_connected`/`_on_started` fires only after the + handshake completes. + """ + fun ref own_event(conn: TCPConnection ref, flags: U32, arg: U32) => + conn._dispatch_io_event(flags, arg) + + fun ref foreign_event(conn: TCPConnection ref, event: AsioEventID, + flags: U32, arg: U32) + => + // Removing this guard causes the test suite to hang. + if PonyAsio.get_disposable(event) then return end + if not (AsioEvent.writeable(flags) or AsioEvent.readable(flags)) then + return + end + + // Happy Eyeballs straggler — clean up + conn._decrement_inflight() + conn._straggler_cleanup(event) + + fun ref send(conn: TCPConnection ref, + data: (ByteSeq | ByteSeqIter)): (SendToken | SendError) + => + SendErrorNotConnected + + fun ref close(conn: TCPConnection ref) => + // Can't drain gracefully during handshake — nothing to FIN. + conn.hard_close() + + fun ref hard_close(conn: TCPConnection ref) => + conn._set_state(_Closed) + conn._hard_close_ssl_handshaking() + + fun ref start_tls(conn: TCPConnection ref, ssl_ctx: SSLContext val, + host: String): (None | StartTLSError) + => + StartTLSNotConnected + + fun ref read_again(conn: TCPConnection ref) => + conn._do_read_again() + + fun ref ssl_handshake_complete(conn: TCPConnection ref, + s: EitherLifecycleEventReceiver ref) + => + conn._set_state(_Open) + conn._cancel_connect_timer() + conn._arm_idle_timer() + match \exhaustive\ s + | let c: ClientLifecycleEventReceiver ref => + c._on_connected() + | let srv: ServerLifecycleEventReceiver ref => + srv._on_started() + end + + fun is_open(): Bool => false + fun is_closed(): Bool => false + fun sends_allowed(): Bool => false + +class _TLSUpgrading is _ConnectionState + """ + Established connection upgrading to TLS via `start_tls()`. The application + has already been notified of the plaintext connection — `_on_tls_ready` + fires when the handshake completes. + """ + fun ref own_event(conn: TCPConnection ref, flags: U32, arg: U32) => + conn._dispatch_io_event(flags, arg) + + fun ref foreign_event(conn: TCPConnection ref, event: AsioEventID, + flags: U32, arg: U32) + => + // Removing this guard causes the test suite to hang. + if PonyAsio.get_disposable(event) then return end + if not (AsioEvent.writeable(flags) or AsioEvent.readable(flags)) then + return + end + + // Happy Eyeballs straggler — clean up + conn._decrement_inflight() + conn._straggler_cleanup(event) + + fun ref send(conn: TCPConnection ref, + data: (ByteSeq | ByteSeqIter)): (SendToken | SendError) + => + SendErrorNotConnected + + fun ref close(conn: TCPConnection ref) => + // Can't send FIN during TLS handshake. + conn.hard_close() + + fun ref hard_close(conn: TCPConnection ref) => + conn._set_state(_Closed) + conn._hard_close_tls_upgrading() + + fun ref start_tls(conn: TCPConnection ref, ssl_ctx: SSLContext val, + host: String): (None | StartTLSError) + => + StartTLSAlreadyTLS + + fun ref read_again(conn: TCPConnection ref) => + conn._do_read_again() + + fun ref ssl_handshake_complete(conn: TCPConnection ref, + s: EitherLifecycleEventReceiver ref) + => + // TLS upgrade handshake complete — no timer arm needed (timer is + // already running from the plaintext phase). + conn._set_state(_Open) + s._on_tls_ready() + + fun is_open(): Bool => true + fun is_closed(): Bool => false + fun sends_allowed(): Bool => false diff --git a/corral/_vendor/lori/_panics.pony b/corral/_vendor/lori/_panics.pony new file mode 100644 index 0000000..ce871b0 --- /dev/null +++ b/corral/_vendor/lori/_panics.pony @@ -0,0 +1,18 @@ +use @exit[None](status: I32) +use @fprintf[I32](stream: Pointer[None] tag, fmt: Pointer[U8] tag, ...) +use @pony_os_stderr[Pointer[None]]() + +primitive _Unreachable + """ + To be used in places that the compiler can't prove is unreachable but we are + certain is unreachable and if we reach it, we'd be silently hiding a bug. + """ + fun apply(loc: SourceLoc = __loc) => + @fprintf( + @pony_os_stderr(), + ("The unreachable was reached in %s at line %s\n" + + "Please open an issue at https://github.com/ponylang/lori/issues") + .cstring(), + loc.file().cstring(), + loc.line().string().cstring()) + @exit(1) diff --git a/corral/_vendor/lori/auth.pony b/corral/_vendor/lori/auth.pony new file mode 100644 index 0000000..60ffd80 --- /dev/null +++ b/corral/_vendor/lori/auth.pony @@ -0,0 +1,19 @@ +primitive NetAuth + new create(from: AmbientAuth) => + None + +primitive TCPAuth + new create(from: (AmbientAuth | NetAuth)) => + None + +primitive TCPListenAuth + new create(from: (AmbientAuth | NetAuth | TCPAuth)) => + None + +primitive TCPConnectAuth + new create(from: (AmbientAuth | NetAuth | TCPAuth)) => + None + +primitive TCPServerAuth + new create(from: (AmbientAuth | NetAuth | TCPAuth | TCPListenAuth)) => + None diff --git a/corral/_vendor/lori/buffer_size.pony b/corral/_vendor/lori/buffer_size.pony new file mode 100644 index 0000000..4e00ae5 --- /dev/null +++ b/corral/_vendor/lori/buffer_size.pony @@ -0,0 +1,38 @@ +use "constrained_types" + +primitive BufferSizeValidator is Validator[USize] + """ + Validates that a buffer-until value is at least 1. + + A buffer-until of 0 is meaningless — use `Streaming` to indicate "deliver all + available data." Used by `MakeBufferSize` to construct `BufferSize` values. + """ + fun apply(value: USize): ValidationResult => + if value == 0 then + recover val + ValidationFailure("buffer size must be greater than zero") + end + else + ValidationSuccess + end + +type BufferSize is Constrained[USize, BufferSizeValidator] + """ + A validated buffer-until value in bytes. The value must be at least 1. + + Construct with `MakeBufferSize(bytes)`, which returns + `(BufferSize | ValidationFailure)`. Pass to `TCPConnection.buffer_until()`. + Use `Streaming` instead of `BufferSize` to indicate "deliver all available + data." + """ + +type MakeBufferSize is MakeConstrained[USize, BufferSizeValidator] + """ + Factory for `BufferSize` values. Returns `(BufferSize | ValidationFailure)`. + """ + +primitive Streaming + """ + Pass to `TCPConnection.buffer_until()` to indicate streaming mode: deliver + all available data as it arrives, with no buffering threshold. + """ diff --git a/corral/_vendor/lori/connection_failure_reason.pony b/corral/_vendor/lori/connection_failure_reason.pony new file mode 100644 index 0000000..2951233 --- /dev/null +++ b/corral/_vendor/lori/connection_failure_reason.pony @@ -0,0 +1,32 @@ +primitive ConnectionFailedDNS + """ + Name resolution failed — no IP addresses could be resolved for the + given host. No TCP connections were attempted. + """ + +primitive ConnectionFailedTCP + """ + Name resolution succeeded but all TCP connection attempts failed. At least + one IP address was resolved, but no connection could be established. + """ + +primitive ConnectionFailedSSL + """ + The TCP connection was established but the SSL handshake failed. This + covers both SSL session creation failures (e.g. bad `SSLContext`) and + handshake protocol errors before `_on_connected` would have fired. + """ + +primitive ConnectionFailedTimeout + """ + The connection attempt timed out before completing. The timer covers + TCP Happy Eyeballs and (for SSL connections) the TLS handshake. The + timeout is configured via the `connection_timeout` parameter on the + `client` or `ssl_client` constructor. + """ + +type ConnectionFailureReason is + ( ConnectionFailedDNS + | ConnectionFailedTCP + | ConnectionFailedSSL + | ConnectionFailedTimeout ) diff --git a/corral/_vendor/lori/connection_timeout.pony b/corral/_vendor/lori/connection_timeout.pony new file mode 100644 index 0000000..fc5aabd --- /dev/null +++ b/corral/_vendor/lori/connection_timeout.pony @@ -0,0 +1,53 @@ +use "constrained_types" + +primitive ConnectionTimeoutValidator is Validator[U64] + """ + Validates that a connection timeout duration is within the allowed range. + + The minimum value is 1 millisecond. The maximum value is + 18,446,744,073,709 milliseconds (~213,503 days) — the largest value + that can be converted to nanoseconds without overflowing U64. + + Used by `MakeConnectionTimeout` to construct `ConnectionTimeout` values. + """ + fun apply(value: U64): ValidationResult => + if value == 0 then + recover val + ValidationFailure( + "connection timeout must be greater than zero") + end + elseif value > _max_millis() then + recover val + ValidationFailure( + "connection timeout must be at most " + + _max_millis().string() + + " milliseconds") + end + else + ValidationSuccess + end + + fun _max_millis(): U64 => + """ + The maximum connection timeout in milliseconds. Values above this would + overflow U64 when converted to nanoseconds internally. + """ + U64.max_value() / 1_000_000 + +type ConnectionTimeout is Constrained[U64, ConnectionTimeoutValidator] + """ + A validated connection timeout duration in milliseconds. The allowed range is + 1 to 18,446,744,073,709 milliseconds (~213,503 days). The upper bound + ensures the value can be safely converted to nanoseconds without + overflowing U64. + + Construct with `MakeConnectionTimeout(milliseconds)`, which returns + `(ConnectionTimeout | ValidationFailure)`. Pass to the `client` or + `ssl_client` constructor's `connection_timeout` parameter, or pass `None` + to disable it (the default). + """ + +type MakeConnectionTimeout is MakeConstrained[U64, ConnectionTimeoutValidator] + """ + Factory for `ConnectionTimeout` values. Returns `(ConnectionTimeout | ValidationFailure)`. + """ diff --git a/corral/_vendor/lori/idle_timeout.pony b/corral/_vendor/lori/idle_timeout.pony new file mode 100644 index 0000000..f403f2f --- /dev/null +++ b/corral/_vendor/lori/idle_timeout.pony @@ -0,0 +1,52 @@ +use "constrained_types" + +primitive IdleTimeoutValidator is Validator[U64] + """ + Validates that an idle timeout duration is within the allowed range. + + The minimum value is 1 millisecond. The maximum value is + 18,446,744,073,709 milliseconds (~213,503 days) — the largest value + that can be converted to nanoseconds without overflowing U64. + + Used by `MakeIdleTimeout` to construct `IdleTimeout` values. + """ + fun apply(value: U64): ValidationResult => + if value == 0 then + recover val + ValidationFailure( + "idle timeout must be greater than zero") + end + elseif value > _max_millis() then + recover val + ValidationFailure( + "idle timeout must be at most " + + _max_millis().string() + + " milliseconds") + end + else + ValidationSuccess + end + + fun _max_millis(): U64 => + """ + The maximum idle timeout in milliseconds. Values above this would + overflow U64 when converted to nanoseconds internally. + """ + U64.max_value() / 1_000_000 + +type IdleTimeout is Constrained[U64, IdleTimeoutValidator] + """ + A validated idle timeout duration in milliseconds. The allowed range is + 1 to 18,446,744,073,709 milliseconds (~213,503 days). The upper bound + ensures the value can be safely converted to nanoseconds without + overflowing U64. + + Construct with `MakeIdleTimeout(milliseconds)`, which returns + `(IdleTimeout | ValidationFailure)`. Pass to `idle_timeout()` to set the + timeout, or pass `None` to disable it. + """ + +type MakeIdleTimeout is MakeConstrained[U64, IdleTimeoutValidator] + """ + Factory for `IdleTimeout` values. Returns `(IdleTimeout | ValidationFailure)`. + """ diff --git a/corral/_vendor/lori/ip_version.pony b/corral/_vendor/lori/ip_version.pony new file mode 100644 index 0000000..62000a0 --- /dev/null +++ b/corral/_vendor/lori/ip_version.pony @@ -0,0 +1,21 @@ +primitive IP4 + """ + Restrict connections to IPv4 only. The listener or client will use + `pony_os_listen_tcp4` or `pony_os_connect_tcp4`, binding or connecting + exclusively over IPv4. + """ + +primitive IP6 + """ + Restrict connections to IPv6 only. The listener or client will use + `pony_os_listen_tcp6` or `pony_os_connect_tcp6`, binding or connecting + exclusively over IPv6. + """ + +primitive DualStack + """ + Allow both IPv4 and IPv6 (the default). Listeners bind to both protocol + versions. Clients use Happy Eyeballs to try both and pick the fastest. + """ + +type IPVersion is (IP4 | IP6 | DualStack) diff --git a/corral/_vendor/lori/lifecycle_event_receiver.pony b/corral/_vendor/lori/lifecycle_event_receiver.pony new file mode 100644 index 0000000..b2d291b --- /dev/null +++ b/corral/_vendor/lori/lifecycle_event_receiver.pony @@ -0,0 +1,250 @@ +trait ServerLifecycleEventReceiver + """ + Application-level callbacks for server-side TCP connections. + One receiver per connection, no chaining. + """ + fun ref _connection(): TCPConnection + + fun ref _on_started() => + """ + Called when a server connection is ready for application data. + """ + None + + fun ref _on_closed() => + """ + Called when the connection is closed. + """ + None + + fun ref _on_received(data: Array[U8] iso) => + """ + Called each time data is received on this connection. + """ + None + + fun ref _on_throttled() => + """ + Called when we start experiencing backpressure. + """ + None + + fun ref _on_unthrottled() => + """ + Called when backpressure is released. + """ + None + + fun ref _on_sent(token: SendToken) => + """ + Called when data from a successful `send()` has been fully handed to + the OS. The token matches the one returned by the `send()` call. + + Always fires in a subsequent behavior turn, never synchronously during + `send()`. This guarantees the caller has received and processed the + `SendToken` return value before the callback arrives. + """ + None + + fun ref _on_send_failed(token: SendToken) => + """ + Called when data from a successful `send()` could not be delivered to + the OS. The token matches the one returned by the `send()` call. This + happens when a connection closes while a partial write is still pending. + + Always fires in a subsequent behavior turn, never synchronously during + `hard_close()`. Always arrives after `_on_closed`, which fires + synchronously during `hard_close()`. + """ + None + + fun ref _on_start_failure(reason: StartFailureReason) => + """ + Called when a server connection fails to start. This covers failures + that occur before _on_started would have fired, such as an SSL + handshake failure. The application was never notified of the connection + via _on_started. + + The `reason` parameter identifies the cause of the failure. Currently + the only reason is `StartFailedSSL` (SSL session creation or handshake + failure). + """ + None + + fun ref _on_tls_ready() => + """ + Called when a TLS handshake initiated by `start_tls()` completes + successfully. The connection is now encrypted and ready for + application data over TLS. + """ + None + + fun ref _on_tls_failure(reason: TLSFailureReason) => + """ + Called when a TLS handshake initiated by `start_tls()` fails. Fires + synchronously during `hard_close()`, immediately before `_on_closed()`. + The connection was already established (the application received + `_on_started` earlier), so `_on_closed` always follows to signal + connection teardown. + + The `reason` parameter distinguishes authentication failures + (`TLSAuthFailed`) from other protocol errors (`TLSGeneralError`). + """ + None + + fun ref _on_idle_timeout() => + """ + Called when no successful send or receive has occurred for the duration + configured by `idle_timeout()`. This measures application-level inactivity, + not wire-level: pending OS write buffer drains and failed sends + (`SendErrorNotWriteable`) do not count as activity. + + The timer automatically re-arms after each firing. Call + `idle_timeout(None)` to disable it. The application decides what action + to take — close the connection, send a keepalive, log a warning, etc. + """ + None + + fun ref _on_timer(token: TimerToken) => + """ + Called when a one-shot timer created by `set_timer()` fires. The token + matches the one returned by `set_timer()`. + + Fires once per `set_timer()` call. The timer is consumed before the + callback, so it is safe to call `set_timer()` from within `_on_timer()` + to re-arm. No automatic re-arming occurs. + """ + None + +trait ClientLifecycleEventReceiver + """ + Application-level callbacks for client-side TCP connections. + One receiver per connection, no chaining. + """ + fun ref _connection(): TCPConnection + + fun ref _on_connecting(inflight_connections: U32) => + """ + Called if name resolution succeeded for a TCPConnection and we are now + waiting for a connection to the server to succeed. The count is the number + of connections we're trying. This callback will be called each time the + count changes, until a connection is made or _on_connection_failure is + called. + """ + None + + fun ref _on_connected() => + """ + Called when a connection is ready for application data. + """ + None + + fun ref _on_connection_failure(reason: ConnectionFailureReason) => + """ + Called when a connection fails to open. For SSL connections, this is + also called when the SSL handshake fails before _on_connected would + have been delivered, since the application was never notified of the + connection. + + The `reason` parameter identifies the failure stage: + `ConnectionFailedDNS` (name resolution failed), `ConnectionFailedTCP` + (resolved but all TCP attempts failed), `ConnectionFailedSSL` + (TCP connected but SSL handshake failed), or `ConnectionFailedTimeout` + (the connection attempt timed out before completing). + """ + None + + fun ref _on_closed() => + """ + Called when the connection is closed. + """ + None + + fun ref _on_received(data: Array[U8] iso) => + """ + Called each time data is received on this connection. + """ + None + + fun ref _on_throttled() => + """ + Called when we start experiencing backpressure. + """ + None + + fun ref _on_unthrottled() => + """ + Called when backpressure is released. + """ + None + + fun ref _on_sent(token: SendToken) => + """ + Called when data from a successful `send()` has been fully handed to + the OS. The token matches the one returned by the `send()` call. + + Always fires in a subsequent behavior turn, never synchronously during + `send()`. This guarantees the caller has received and processed the + `SendToken` return value before the callback arrives. + """ + None + + fun ref _on_send_failed(token: SendToken) => + """ + Called when data from a successful `send()` could not be delivered to + the OS. The token matches the one returned by the `send()` call. This + happens when a connection closes while a partial write is still pending. + + Always fires in a subsequent behavior turn, never synchronously during + `hard_close()`. Always arrives after `_on_closed`, which fires + synchronously during `hard_close()`. + """ + None + + fun ref _on_tls_ready() => + """ + Called when a TLS handshake initiated by `start_tls()` completes + successfully. The connection is now encrypted and ready for + application data over TLS. + """ + None + + fun ref _on_tls_failure(reason: TLSFailureReason) => + """ + Called when a TLS handshake initiated by `start_tls()` fails. Fires + synchronously during `hard_close()`, immediately before `_on_closed()`. + The connection was already established (the application received + `_on_connected` earlier), so `_on_closed` always follows to signal + connection teardown. + + The `reason` parameter distinguishes authentication failures + (`TLSAuthFailed`) from other protocol errors (`TLSGeneralError`). + """ + None + + fun ref _on_idle_timeout() => + """ + Called when no successful send or receive has occurred for the duration + configured by `idle_timeout()`. This measures application-level inactivity, + not wire-level: pending OS write buffer drains and failed sends + (`SendErrorNotWriteable`) do not count as activity. + + The timer automatically re-arms after each firing. Call + `idle_timeout(None)` to disable it. The application decides what action + to take — close the connection, send a keepalive, log a warning, etc. + """ + None + + fun ref _on_timer(token: TimerToken) => + """ + Called when a one-shot timer created by `set_timer()` fires. The token + matches the one returned by `set_timer()`. + + Fires once per `set_timer()` call. The timer is consumed before the + callback, so it is safe to call `set_timer()` from within `_on_timer()` + to re-arm. No automatic re-arming occurs. + """ + None + +type EitherLifecycleEventReceiver is + (ServerLifecycleEventReceiver | ClientLifecycleEventReceiver) diff --git a/corral/_vendor/lori/lori.pony b/corral/_vendor/lori/lori.pony new file mode 100644 index 0000000..754f212 --- /dev/null +++ b/corral/_vendor/lori/lori.pony @@ -0,0 +1,530 @@ +""" +# Lori Package + +Lori is a TCP networking library that separates connection logic from actor +scheduling. Unlike the standard library's `net` package, which bakes connection +handling into a single actor, lori puts the TCP state machine in a plain +[`TCPConnection`](/lori/lori-TCPConnection/) class that your actor delegates to. +This separation gives you control over how your actor is structured while lori +handles the low-level I/O. + +To build a TCP application with lori, you implement an actor that mixes in two +traits: [`TCPConnectionActor`](/lori/lori-TCPConnectionActor/) (which wires up +the ASIO event plumbing) and a lifecycle event receiver +([`ServerLifecycleEventReceiver`](/lori/lori-ServerLifecycleEventReceiver/) or +[`ClientLifecycleEventReceiver`](/lori/lori-ClientLifecycleEventReceiver/)) that +delivers callbacks like `_on_received`, `_on_connected`, and `_on_closed`. + +## Echo Server + +Here is a complete echo server. It has two actors: a listener that accepts +connections and a connection handler that echoes data back to the client. + +```pony +use "lori" + +actor Main + new create(env: Env) => + EchoServer(TCPListenAuth(env.root), "", "7669", env.out) + +actor EchoServer is TCPListenerActor + var _tcp_listener: TCPListener = TCPListener.none() + let _out: OutStream + let _server_auth: TCPServerAuth + + new create(listen_auth: TCPListenAuth, + host: String, + port: String, + out: OutStream) + => + _out = out + _server_auth = TCPServerAuth(listen_auth) + _tcp_listener = TCPListener(listen_auth, host, port, this) + + fun ref _listener(): TCPListener => + _tcp_listener + + fun ref _on_accept(fd: U32): Echoer => + Echoer(_server_auth, fd, _out) + + fun ref _on_listening() => + _out.print("Echo server started.") + + fun ref _on_listen_failure() => + _out.print("Couldn't start Echo server.") + +actor Echoer is (TCPConnectionActor & ServerLifecycleEventReceiver) + var _tcp_connection: TCPConnection = TCPConnection.none() + let _out: OutStream + + new create(auth: TCPServerAuth, fd: U32, out: OutStream) => + _out = out + _tcp_connection = TCPConnection.server(auth, fd, this, this) + + fun ref _connection(): TCPConnection => + _tcp_connection + + fun ref _on_received(data: Array[U8] iso) => + _tcp_connection.send(consume data) + + fun ref _on_closed() => + _out.print("Connection closed.") +``` + +The listener actor implements +[`TCPListenerActor`](/lori/lori-TCPListenerActor/). It owns a +[`TCPListener`](/lori/lori-TCPListener/) and must provide `_listener()` to +return it. When a client connects, `_on_accept` is called with the raw file +descriptor. You create and return a connection-handling actor from there. + +The connection handler implements both `TCPConnectionActor` and +`ServerLifecycleEventReceiver`. It owns a `TCPConnection` and must provide +`_connection()` to return it. Data arrives via `_on_received`. + +Note the `TCPConnection.none()` and `TCPListener.none()` field initializers. +Pony requires fields to be initialized before the constructor body runs, but the +real connection setup happens asynchronously. The `none()` constructors provide +safe placeholder values that are replaced by real initialization via the +`_finish_initialization` behavior. + +## Client + +Here is a client that connects to a server and sends a message: + +```pony +use "lori" + +actor MyClient is (TCPConnectionActor & ClientLifecycleEventReceiver) + var _tcp_connection: TCPConnection = TCPConnection.none() + + new create(auth: TCPConnectAuth, host: String, port: String) => + _tcp_connection = TCPConnection.client(auth, host, port, "", this, this) + + fun ref _connection(): TCPConnection => + _tcp_connection + + fun ref _on_connected() => + _tcp_connection.send("Hello, server!") + + fun ref _on_connection_failure(reason: ConnectionFailureReason) => + // DNS, TCP, SSL, or timeout failure + None + + fun ref _on_received(data: Array[U8] iso) => + // Handle response from server + None +``` + +Clients use `ClientLifecycleEventReceiver` instead of +`ServerLifecycleEventReceiver`. The key difference is the connection lifecycle: +clients get `_on_connecting` (called as connection attempts are in progress), +`_on_connected` (ready for data), and `_on_connection_failure` (all attempts +failed, with a [`ConnectionFailureReason`](/lori/lori-ConnectionFailureReason/) +indicating the failure stage). Servers get `_on_started` (ready for data) and +`_on_start_failure`. + +## Sending Data + +Unlike many networking libraries, `send()` is fallible. It returns +`(SendToken | SendError)` rather than silently dropping data: + +```pony +match _tcp_connection.send("some data") +| let token: SendToken => + // Data accepted. token will arrive in _on_sent when fully written. + None +| SendErrorNotConnected => + // Connection is not open. + None +| SendErrorNotWriteable => + // Under backpressure. Wait for _on_unthrottled before retrying. + None +end +``` + +[`SendToken`](/lori/lori-SendToken/) is an opaque value identifying the send +operation. When the data has been fully handed to the OS, lori delivers the +same token to `_on_sent`. If the connection closes while a write is still +partially pending, `_on_send_failed` fires instead. Both callbacks always arrive +in a subsequent behavior turn, never during `send()` itself. + +The library does not queue data during backpressure. When `send()` returns +[`SendErrorNotWriteable`](/lori/lori-SendErrorNotWriteable/), the application +decides what to do: queue, drop, or close. Use `_on_throttled` and +`_on_unthrottled` to track backpressure state, or check `is_writeable()` before +calling `send()`. + +`send()` accepts both a single buffer (`ByteSeq`) and multiple buffers +(`ByteSeqIter`). When a protocol sends structured data (e.g. a length header +followed by a payload), passing multiple buffers sends them in a single writev +syscall — avoiding both the per-buffer syscall overhead of calling `send()` +multiple times and the cost of copying into a contiguous buffer: + +```pony +// Single buffer +_tcp_connection.send("Hello, world!") + +// Multiple buffers — one writev syscall +let header: Array[U8] val = _encode_header(payload.size()) +_tcp_connection.send(recover val [as ByteSeq: header; payload] end) +``` + +## SSL + +Adding SSL to a connection requires only a constructor change. Use +`TCPConnection.ssl_client` or `TCPConnection.ssl_server` with an +`SSLContext val`: + +```pony +use "lori" +use "ssl/net" + +actor SSLEchoer is (TCPConnectionActor & ServerLifecycleEventReceiver) + var _tcp_connection: TCPConnection = TCPConnection.none() + + new create(auth: TCPServerAuth, sslctx: SSLContext val, fd: U32) => + _tcp_connection = TCPConnection.ssl_server(auth, sslctx, fd, this, this) + + fun ref _connection(): TCPConnection => + _tcp_connection + + fun ref _on_received(data: Array[U8] iso) => + _tcp_connection.send(consume data) + + fun ref _on_start_failure(reason: StartFailureReason) => + // SSL handshake failed + None +``` + +SSL is handled entirely inside `TCPConnection`. The handshake runs +transparently after the TCP connection is established, and `_on_connected` +(client) or `_on_started` (server) fires only after the handshake completes. If +the handshake fails, clients get `_on_connection_failure` (with +[`ConnectionFailedSSL`](/lori/lori-ConnectionFailedSSL/)) and servers get +`_on_start_failure` (with [`StartFailedSSL`](/lori/lori-StartFailedSSL/)). +The rest of the application code (sending, receiving, closing) is identical +to the non-SSL case. + +## TLS Upgrade (STARTTLS) + +Some protocols (PostgreSQL, SMTP, LDAP) require upgrading an existing plaintext +connection to TLS mid-stream. Use `start_tls()` on an established connection to +initiate a TLS handshake: + +```pony +use "lori" +use "ssl/net" + +actor MyStartTLSClient is (TCPConnectionActor & ClientLifecycleEventReceiver) + var _tcp_connection: TCPConnection = TCPConnection.none() + let _sslctx: SSLContext val + + new create(auth: TCPConnectAuth, sslctx: SSLContext val, + host: String, port: String) + => + _sslctx = sslctx + _tcp_connection = TCPConnection.client(auth, host, port, "", this, this) + + fun ref _connection(): TCPConnection => + _tcp_connection + + fun ref _on_connected() => + // Send protocol-specific upgrade request over plaintext + _tcp_connection.send("STARTTLS") + + fun ref _on_received(data: Array[U8] iso) => + let msg = String.from_array(consume data) + if msg == "OK" then + // Server agreed to upgrade — initiate TLS handshake + match _tcp_connection.start_tls(_sslctx, "localhost") + | let err: StartTLSError => None // handle error + end + end + + fun ref _on_tls_ready() => + // TLS handshake complete — now sending encrypted data + _tcp_connection.send("encrypted payload") + + fun ref _on_tls_failure(reason: TLSFailureReason) => + // TLS handshake failed — _on_closed will follow + None +``` + +`start_tls()` returns `None` when the handshake has been started, or a +[`StartTLSError`](/lori/lori-StartTLSError/) if the upgrade cannot proceed. The +connection must be open, not already TLS, not muted, and have no buffered read +data or pending writes. During the handshake, `send()` returns +`SendErrorNotConnected`. When the handshake completes, `_on_tls_ready()` fires. +If it fails, `_on_tls_failure` fires (with a +[`TLSFailureReason`](/lori/lori-TLSFailureReason/) distinguishing +authentication errors from protocol errors) followed by `_on_closed()`. + +## Idle Timeout + +`idle_timeout()` sets a per-connection timer that fires when no data is sent +or received for the configured duration. Idle timeout is disabled by default. The duration is an +[`IdleTimeout`](/lori/lori-IdleTimeout/) value — a constrained type that +guarantees a millisecond value in the range 1 to 18,446,744,073,709. Pass `None` to disable: + +```pony +fun ref _on_started() => + // Close connections idle for more than 30 seconds + match MakeIdleTimeout(30_000) + | let t: IdleTimeout => + _tcp_connection.idle_timeout(t) + end + +fun ref _on_idle_timeout() => + _tcp_connection.close() +``` + +The timer resets on every successful `send()` and every received data event. +It automatically re-arms after each firing — the application decides what to +do (close, send a keepalive, log, etc.). Call `idle_timeout(None)` to disable. + +Idle timeout uses a per-connection ASIO timer event, requiring no extra actors +or shared state. This avoids the muting-livelock problem that occurs with +shared `Timers` actors under backpressure. + +This is independent of TCP keepalive (`keepalive()`). TCP keepalive is a +transport-level dead-peer probe. Idle timeout is application-level inactivity +detection. + +## Connection Timeout + +Client connections can hang indefinitely when SYN packets are black-holed or an SSL handshake stalls. The `connection_timeout` constructor parameter bounds the connect-to-ready phase — TCP Happy Eyeballs and (for SSL connections) the TLS handshake. If the timeout fires before `_on_connected`, the connection fails with [`ConnectionFailedTimeout`](/lori/lori-ConnectionFailedTimeout/). + +```pony +match MakeConnectionTimeout(5_000) +| let ct: ConnectionTimeout => + _tcp_connection = TCPConnection.client(auth, host, port, "", this, this + where connection_timeout = ct) +end +``` + +Connection timeout is disabled by default (`None`). The duration is a [`ConnectionTimeout`](/lori/lori-ConnectionTimeout/) value — a constrained type with the same range as `IdleTimeout` (1 to 18,446,744,073,709 milliseconds). The timer is a one-shot: it either fires and fails the connection, or is cancelled when the connection becomes ready. + +The timer is armed after `PonyTCP.connect` returns, so it does not cover DNS resolution time. If DNS itself blocks (common with unresponsive nameservers), the total wait will exceed the configured timeout by the DNS resolution time. + +```pony +fun ref _on_connection_failure(reason: ConnectionFailureReason) => + match reason + | ConnectionFailedTimeout => // timed out + | ConnectionFailedDNS => // name resolution failed + | ConnectionFailedTCP => // all TCP attempts failed + | ConnectionFailedSSL => // SSL handshake failed + end +``` + +## General-Purpose Timer + +`set_timer()` creates a one-shot timer that fires `_on_timer()` after a +configured duration. Unlike `idle_timeout()`, this timer has no I/O-reset +behavior — it fires unconditionally regardless of send/receive activity. There +is no automatic re-arming; call `set_timer()` again from `_on_timer()` for +repetition. + +```pony +fun ref _on_started() => + match MakeTimerDuration(10_000) + | let d: TimerDuration => + match _tcp_connection.set_timer(d) + | let t: TimerToken => + _query_timer = t + | let err: SetTimerError => None // handle error + end + end + +fun ref _on_timer(token: TimerToken) => + // Timer fired — take action (close, retry, etc.) + _tcp_connection.close() +``` + +The duration is a [`TimerDuration`](/lori/lori-TimerDuration/) value — a +constrained type with the same range as `IdleTimeout` (1 to +18,446,744,073,709 milliseconds). Only one timer can be active at a time; +calling `set_timer()` while one is active returns +[`SetTimerAlreadyActive`](/lori/lori-SetTimerAlreadyActive/). Cancel with +`cancel_timer(token)` before setting a new one. The timer is cancelled by +`hard_close()` but survives `close()`. + +## Read Yielding + +Under sustained inbound traffic, a single connection's read loop can +monopolize the Pony scheduler. `yield_read()` lets the application exit the +read loop cooperatively, giving other actors a chance to run. Reading resumes +automatically in the next scheduler turn — no explicit `unmute()` is needed. + +```pony +fun ref _on_received(data: Array[U8] iso) => + _received_count = _received_count + 1 + + // Yield every 10 messages to let other actors run + if (_received_count % 10) == 0 then + _tcp_connection.yield_read() + end +``` + +Unlike `mute()`/`unmute()`, which persistently stop reading until reversed, +`yield_read()` is a one-shot pause: the read loop resumes on its own. The +application calls it from `_on_received()` and can implement any yield policy +(message count, byte threshold, time-based, etc.). + +For SSL connections, `yield_read()` operates at TCP-read granularity. All +SSL-decrypted messages from a single TCP read are delivered before the yield +takes effect. + +## Read Buffer Size + +The read buffer defaults to 16KB. To start with a different size, pass a +[`ReadBufferSize`](/lori/lori-ReadBufferSize/) to the constructor: + +```pony +match MakeReadBufferSize(512) +| let rbs: ReadBufferSize => + _tcp_connection = TCPConnection.server(auth, fd, this, this + where read_buffer_size = rbs) +end +``` + +At runtime, use `set_read_buffer_minimum()` to change the shrink-back floor and +`resize_read_buffer()` to force the buffer to a specific size: + +```pony +match MakeReadBufferSize(8192) +| let rbs: ReadBufferSize => + // Raise the minimum for bulk transfer + _tcp_connection.set_read_buffer_minimum(rbs) + // Resize the buffer to match + _tcp_connection.resize_read_buffer(rbs) +end +``` + +The `buffer_until()` method accepts `(BufferSize | Streaming)` where `Streaming` +means "deliver all available data." The invariant chain is: `buffer_until <= +read_buffer_min <= read_buffer_size`. Setting buffer_until above the buffer +minimum returns +[`BufferSizeAboveMinimum`](/lori/lori-BufferSizeAboveMinimum/) — raise the +minimum first, then set buffer_until. Resizing below the current buffer_until +returns +[`ReadBufferResizeBelowBufferSize`](/lori/lori-ReadBufferResizeBelowBufferSize/). +Resizing below the amount of unprocessed data in the buffer returns +[`ReadBufferResizeBelowUsed`](/lori/lori-ReadBufferResizeBelowUsed/). + +When the buffer is empty and larger than the minimum, it automatically shrinks +back to the minimum size. + +## Socket Options + +`TCPConnection` exposes commonly-tuned socket options for connected sockets. +All methods guard with `is_open()` and return an error indicator when the +connection is not open. + +**TCP_NODELAY** disables Nagle's algorithm so small writes are sent immediately: + +```pony +fun ref _on_started() => + // Disable Nagle for low-latency responses + _tcp_connection.set_nodelay(true) +``` + +**OS buffer sizes** control the kernel's receive and send buffers. The OS may +round the requested size up to a platform-specific minimum: + +```pony +fun ref _on_started() => + _tcp_connection.set_so_rcvbuf(65536) + _tcp_connection.set_so_sndbuf(65536) + + // Read back the actual values + (let errno: U32, let actual: U32) = _tcp_connection.get_so_rcvbuf() + if errno == 0 then + // actual may be >= 65536 due to OS rounding + end +``` + +All setters return `U32` — 0 on success, or a non-zero errno on failure. +Getters return `(U32, U32)` — (errno, value). + +**General-purpose access** is available via `getsockopt`/`setsockopt` and their `_u32` variants for any option in [`OSSockOpt`](/lori/lori-OSSockOpt/). For commonly-tuned options, prefer the dedicated methods above. + +```pony +fun ref _on_started() => + // Set TCP_KEEPIDLE via the general-purpose interface + _tcp_connection.setsockopt_u32( + OSSockOpt.ipproto_tcp(), OSSockOpt.tcp_keepidle(), 60) +``` + +## Connection Limits + +`TCPListener` accepts an optional `limit` parameter to cap the number of +concurrent connections. The default limit is 100,000 connections +([`DefaultMaxSpawn`](/lori/lori-DefaultMaxSpawn/)). Pass `None` to disable the +limit entirely: + +```pony +// Use a custom limit +match MakeMaxSpawn(100) +| let limit: MaxSpawn => + _tcp_listener = TCPListener(listen_auth, host, port, this where limit = limit) +end + +// No connection limit +_tcp_listener = TCPListener(listen_auth, host, port, this where limit = None) +``` + +When the limit is reached, the listener pauses accepting. As connections close, +it resumes automatically. + +## IP Version + +By default, lori uses dual-stack connections (both IPv4 and IPv6). To restrict +a client or listener to a specific protocol version, pass an +[`IPVersion`](/lori/lori-IPVersion/) parameter: + +```pony +// IPv4-only listener +_tcp_listener = TCPListener(listen_auth, "127.0.0.1", "7669", this + where ip_version = IP4) + +// IPv6-only client +_tcp_connection = TCPConnection.client(auth, "::1", "7669", "", this, this + where ip_version = IP6) +``` + +[`IP4`](/lori/lori-IP4/) restricts to IPv4 only, +[`IP6`](/lori/lori-IP6/) restricts to IPv6 only, and +[`DualStack`](/lori/lori-DualStack/) (the default) allows both. The same +parameter works on `ssl_client`: + +```pony +_tcp_connection = TCPConnection.ssl_client(auth, sslctx, "127.0.0.1", "7669", + "", this, this where ip_version = IP4) +``` + +Server-side constructors (`server`, `ssl_server`) don't need this parameter — +they accept an already-connected fd whose protocol version was determined by the +listener. + +## Auth Hierarchy + +Lori uses Pony's object capability model for authorization. Each operation +requires a specific auth token, and tokens form a hierarchy — a more powerful +token can create a less powerful one: + +- [`NetAuth`](/lori/lori-NetAuth/) (from `AmbientAuth`) — general network access +- [`TCPAuth`](/lori/lori-TCPAuth/) (from `AmbientAuth` or `NetAuth`) — any TCP + operation +- [`TCPListenAuth`](/lori/lori-TCPListenAuth/) (from `AmbientAuth`, `NetAuth`, + or `TCPAuth`) — open a listener +- [`TCPConnectAuth`](/lori/lori-TCPConnectAuth/) (from `AmbientAuth`, `NetAuth`, + or `TCPAuth`) — open a client connection +- [`TCPServerAuth`](/lori/lori-TCPServerAuth/) (from `AmbientAuth`, `NetAuth`, + `TCPAuth`, or `TCPListenAuth`) — handle an accepted server connection + +In practice, `Main` creates the auth tokens it needs from `env.root` and passes +them to the actors that need them. The echo server example above shows the +typical pattern: `Main` creates a `TCPListenAuth`, the listener creates a +`TCPServerAuth` from it, and each accepted connection receives that +`TCPServerAuth`. +""" diff --git a/corral/_vendor/lori/max_spawn.pony b/corral/_vendor/lori/max_spawn.pony new file mode 100644 index 0000000..08eb8b5 --- /dev/null +++ b/corral/_vendor/lori/max_spawn.pony @@ -0,0 +1,45 @@ +use "constrained_types" + +primitive MaxSpawnValidator is Validator[U32] + """ + Validates that a max spawn value is at least 1. + + A limit of 0 is nonsensical — it would prevent any connections from being + accepted. Used by `MakeMaxSpawn` to construct `MaxSpawn` values. + """ + fun apply(value: U32): ValidationResult => + if value == 0 then + recover val + ValidationFailure("max spawn must be greater than zero") + end + else + ValidationSuccess + end + +type MaxSpawn is Constrained[U32, MaxSpawnValidator] + """ + A validated maximum number of concurrent connections. The value must be + at least 1. + + Construct with `MakeMaxSpawn(count)`, which returns + `(MaxSpawn | ValidationFailure)`. Pass to `TCPListener` via the `limit` + parameter, or pass `None` to disable the connection limit. + """ + +type MakeMaxSpawn is MakeConstrained[U32, MaxSpawnValidator] + """ + Factory for `MaxSpawn` values. Returns `(MaxSpawn | ValidationFailure)`. + """ + +primitive DefaultMaxSpawn + """ + Returns a `MaxSpawn` with the default connection limit of 100,000. + """ + fun apply(): MaxSpawn => + match MakeMaxSpawn(100_000) + | let m: MaxSpawn => m + | let _: ValidationFailure => + // Known unreachable: 100,000 is always valid. + _Unreachable() + apply() + end diff --git a/corral/_vendor/lori/ossocket.pony b/corral/_vendor/lori/ossocket.pony new file mode 100644 index 0000000..520261a --- /dev/null +++ b/corral/_vendor/lori/ossocket.pony @@ -0,0 +1,170 @@ +use @pony_os_errno[I32]() +use @getsockopt[I32](fd: U32, level: I32, option_name: I32, + option_value: Pointer[U8] tag, option_len: Pointer[USize]) +use @setsockopt[I32](fd: U32, level: I32, option_name: I32, + option_value: Pointer[U8] tag, option_len: U32) + +primitive _OSSocket + """ + Socket type-independent wrapper functions for `getsockopt(2)` and + `setsockopt(2)` system calls. + """ + + fun get_so_error(fd: U32): (U32, U32) => + """ + Wrapper for the FFI call `getsockopt(fd, SOL_SOCKET, SO_ERROR, ...)` + """ + getsockopt_u32(fd, OSSockOpt.sol_socket(), OSSockOpt.so_error()) + + fun get_so_rcvbuf(fd: U32): (U32, U32) => + """ + Wrapper for the FFI call `getsockopt(fd, SOL_SOCKET, SO_RCVBUF, ...)` + """ + getsockopt_u32(fd, OSSockOpt.sol_socket(), OSSockOpt.so_rcvbuf()) + + fun get_so_sndbuf(fd: U32): (U32, U32) => + """ + Wrapper for the FFI call `getsockopt(fd, SOL_SOCKET, SO_SNDBUF, ...)` + """ + getsockopt_u32(fd, OSSockOpt.sol_socket(), OSSockOpt.so_sndbuf()) + + fun get_so_connect_time(fd: U32): (U32, U32) => + """ + Wrapper for the FFI call `getsockopt(fd, SOL_SOCKET, SO_CONNECT_TIME, ...)` + """ + getsockopt_u32(fd, OSSockOpt.sol_socket(), OSSockOpt.so_connect_time()) + + fun set_so_rcvbuf(fd: U32, bufsize: U32): U32 => + """ + Wrapper for the FFI call `setsockopt(fd, SOL_SOCKET, SO_RCVBUF, ...)` + """ + setsockopt_u32(fd, OSSockOpt.sol_socket(), OSSockOpt.so_rcvbuf(), bufsize) + + fun set_so_sndbuf(fd: U32, bufsize: U32): U32 => + """ + Wrapper for the FFI call `setsockopt(fd, SOL_SOCKET, SO_SNDBUF, ...)` + """ + setsockopt_u32(fd, OSSockOpt.sol_socket(), OSSockOpt.so_sndbuf(), bufsize) + + fun getsockopt(fd: U32, level: I32, option_name: I32, option_max_size: USize = 4): (U32, Array[U8] iso^) => + """ + General wrapper for sockets to the `getsockopt(2)` system call. + + The `option_max_size` argument is the maximum number of bytes that + the caller expects the kernel to return via the system call's + `void *` 4th argument. This function will allocate a Pony + `Array[U8]` array of size `option_max_size` prior to calling + `getsockopt(2)`. + + In case of system call success, this function returns the 2-tuple: + 1. The integer `0`. + 2. An `Array[U8]` of data returned by the system call's `void *` + 4th argument. Its size is specified by the kernel via the + system call's `sockopt_len_t *` 5th argument. + + In case of system call failure, this function returns the 2-tuple: + 1. The value of `errno`. + 2. An undefined value that must be ignored. + """ + get_so(fd, level, option_name, option_max_size) + + fun getsockopt_u32(fd: U32, level: I32, option_name: I32): (U32, U32) => + """ + Wrapper for sockets to the `getsockopt(2)` system call where + the kernel's returned option value is a C `uint32_t` type / Pony + type `U32`. + + In case of system call success, this function returns the 2-tuple: + 1. The integer `0`. + 2. The `*option_value` returned by the kernel converted to a Pony `U32`. + + In case of system call failure, this function returns the 2-tuple: + 1. The value of `errno`. + 2. An undefined value that must be ignored. + """ + (let errno: U32, let buffer: Array[U8] iso) = + get_so(fd, level, option_name, 4) + + if errno == 0 then + try + (errno, bytes4_to_u32(consume buffer)?) + else + (1, 0) + end + else + (errno, 0) + end + + fun setsockopt(fd: U32, level: I32, option_name: I32, option: Array[U8]): U32 => + """ + General wrapper for sockets to the `setsockopt(2)` system call. + + The caller is responsible for the correct size and byte contents of + the `option` array for the requested `level` and `option_name`, + including using the appropriate CPU endian byte order. + + This function returns `0` on success, else the value of `errno` on + failure. + """ + set_so(fd, level, option_name, option) + + fun setsockopt_u32(fd: U32, level: I32, option_name: I32, option: U32): U32 => + """ + Wrapper for sockets to the `setsockopt(2)` system call where + the kernel expects an option value of a C `uint32_t` type / Pony + type `U32`. + + This function returns `0` on success, else the value of `errno` on + failure. + """ + var word: Array[U8] ref = u32_to_bytes4(option) + set_so(fd, level, option_name, word) + + fun get_so(fd: U32, level: I32, option_name: I32, option_max_size: USize): (U32, Array[U8] iso^) => + """ + Low-level interface to `getsockopt(2)`. + + In case of system call success, this function returns the 2-tuple: + 1. The integer `0`. + 2. An `Array[U8]` of data returned by the system call's `void *` + 4th argument. Its size is specified by the kernel via the + system call's `sockopt_len_t *` 5th argument. + + In case of system call failure, `errno` is returned in the first + element of the 2-tuple, and the second element's value is junk. + """ + var option: Array[U8] iso = recover option.create().>undefined(option_max_size) end + var option_size: USize = option_max_size + let result: I32 = @getsockopt(fd, level, option_name, + option.cpointer(), addressof option_size) + + if result == 0 then + option.truncate(option_size) + (0, consume option) + else + option.truncate(0) + (@pony_os_errno().u32(), consume option) + end + + fun set_so(fd: U32, level: I32, option_name: I32, option: Array[U8]): U32 => + var option_size: U32 = option.size().u32() + """ + Low-level interface to `setsockopt(2)`. + + This function returns `0` on success, else the value of `errno` on + failure. + """ + let result: I32 = @setsockopt(fd, level, option_name, + option.cpointer(), option_size) + + if result == 0 then + 0 + else + @pony_os_errno().u32() + end + + fun bytes4_to_u32(b: Array[U8]): U32 ? => + b.read_u32(0)? + + fun u32_to_bytes4(option: U32): Array[U8] => + Array[U8](4).>push_u32(option) diff --git a/corral/_vendor/lori/ossocketopt.pony b/corral/_vendor/lori/ossocketopt.pony new file mode 100644 index 0000000..fb56356 --- /dev/null +++ b/corral/_vendor/lori/ossocketopt.pony @@ -0,0 +1,1326 @@ +use @pony_os_sockopt_level[I32](option: I32) +use @pony_os_sockopt_option[I32](option: I32) + +primitive OSSockOpt + """ + Convenience functions to fetch the option level and option + name constants (arguments #2 and #3) for the + `getsockopt(2)` and `setsockopt(2)` operating system calls. + + The values of the option level and option name constants are + typically C preprocessor macros, e.g., `#define SOMETHING 42`. + These macro names are upper case and may contain multiple + consecutive underscore characters (though this is rare, for + example, `IP_NAT__XXX`). The function names in this primitive + are derived by the C macro name and then: + + * converted to lower case + * any double underscore (`__`) is converted to a + single underscore (`_`). + + These constants are _not_ stable between Pony releases. + Values returned by this function may be held by long-lived variables + by the calling process: values cannot change while the process runs. + Programmers must not cache any of these values for purposes of + sharing them for use by any other Pony program (for example, + sharing via serialization & deserialization or via direct + shared memory). + + Many functions may return `-1`, which means that the constant's + value could not be determined at the Pony runtime library compile + time. One cause may be that the option truly isn't available, + for example, the option level constant `IPPROTO_3PC` is available + on MacOS 10.x but not on Linux 4.4. Another cause may be the + Pony runtime library's compilation did not include the correct + header file(s) for the target OS platform. + + A third cause of error is due to the regular expression-based + approach used to harvest desirable constants. It is not fool-proof. + The regexp used is too broad and finds some macros that are not + supposed to be used with `getsockopt(2)` and `setsockopt(2)`. + Please consult your platform's documentation to verify the names + of the option level and option name macros. + + The following code fragments are equivalent: set the socket + receive buffer size for the file descriptor `fd` to `4455`. + + ```c + /* In C */ + int option_value = 4455; + setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &option_value, 4); + + /* In Pony */ + var option: I32 = 4455; + @setsockopt[I32](fd, OSSockOpt.sol_socket(), OSSockOpt.so_rcvbuf(), + addressof option, I32(4)) + ``` + """ + + /* Constants are from + * macOS Sierra 10.12.6 + * Ubuntu Linux Xenial/16.04 LTS + kernel 4.4.0-109-generic + * FreeBSD 11.1-RELEASE + * Windows Winsock function reference for getsockopt & setsockopt: + * https://msdn.microsoft.com/en-us/library/windows/desktop/ms738544(v=vs.85).aspx + * https://msdn.microsoft.com/en-us/library/windows/desktop/ms740476(v=vs.85).aspx + + * Harvested by recipe given in socket.c + */ + + /* + * Levels formatted in Pony by: + * egrep '^(IP[A-Z0-6]*PROTO_|NSPROTO_|SOL_)' ~/sum-of-all-constants.txt | egrep -v '\(' | sort -u | egrep -v '^$' | sed 's/__/_/g' | awk 'BEGIN { count=4000; } { printf(" fun %s():I32 => @pony_os_sockopt_level(I32(%d))\n", tolower($1), count++); }' + */ + + // levels + fun ipproto_3pc():I32 => @pony_os_sockopt_level(I32(4000)) + fun ipproto_adfs():I32 => @pony_os_sockopt_level(I32(4001)) + fun ipproto_ah():I32 => @pony_os_sockopt_level(I32(4002)) + fun ipproto_ahip():I32 => @pony_os_sockopt_level(I32(4003)) + fun ipproto_apes():I32 => @pony_os_sockopt_level(I32(4004)) + fun ipproto_argus():I32 => @pony_os_sockopt_level(I32(4005)) + fun ipproto_ax25():I32 => @pony_os_sockopt_level(I32(4006)) + fun ipproto_beetph():I32 => @pony_os_sockopt_level(I32(4007)) + fun ipproto_bha():I32 => @pony_os_sockopt_level(I32(4008)) + fun ipproto_blt():I32 => @pony_os_sockopt_level(I32(4009)) + fun ipproto_brsatmon():I32 => @pony_os_sockopt_level(I32(4010)) + fun ipproto_carp():I32 => @pony_os_sockopt_level(I32(4011)) + fun ipproto_cftp():I32 => @pony_os_sockopt_level(I32(4012)) + fun ipproto_chaos():I32 => @pony_os_sockopt_level(I32(4013)) + fun ipproto_cmtp():I32 => @pony_os_sockopt_level(I32(4014)) + fun ipproto_comp():I32 => @pony_os_sockopt_level(I32(4015)) + fun ipproto_cphb():I32 => @pony_os_sockopt_level(I32(4016)) + fun ipproto_cpnx():I32 => @pony_os_sockopt_level(I32(4017)) + fun ipproto_dccp():I32 => @pony_os_sockopt_level(I32(4018)) + fun ipproto_ddp():I32 => @pony_os_sockopt_level(I32(4019)) + fun ipproto_dgp():I32 => @pony_os_sockopt_level(I32(4020)) + fun ipproto_divert():I32 => @pony_os_sockopt_level(I32(4021)) + fun ipproto_done():I32 => @pony_os_sockopt_level(I32(4022)) + fun ipproto_dstopts():I32 => @pony_os_sockopt_level(I32(4023)) + fun ipproto_egp():I32 => @pony_os_sockopt_level(I32(4024)) + fun ipproto_emcon():I32 => @pony_os_sockopt_level(I32(4025)) + fun ipproto_encap():I32 => @pony_os_sockopt_level(I32(4026)) + fun ipproto_eon():I32 => @pony_os_sockopt_level(I32(4027)) + fun ipproto_esp():I32 => @pony_os_sockopt_level(I32(4028)) + fun ipproto_etherip():I32 => @pony_os_sockopt_level(I32(4029)) + fun ipproto_fragment():I32 => @pony_os_sockopt_level(I32(4030)) + fun ipproto_ggp():I32 => @pony_os_sockopt_level(I32(4031)) + fun ipproto_gmtp():I32 => @pony_os_sockopt_level(I32(4032)) + fun ipproto_gre():I32 => @pony_os_sockopt_level(I32(4033)) + fun ipproto_hello():I32 => @pony_os_sockopt_level(I32(4034)) + fun ipproto_hip():I32 => @pony_os_sockopt_level(I32(4035)) + fun ipproto_hmp():I32 => @pony_os_sockopt_level(I32(4036)) + fun ipproto_hopopts():I32 => @pony_os_sockopt_level(I32(4037)) + fun ipproto_icmp():I32 => @pony_os_sockopt_level(I32(4038)) + fun ipproto_icmpv6():I32 => @pony_os_sockopt_level(I32(4039)) + fun ipproto_idp():I32 => @pony_os_sockopt_level(I32(4040)) + fun ipproto_idpr():I32 => @pony_os_sockopt_level(I32(4041)) + fun ipproto_idrp():I32 => @pony_os_sockopt_level(I32(4042)) + fun ipproto_igmp():I32 => @pony_os_sockopt_level(I32(4043)) + fun ipproto_igp():I32 => @pony_os_sockopt_level(I32(4044)) + fun ipproto_igrp():I32 => @pony_os_sockopt_level(I32(4045)) + fun ipproto_il():I32 => @pony_os_sockopt_level(I32(4046)) + fun ipproto_inlsp():I32 => @pony_os_sockopt_level(I32(4047)) + fun ipproto_inp():I32 => @pony_os_sockopt_level(I32(4048)) + fun ipproto_ip():I32 => @pony_os_sockopt_level(I32(4049)) + fun ipproto_ipcomp():I32 => @pony_os_sockopt_level(I32(4050)) + fun ipproto_ipcv():I32 => @pony_os_sockopt_level(I32(4051)) + fun ipproto_ipeip():I32 => @pony_os_sockopt_level(I32(4052)) + fun ipproto_ipip():I32 => @pony_os_sockopt_level(I32(4053)) + fun ipproto_ippc():I32 => @pony_os_sockopt_level(I32(4054)) + fun ipproto_ipv4():I32 => @pony_os_sockopt_level(I32(4055)) + fun ipproto_ipv6():I32 => @pony_os_sockopt_level(I32(4056)) + fun ipproto_irtp():I32 => @pony_os_sockopt_level(I32(4057)) + fun ipproto_kryptolan():I32 => @pony_os_sockopt_level(I32(4058)) + fun ipproto_larp():I32 => @pony_os_sockopt_level(I32(4059)) + fun ipproto_leaf1():I32 => @pony_os_sockopt_level(I32(4060)) + fun ipproto_leaf2():I32 => @pony_os_sockopt_level(I32(4061)) + fun ipproto_max():I32 => @pony_os_sockopt_level(I32(4062)) + fun ipproto_maxid():I32 => @pony_os_sockopt_level(I32(4063)) + fun ipproto_meas():I32 => @pony_os_sockopt_level(I32(4064)) + fun ipproto_mh():I32 => @pony_os_sockopt_level(I32(4065)) + fun ipproto_mhrp():I32 => @pony_os_sockopt_level(I32(4066)) + fun ipproto_micp():I32 => @pony_os_sockopt_level(I32(4067)) + fun ipproto_mobile():I32 => @pony_os_sockopt_level(I32(4068)) + fun ipproto_mpls():I32 => @pony_os_sockopt_level(I32(4069)) + fun ipproto_mtp():I32 => @pony_os_sockopt_level(I32(4070)) + fun ipproto_mux():I32 => @pony_os_sockopt_level(I32(4071)) + fun ipproto_nd():I32 => @pony_os_sockopt_level(I32(4072)) + fun ipproto_nhrp():I32 => @pony_os_sockopt_level(I32(4073)) + fun ipproto_none():I32 => @pony_os_sockopt_level(I32(4074)) + fun ipproto_nsp():I32 => @pony_os_sockopt_level(I32(4075)) + fun ipproto_nvpii():I32 => @pony_os_sockopt_level(I32(4076)) + fun ipproto_old_divert():I32 => @pony_os_sockopt_level(I32(4077)) + fun ipproto_ospfigp():I32 => @pony_os_sockopt_level(I32(4078)) + fun ipproto_pfsync():I32 => @pony_os_sockopt_level(I32(4079)) + fun ipproto_pgm():I32 => @pony_os_sockopt_level(I32(4080)) + fun ipproto_pigp():I32 => @pony_os_sockopt_level(I32(4081)) + fun ipproto_pim():I32 => @pony_os_sockopt_level(I32(4082)) + fun ipproto_prm():I32 => @pony_os_sockopt_level(I32(4083)) + fun ipproto_pup():I32 => @pony_os_sockopt_level(I32(4084)) + fun ipproto_pvp():I32 => @pony_os_sockopt_level(I32(4085)) + fun ipproto_raw():I32 => @pony_os_sockopt_level(I32(4086)) + fun ipproto_rccmon():I32 => @pony_os_sockopt_level(I32(4087)) + fun ipproto_rdp():I32 => @pony_os_sockopt_level(I32(4088)) + fun ipproto_reserved_253():I32 => @pony_os_sockopt_level(I32(4089)) + fun ipproto_reserved_254():I32 => @pony_os_sockopt_level(I32(4090)) + fun ipproto_routing():I32 => @pony_os_sockopt_level(I32(4091)) + fun ipproto_rsvp():I32 => @pony_os_sockopt_level(I32(4092)) + fun ipproto_rvd():I32 => @pony_os_sockopt_level(I32(4093)) + fun ipproto_satexpak():I32 => @pony_os_sockopt_level(I32(4094)) + fun ipproto_satmon():I32 => @pony_os_sockopt_level(I32(4095)) + fun ipproto_sccsp():I32 => @pony_os_sockopt_level(I32(4096)) + fun ipproto_sctp():I32 => @pony_os_sockopt_level(I32(4097)) + fun ipproto_sdrp():I32 => @pony_os_sockopt_level(I32(4098)) + fun ipproto_send():I32 => @pony_os_sockopt_level(I32(4099)) + fun ipproto_sep():I32 => @pony_os_sockopt_level(I32(4100)) + fun ipproto_shim6():I32 => @pony_os_sockopt_level(I32(4101)) + fun ipproto_skip():I32 => @pony_os_sockopt_level(I32(4102)) + fun ipproto_spacer():I32 => @pony_os_sockopt_level(I32(4103)) + fun ipproto_srpc():I32 => @pony_os_sockopt_level(I32(4104)) + fun ipproto_st():I32 => @pony_os_sockopt_level(I32(4105)) + fun ipproto_svmtp():I32 => @pony_os_sockopt_level(I32(4106)) + fun ipproto_swipe():I32 => @pony_os_sockopt_level(I32(4107)) + fun ipproto_tcf():I32 => @pony_os_sockopt_level(I32(4108)) + fun ipproto_tcp():I32 => @pony_os_sockopt_level(I32(4109)) + fun ipproto_tlsp():I32 => @pony_os_sockopt_level(I32(4110)) + fun ipproto_tp():I32 => @pony_os_sockopt_level(I32(4111)) + fun ipproto_tpxx():I32 => @pony_os_sockopt_level(I32(4112)) + fun ipproto_trunk1():I32 => @pony_os_sockopt_level(I32(4113)) + fun ipproto_trunk2():I32 => @pony_os_sockopt_level(I32(4114)) + fun ipproto_ttp():I32 => @pony_os_sockopt_level(I32(4115)) + fun ipproto_udp():I32 => @pony_os_sockopt_level(I32(4116)) + fun ipproto_udplite():I32 => @pony_os_sockopt_level(I32(4117)) + fun ipproto_vines():I32 => @pony_os_sockopt_level(I32(4118)) + fun ipproto_visa():I32 => @pony_os_sockopt_level(I32(4119)) + fun ipproto_vmtp():I32 => @pony_os_sockopt_level(I32(4120)) + fun ipproto_wbexpak():I32 => @pony_os_sockopt_level(I32(4121)) + fun ipproto_wbmon():I32 => @pony_os_sockopt_level(I32(4122)) + fun ipproto_wsn():I32 => @pony_os_sockopt_level(I32(4123)) + fun ipproto_xnet():I32 => @pony_os_sockopt_level(I32(4124)) + fun ipproto_xtp():I32 => @pony_os_sockopt_level(I32(4125)) + fun sol_atalk():I32 => @pony_os_sockopt_level(I32(4126)) + fun sol_ax25():I32 => @pony_os_sockopt_level(I32(4127)) + fun sol_hci_raw():I32 => @pony_os_sockopt_level(I32(4128)) + fun sol_ipx():I32 => @pony_os_sockopt_level(I32(4129)) + fun sol_l2cap():I32 => @pony_os_sockopt_level(I32(4130)) + fun sol_local():I32 => @pony_os_sockopt_level(I32(4131)) + fun sol_ndrvproto():I32 => @pony_os_sockopt_level(I32(4132)) + fun sol_netrom():I32 => @pony_os_sockopt_level(I32(4133)) + fun sol_rds():I32 => @pony_os_sockopt_level(I32(4134)) + fun sol_rfcomm():I32 => @pony_os_sockopt_level(I32(4135)) + fun sol_rose():I32 => @pony_os_sockopt_level(I32(4136)) + fun sol_sco():I32 => @pony_os_sockopt_level(I32(4137)) + fun sol_socket():I32 => @pony_os_sockopt_level(I32(4138)) + fun sol_tipc():I32 => @pony_os_sockopt_level(I32(4139)) + fun sol_udp():I32 => @pony_os_sockopt_level(I32(4140)) + + /* + * + * Options formatted in Pony by: + * egrep -v '^(IP[A-Z0-6]*PROTO_|NSPROTO_|SOL_)' ~/sum-of-all-constants.txt | egrep -v '\(' | sort -u | egrep -v '^$' | sed 's/__/_/g' | awk 'BEGIN { count=0; } { printf(" fun %s():I32 => @pony_os_sockopt_option(I32(%d))\n", tolower($1), count++); }' + */ + + // options + fun af_coip():I32 => @pony_os_sockopt_option(I32(0)) + fun af_inet():I32 => @pony_os_sockopt_option(I32(1)) + fun af_inet6():I32 => @pony_os_sockopt_option(I32(2)) + fun bluetooth_proto_sco():I32 => @pony_os_sockopt_option(I32(3)) + fun dccp_nr_pkt_types():I32 => @pony_os_sockopt_option(I32(4)) + fun dccp_service_list_max_len():I32 => @pony_os_sockopt_option(I32(5)) + fun dccp_single_opt_maxlen():I32 => @pony_os_sockopt_option(I32(6)) + fun dccp_sockopt_available_ccids():I32 => @pony_os_sockopt_option(I32(7)) + fun dccp_sockopt_ccid():I32 => @pony_os_sockopt_option(I32(8)) + fun dccp_sockopt_ccid_rx_info():I32 => @pony_os_sockopt_option(I32(9)) + fun dccp_sockopt_ccid_tx_info():I32 => @pony_os_sockopt_option(I32(10)) + fun dccp_sockopt_change_l():I32 => @pony_os_sockopt_option(I32(11)) + fun dccp_sockopt_change_r():I32 => @pony_os_sockopt_option(I32(12)) + fun dccp_sockopt_get_cur_mps():I32 => @pony_os_sockopt_option(I32(13)) + fun dccp_sockopt_packet_size():I32 => @pony_os_sockopt_option(I32(14)) + fun dccp_sockopt_qpolicy_id():I32 => @pony_os_sockopt_option(I32(15)) + fun dccp_sockopt_qpolicy_txqlen():I32 => @pony_os_sockopt_option(I32(16)) + fun dccp_sockopt_recv_cscov():I32 => @pony_os_sockopt_option(I32(17)) + fun dccp_sockopt_rx_ccid():I32 => @pony_os_sockopt_option(I32(18)) + fun dccp_sockopt_send_cscov():I32 => @pony_os_sockopt_option(I32(19)) + fun dccp_sockopt_server_timewait():I32 => @pony_os_sockopt_option(I32(20)) + fun dccp_sockopt_service():I32 => @pony_os_sockopt_option(I32(21)) + fun dccp_sockopt_tx_ccid():I32 => @pony_os_sockopt_option(I32(22)) + fun dso_acceptmode():I32 => @pony_os_sockopt_option(I32(23)) + fun dso_conaccept():I32 => @pony_os_sockopt_option(I32(24)) + fun dso_conaccess():I32 => @pony_os_sockopt_option(I32(25)) + fun dso_condata():I32 => @pony_os_sockopt_option(I32(26)) + fun dso_conreject():I32 => @pony_os_sockopt_option(I32(27)) + fun dso_cork():I32 => @pony_os_sockopt_option(I32(28)) + fun dso_disdata():I32 => @pony_os_sockopt_option(I32(29)) + fun dso_info():I32 => @pony_os_sockopt_option(I32(30)) + fun dso_linkinfo():I32 => @pony_os_sockopt_option(I32(31)) + fun dso_max():I32 => @pony_os_sockopt_option(I32(32)) + fun dso_maxwindow():I32 => @pony_os_sockopt_option(I32(33)) + fun dso_nodelay():I32 => @pony_os_sockopt_option(I32(34)) + fun dso_seqpacket():I32 => @pony_os_sockopt_option(I32(35)) + fun dso_services():I32 => @pony_os_sockopt_option(I32(36)) + fun dso_stream():I32 => @pony_os_sockopt_option(I32(37)) + fun icmp_address():I32 => @pony_os_sockopt_option(I32(38)) + fun icmp_addressreply():I32 => @pony_os_sockopt_option(I32(39)) + fun icmp_dest_unreach():I32 => @pony_os_sockopt_option(I32(40)) + fun icmp_echo():I32 => @pony_os_sockopt_option(I32(41)) + fun icmp_echoreply():I32 => @pony_os_sockopt_option(I32(42)) + fun icmp_exc_fragtime():I32 => @pony_os_sockopt_option(I32(43)) + fun icmp_exc_ttl():I32 => @pony_os_sockopt_option(I32(44)) + fun icmp_filter():I32 => @pony_os_sockopt_option(I32(45)) + fun icmp_frag_needed():I32 => @pony_os_sockopt_option(I32(46)) + fun icmp_host_ano():I32 => @pony_os_sockopt_option(I32(47)) + fun icmp_host_isolated():I32 => @pony_os_sockopt_option(I32(48)) + fun icmp_host_unknown():I32 => @pony_os_sockopt_option(I32(49)) + fun icmp_host_unreach():I32 => @pony_os_sockopt_option(I32(50)) + fun icmp_host_unr_tos():I32 => @pony_os_sockopt_option(I32(51)) + fun icmp_info_reply():I32 => @pony_os_sockopt_option(I32(52)) + fun icmp_info_request():I32 => @pony_os_sockopt_option(I32(53)) + fun icmp_net_ano():I32 => @pony_os_sockopt_option(I32(54)) + fun icmp_net_unknown():I32 => @pony_os_sockopt_option(I32(55)) + fun icmp_net_unreach():I32 => @pony_os_sockopt_option(I32(56)) + fun icmp_net_unr_tos():I32 => @pony_os_sockopt_option(I32(57)) + fun icmp_parameterprob():I32 => @pony_os_sockopt_option(I32(58)) + fun icmp_pkt_filtered():I32 => @pony_os_sockopt_option(I32(59)) + fun icmp_port_unreach():I32 => @pony_os_sockopt_option(I32(60)) + fun icmp_prec_cutoff():I32 => @pony_os_sockopt_option(I32(61)) + fun icmp_prec_violation():I32 => @pony_os_sockopt_option(I32(62)) + fun icmp_prot_unreach():I32 => @pony_os_sockopt_option(I32(63)) + fun icmp_redirect():I32 => @pony_os_sockopt_option(I32(64)) + fun icmp_redir_host():I32 => @pony_os_sockopt_option(I32(65)) + fun icmp_redir_hosttos():I32 => @pony_os_sockopt_option(I32(66)) + fun icmp_redir_net():I32 => @pony_os_sockopt_option(I32(67)) + fun icmp_redir_nettos():I32 => @pony_os_sockopt_option(I32(68)) + fun icmp_source_quench():I32 => @pony_os_sockopt_option(I32(69)) + fun icmp_sr_failed():I32 => @pony_os_sockopt_option(I32(70)) + fun icmp_timestamp():I32 => @pony_os_sockopt_option(I32(71)) + fun icmp_timestampreply():I32 => @pony_os_sockopt_option(I32(72)) + fun icmp_time_exceeded():I32 => @pony_os_sockopt_option(I32(73)) + fun ipctl_acceptsourceroute():I32 => @pony_os_sockopt_option(I32(74)) + fun ipctl_defmtu():I32 => @pony_os_sockopt_option(I32(75)) + fun ipctl_defttl():I32 => @pony_os_sockopt_option(I32(76)) + fun ipctl_directedbroadcast():I32 => @pony_os_sockopt_option(I32(77)) + fun ipctl_fastforwarding():I32 => @pony_os_sockopt_option(I32(78)) + fun ipctl_forwarding():I32 => @pony_os_sockopt_option(I32(79)) + fun ipctl_gif_ttl():I32 => @pony_os_sockopt_option(I32(80)) + fun ipctl_intrdqdrops():I32 => @pony_os_sockopt_option(I32(81)) + fun ipctl_intrdqmaxlen():I32 => @pony_os_sockopt_option(I32(82)) + fun ipctl_intrqdrops():I32 => @pony_os_sockopt_option(I32(83)) + fun ipctl_intrqmaxlen():I32 => @pony_os_sockopt_option(I32(84)) + fun ipctl_keepfaith():I32 => @pony_os_sockopt_option(I32(85)) + fun ipctl_maxid():I32 => @pony_os_sockopt_option(I32(86)) + fun ipctl_rtexpire():I32 => @pony_os_sockopt_option(I32(87)) + fun ipctl_rtmaxcache():I32 => @pony_os_sockopt_option(I32(88)) + fun ipctl_rtminexpire():I32 => @pony_os_sockopt_option(I32(89)) + fun ipctl_sendredirects():I32 => @pony_os_sockopt_option(I32(90)) + fun ipctl_sourceroute():I32 => @pony_os_sockopt_option(I32(91)) + fun ipctl_stats():I32 => @pony_os_sockopt_option(I32(92)) + fun ipport_ephemeralfirst():I32 => @pony_os_sockopt_option(I32(93)) + fun ipport_ephemerallast():I32 => @pony_os_sockopt_option(I32(94)) + fun ipport_hifirstauto():I32 => @pony_os_sockopt_option(I32(95)) + fun ipport_hilastauto():I32 => @pony_os_sockopt_option(I32(96)) + fun ipport_max():I32 => @pony_os_sockopt_option(I32(97)) + fun ipport_reserved():I32 => @pony_os_sockopt_option(I32(98)) + fun ipport_reservedstart():I32 => @pony_os_sockopt_option(I32(99)) + fun ipport_userreserved():I32 => @pony_os_sockopt_option(I32(100)) + fun ipv6_2292dstopts():I32 => @pony_os_sockopt_option(I32(101)) + fun ipv6_2292hoplimit():I32 => @pony_os_sockopt_option(I32(102)) + fun ipv6_2292hopopts():I32 => @pony_os_sockopt_option(I32(103)) + fun ipv6_2292pktinfo():I32 => @pony_os_sockopt_option(I32(104)) + fun ipv6_2292pktoptions():I32 => @pony_os_sockopt_option(I32(105)) + fun ipv6_2292rthdr():I32 => @pony_os_sockopt_option(I32(106)) + fun ipv6_addrform():I32 => @pony_os_sockopt_option(I32(107)) + fun ipv6_addr_preferences():I32 => @pony_os_sockopt_option(I32(108)) + fun ipv6_add_membership():I32 => @pony_os_sockopt_option(I32(109)) + fun ipv6_authhdr():I32 => @pony_os_sockopt_option(I32(110)) + fun ipv6_autoflowlabel():I32 => @pony_os_sockopt_option(I32(111)) + fun ipv6_checksum():I32 => @pony_os_sockopt_option(I32(112)) + fun ipv6_dontfrag():I32 => @pony_os_sockopt_option(I32(113)) + fun ipv6_drop_membership():I32 => @pony_os_sockopt_option(I32(114)) + fun ipv6_dstopts():I32 => @pony_os_sockopt_option(I32(115)) + fun ipv6_flowinfo():I32 => @pony_os_sockopt_option(I32(116)) + fun ipv6_flowinfo_flowlabel():I32 => @pony_os_sockopt_option(I32(117)) + fun ipv6_flowinfo_priority():I32 => @pony_os_sockopt_option(I32(118)) + fun ipv6_flowinfo_send():I32 => @pony_os_sockopt_option(I32(119)) + fun ipv6_flowlabel_mgr():I32 => @pony_os_sockopt_option(I32(120)) + fun ipv6_fl_a_get():I32 => @pony_os_sockopt_option(I32(121)) + fun ipv6_fl_a_put():I32 => @pony_os_sockopt_option(I32(122)) + fun ipv6_fl_a_renew():I32 => @pony_os_sockopt_option(I32(123)) + fun ipv6_fl_f_create():I32 => @pony_os_sockopt_option(I32(124)) + fun ipv6_fl_f_excl():I32 => @pony_os_sockopt_option(I32(125)) + fun ipv6_fl_f_reflect():I32 => @pony_os_sockopt_option(I32(126)) + fun ipv6_fl_f_remote():I32 => @pony_os_sockopt_option(I32(127)) + fun ipv6_fl_s_any():I32 => @pony_os_sockopt_option(I32(128)) + fun ipv6_fl_s_excl():I32 => @pony_os_sockopt_option(I32(129)) + fun ipv6_fl_s_none():I32 => @pony_os_sockopt_option(I32(130)) + fun ipv6_fl_s_process():I32 => @pony_os_sockopt_option(I32(131)) + fun ipv6_fl_s_user():I32 => @pony_os_sockopt_option(I32(132)) + fun ipv6_hoplimit():I32 => @pony_os_sockopt_option(I32(133)) + fun ipv6_hopopts():I32 => @pony_os_sockopt_option(I32(134)) + fun ipv6_ipsec_policy():I32 => @pony_os_sockopt_option(I32(135)) + fun ipv6_join_anycast():I32 => @pony_os_sockopt_option(I32(136)) + fun ipv6_leave_anycast():I32 => @pony_os_sockopt_option(I32(137)) + fun ipv6_minhopcount():I32 => @pony_os_sockopt_option(I32(138)) + fun ipv6_mtu():I32 => @pony_os_sockopt_option(I32(139)) + fun ipv6_mtu_discover():I32 => @pony_os_sockopt_option(I32(140)) + fun ipv6_multicast_hops():I32 => @pony_os_sockopt_option(I32(141)) + fun ipv6_multicast_if():I32 => @pony_os_sockopt_option(I32(142)) + fun ipv6_multicast_loop():I32 => @pony_os_sockopt_option(I32(143)) + fun ipv6_nexthop():I32 => @pony_os_sockopt_option(I32(144)) + fun ipv6_origdstaddr():I32 => @pony_os_sockopt_option(I32(145)) + fun ipv6_pathmtu():I32 => @pony_os_sockopt_option(I32(146)) + fun ipv6_pktinfo():I32 => @pony_os_sockopt_option(I32(147)) + fun ipv6_pmtudisc_do():I32 => @pony_os_sockopt_option(I32(148)) + fun ipv6_pmtudisc_dont():I32 => @pony_os_sockopt_option(I32(149)) + fun ipv6_pmtudisc_interface():I32 => @pony_os_sockopt_option(I32(150)) + fun ipv6_pmtudisc_omit():I32 => @pony_os_sockopt_option(I32(151)) + fun ipv6_pmtudisc_probe():I32 => @pony_os_sockopt_option(I32(152)) + fun ipv6_pmtudisc_want():I32 => @pony_os_sockopt_option(I32(153)) + fun ipv6_prefer_src_cga():I32 => @pony_os_sockopt_option(I32(154)) + fun ipv6_prefer_src_coa():I32 => @pony_os_sockopt_option(I32(155)) + fun ipv6_prefer_src_home():I32 => @pony_os_sockopt_option(I32(156)) + fun ipv6_prefer_src_noncga():I32 => @pony_os_sockopt_option(I32(157)) + fun ipv6_prefer_src_public():I32 => @pony_os_sockopt_option(I32(158)) + fun ipv6_prefer_src_pubtmp_default():I32 => @pony_os_sockopt_option(I32(159)) + fun ipv6_prefer_src_tmp():I32 => @pony_os_sockopt_option(I32(160)) + fun ipv6_priority_10():I32 => @pony_os_sockopt_option(I32(161)) + fun ipv6_priority_11():I32 => @pony_os_sockopt_option(I32(162)) + fun ipv6_priority_12():I32 => @pony_os_sockopt_option(I32(163)) + fun ipv6_priority_13():I32 => @pony_os_sockopt_option(I32(164)) + fun ipv6_priority_14():I32 => @pony_os_sockopt_option(I32(165)) + fun ipv6_priority_15():I32 => @pony_os_sockopt_option(I32(166)) + fun ipv6_priority_8():I32 => @pony_os_sockopt_option(I32(167)) + fun ipv6_priority_9():I32 => @pony_os_sockopt_option(I32(168)) + fun ipv6_priority_bulk():I32 => @pony_os_sockopt_option(I32(169)) + fun ipv6_priority_control():I32 => @pony_os_sockopt_option(I32(170)) + fun ipv6_priority_filler():I32 => @pony_os_sockopt_option(I32(171)) + fun ipv6_priority_interactive():I32 => @pony_os_sockopt_option(I32(172)) + fun ipv6_priority_reserved1():I32 => @pony_os_sockopt_option(I32(173)) + fun ipv6_priority_reserved2():I32 => @pony_os_sockopt_option(I32(174)) + fun ipv6_priority_unattended():I32 => @pony_os_sockopt_option(I32(175)) + fun ipv6_priority_uncharacterized():I32 => @pony_os_sockopt_option(I32(176)) + fun ipv6_recvdstopts():I32 => @pony_os_sockopt_option(I32(177)) + fun ipv6_recverr():I32 => @pony_os_sockopt_option(I32(178)) + fun ipv6_recvhoplimit():I32 => @pony_os_sockopt_option(I32(179)) + fun ipv6_recvhopopts():I32 => @pony_os_sockopt_option(I32(180)) + fun ipv6_recvorigdstaddr():I32 => @pony_os_sockopt_option(I32(181)) + fun ipv6_recvpathmtu():I32 => @pony_os_sockopt_option(I32(182)) + fun ipv6_recvpktinfo():I32 => @pony_os_sockopt_option(I32(183)) + fun ipv6_recvrthdr():I32 => @pony_os_sockopt_option(I32(184)) + fun ipv6_recvtclass():I32 => @pony_os_sockopt_option(I32(185)) + fun ipv6_router_alert():I32 => @pony_os_sockopt_option(I32(186)) + fun ipv6_rthdr():I32 => @pony_os_sockopt_option(I32(187)) + fun ipv6_rthdrdstopts():I32 => @pony_os_sockopt_option(I32(188)) + fun ipv6_tclass():I32 => @pony_os_sockopt_option(I32(189)) + fun ipv6_tlv_hao():I32 => @pony_os_sockopt_option(I32(190)) + fun ipv6_tlv_jumbo():I32 => @pony_os_sockopt_option(I32(191)) + fun ipv6_tlv_pad1():I32 => @pony_os_sockopt_option(I32(192)) + fun ipv6_tlv_padn():I32 => @pony_os_sockopt_option(I32(193)) + fun ipv6_tlv_routeralert():I32 => @pony_os_sockopt_option(I32(194)) + fun ipv6_transparent():I32 => @pony_os_sockopt_option(I32(195)) + fun ipv6_unicast_hops():I32 => @pony_os_sockopt_option(I32(196)) + fun ipv6_unicast_if():I32 => @pony_os_sockopt_option(I32(197)) + fun ipv6_use_min_mtu():I32 => @pony_os_sockopt_option(I32(198)) + fun ipv6_v6only():I32 => @pony_os_sockopt_option(I32(199)) + fun ipv6_xfrm_policy():I32 => @pony_os_sockopt_option(I32(200)) + fun ipx_address():I32 => @pony_os_sockopt_option(I32(201)) + fun ipx_address_notify():I32 => @pony_os_sockopt_option(I32(202)) + fun ipx_crtitf():I32 => @pony_os_sockopt_option(I32(203)) + fun ipx_dltitf():I32 => @pony_os_sockopt_option(I32(204)) + fun ipx_dstype():I32 => @pony_os_sockopt_option(I32(205)) + fun ipx_extended_address():I32 => @pony_os_sockopt_option(I32(206)) + fun ipx_filterptype():I32 => @pony_os_sockopt_option(I32(207)) + fun ipx_frame_8022():I32 => @pony_os_sockopt_option(I32(208)) + fun ipx_frame_8023():I32 => @pony_os_sockopt_option(I32(209)) + fun ipx_frame_etherii():I32 => @pony_os_sockopt_option(I32(210)) + fun ipx_frame_none():I32 => @pony_os_sockopt_option(I32(211)) + fun ipx_frame_snap():I32 => @pony_os_sockopt_option(I32(212)) + fun ipx_frame_tr_8022():I32 => @pony_os_sockopt_option(I32(213)) + fun ipx_getnetinfo():I32 => @pony_os_sockopt_option(I32(214)) + fun ipx_getnetinfo_norip():I32 => @pony_os_sockopt_option(I32(215)) + fun ipx_immediatespxack():I32 => @pony_os_sockopt_option(I32(216)) + fun ipx_internal():I32 => @pony_os_sockopt_option(I32(217)) + fun ipx_maxsize():I32 => @pony_os_sockopt_option(I32(218)) + fun ipx_max_adapter_num():I32 => @pony_os_sockopt_option(I32(219)) + fun ipx_mtu():I32 => @pony_os_sockopt_option(I32(220)) + fun ipx_node_len():I32 => @pony_os_sockopt_option(I32(221)) + fun ipx_primary():I32 => @pony_os_sockopt_option(I32(222)) + fun ipx_ptype():I32 => @pony_os_sockopt_option(I32(223)) + fun ipx_receive_broadcast():I32 => @pony_os_sockopt_option(I32(224)) + fun ipx_recvhdr():I32 => @pony_os_sockopt_option(I32(225)) + fun ipx_reripnetnumber():I32 => @pony_os_sockopt_option(I32(226)) + fun ipx_route_no_router():I32 => @pony_os_sockopt_option(I32(227)) + fun ipx_rt_8022():I32 => @pony_os_sockopt_option(I32(228)) + fun ipx_rt_bluebook():I32 => @pony_os_sockopt_option(I32(229)) + fun ipx_rt_routed():I32 => @pony_os_sockopt_option(I32(230)) + fun ipx_rt_snap():I32 => @pony_os_sockopt_option(I32(231)) + fun ipx_special_none():I32 => @pony_os_sockopt_option(I32(232)) + fun ipx_spxgetconnectionstatus():I32 => @pony_os_sockopt_option(I32(233)) + fun ipx_stopfilterptype():I32 => @pony_os_sockopt_option(I32(234)) + fun ipx_type():I32 => @pony_os_sockopt_option(I32(235)) + fun ip_add_membership():I32 => @pony_os_sockopt_option(I32(236)) + fun ip_add_source_membership():I32 => @pony_os_sockopt_option(I32(237)) + fun ip_bindany():I32 => @pony_os_sockopt_option(I32(238)) + fun ip_bindmulti():I32 => @pony_os_sockopt_option(I32(239)) + fun ip_bind_address_no_port():I32 => @pony_os_sockopt_option(I32(240)) + fun ip_block_source():I32 => @pony_os_sockopt_option(I32(241)) + fun ip_bound_if():I32 => @pony_os_sockopt_option(I32(242)) + fun ip_checksum():I32 => @pony_os_sockopt_option(I32(243)) + fun ip_default_multicast_loop():I32 => @pony_os_sockopt_option(I32(244)) + fun ip_default_multicast_ttl():I32 => @pony_os_sockopt_option(I32(245)) + fun ip_dontfrag():I32 => @pony_os_sockopt_option(I32(246)) + fun ip_drop_membership():I32 => @pony_os_sockopt_option(I32(247)) + fun ip_drop_source_membership():I32 => @pony_os_sockopt_option(I32(248)) + fun ip_dummynet3():I32 => @pony_os_sockopt_option(I32(249)) + fun ip_dummynet_configure():I32 => @pony_os_sockopt_option(I32(250)) + fun ip_dummynet_del():I32 => @pony_os_sockopt_option(I32(251)) + fun ip_dummynet_flush():I32 => @pony_os_sockopt_option(I32(252)) + fun ip_dummynet_get():I32 => @pony_os_sockopt_option(I32(253)) + fun ip_faith():I32 => @pony_os_sockopt_option(I32(254)) + fun ip_flowid():I32 => @pony_os_sockopt_option(I32(255)) + fun ip_flowtype():I32 => @pony_os_sockopt_option(I32(256)) + fun ip_freebind():I32 => @pony_os_sockopt_option(I32(257)) + fun ip_fw3():I32 => @pony_os_sockopt_option(I32(258)) + fun ip_fw_add():I32 => @pony_os_sockopt_option(I32(259)) + fun ip_fw_del():I32 => @pony_os_sockopt_option(I32(260)) + fun ip_fw_flush():I32 => @pony_os_sockopt_option(I32(261)) + fun ip_fw_get():I32 => @pony_os_sockopt_option(I32(262)) + fun ip_fw_nat_cfg():I32 => @pony_os_sockopt_option(I32(263)) + fun ip_fw_nat_del():I32 => @pony_os_sockopt_option(I32(264)) + fun ip_fw_nat_get_config():I32 => @pony_os_sockopt_option(I32(265)) + fun ip_fw_nat_get_log():I32 => @pony_os_sockopt_option(I32(266)) + fun ip_fw_resetlog():I32 => @pony_os_sockopt_option(I32(267)) + fun ip_fw_table_add():I32 => @pony_os_sockopt_option(I32(268)) + fun ip_fw_table_del():I32 => @pony_os_sockopt_option(I32(269)) + fun ip_fw_table_flush():I32 => @pony_os_sockopt_option(I32(270)) + fun ip_fw_table_getsize():I32 => @pony_os_sockopt_option(I32(271)) + fun ip_fw_table_list():I32 => @pony_os_sockopt_option(I32(272)) + fun ip_fw_zero():I32 => @pony_os_sockopt_option(I32(273)) + fun ip_hdrincl():I32 => @pony_os_sockopt_option(I32(274)) + fun ip_ipsec_policy():I32 => @pony_os_sockopt_option(I32(275)) + fun ip_max_group_src_filter():I32 => @pony_os_sockopt_option(I32(276)) + fun ip_max_memberships():I32 => @pony_os_sockopt_option(I32(277)) + fun ip_max_sock_mute_filter():I32 => @pony_os_sockopt_option(I32(278)) + fun ip_max_sock_src_filter():I32 => @pony_os_sockopt_option(I32(279)) + fun ip_max_source_filter():I32 => @pony_os_sockopt_option(I32(280)) + fun ip_minttl():I32 => @pony_os_sockopt_option(I32(281)) + fun ip_min_memberships():I32 => @pony_os_sockopt_option(I32(282)) + fun ip_msfilter():I32 => @pony_os_sockopt_option(I32(283)) + fun ip_mtu():I32 => @pony_os_sockopt_option(I32(284)) + fun ip_mtu_discover():I32 => @pony_os_sockopt_option(I32(285)) + fun ip_multicast_all():I32 => @pony_os_sockopt_option(I32(286)) + fun ip_multicast_if():I32 => @pony_os_sockopt_option(I32(287)) + fun ip_multicast_ifindex():I32 => @pony_os_sockopt_option(I32(288)) + fun ip_multicast_loop():I32 => @pony_os_sockopt_option(I32(289)) + fun ip_multicast_ttl():I32 => @pony_os_sockopt_option(I32(290)) + fun ip_multicast_vif():I32 => @pony_os_sockopt_option(I32(291)) + fun ip_nat_xxx():I32 => @pony_os_sockopt_option(I32(292)) + fun ip_nodefrag():I32 => @pony_os_sockopt_option(I32(293)) + fun ip_old_fw_add():I32 => @pony_os_sockopt_option(I32(294)) + fun ip_old_fw_del():I32 => @pony_os_sockopt_option(I32(295)) + fun ip_old_fw_flush():I32 => @pony_os_sockopt_option(I32(296)) + fun ip_old_fw_get():I32 => @pony_os_sockopt_option(I32(297)) + fun ip_old_fw_resetlog():I32 => @pony_os_sockopt_option(I32(298)) + fun ip_old_fw_zero():I32 => @pony_os_sockopt_option(I32(299)) + fun ip_onesbcast():I32 => @pony_os_sockopt_option(I32(300)) + fun ip_options():I32 => @pony_os_sockopt_option(I32(301)) + fun ip_origdstaddr():I32 => @pony_os_sockopt_option(I32(302)) + fun ip_passsec():I32 => @pony_os_sockopt_option(I32(303)) + fun ip_pktinfo():I32 => @pony_os_sockopt_option(I32(304)) + fun ip_pktoptions():I32 => @pony_os_sockopt_option(I32(305)) + fun ip_pmtudisc_do():I32 => @pony_os_sockopt_option(I32(306)) + fun ip_pmtudisc_dont():I32 => @pony_os_sockopt_option(I32(307)) + fun ip_pmtudisc_interface():I32 => @pony_os_sockopt_option(I32(308)) + fun ip_pmtudisc_omit():I32 => @pony_os_sockopt_option(I32(309)) + fun ip_pmtudisc_probe():I32 => @pony_os_sockopt_option(I32(310)) + fun ip_pmtudisc_want():I32 => @pony_os_sockopt_option(I32(311)) + fun ip_portrange():I32 => @pony_os_sockopt_option(I32(312)) + fun ip_portrange_default():I32 => @pony_os_sockopt_option(I32(313)) + fun ip_portrange_high():I32 => @pony_os_sockopt_option(I32(314)) + fun ip_portrange_low():I32 => @pony_os_sockopt_option(I32(315)) + fun ip_recvdstaddr():I32 => @pony_os_sockopt_option(I32(316)) + fun ip_recverr():I32 => @pony_os_sockopt_option(I32(317)) + fun ip_recvflowid():I32 => @pony_os_sockopt_option(I32(318)) + fun ip_recvif():I32 => @pony_os_sockopt_option(I32(319)) + fun ip_recvopts():I32 => @pony_os_sockopt_option(I32(320)) + fun ip_recvorigdstaddr():I32 => @pony_os_sockopt_option(I32(321)) + fun ip_recvpktinfo():I32 => @pony_os_sockopt_option(I32(322)) + fun ip_recvretopts():I32 => @pony_os_sockopt_option(I32(323)) + fun ip_recvrssbucketid():I32 => @pony_os_sockopt_option(I32(324)) + fun ip_recvtos():I32 => @pony_os_sockopt_option(I32(325)) + fun ip_recvttl():I32 => @pony_os_sockopt_option(I32(326)) + fun ip_retopts():I32 => @pony_os_sockopt_option(I32(327)) + fun ip_router_alert():I32 => @pony_os_sockopt_option(I32(328)) + fun ip_rssbucketid():I32 => @pony_os_sockopt_option(I32(329)) + fun ip_rss_listen_bucket():I32 => @pony_os_sockopt_option(I32(330)) + fun ip_rsvp_off():I32 => @pony_os_sockopt_option(I32(331)) + fun ip_rsvp_on():I32 => @pony_os_sockopt_option(I32(332)) + fun ip_rsvp_vif_off():I32 => @pony_os_sockopt_option(I32(333)) + fun ip_rsvp_vif_on():I32 => @pony_os_sockopt_option(I32(334)) + fun ip_sendsrcaddr():I32 => @pony_os_sockopt_option(I32(335)) + fun ip_striphdr():I32 => @pony_os_sockopt_option(I32(336)) + fun ip_tos():I32 => @pony_os_sockopt_option(I32(337)) + fun ip_traffic_mgt_background():I32 => @pony_os_sockopt_option(I32(338)) + fun ip_transparent():I32 => @pony_os_sockopt_option(I32(339)) + fun ip_ttl():I32 => @pony_os_sockopt_option(I32(340)) + fun ip_unblock_source():I32 => @pony_os_sockopt_option(I32(341)) + fun ip_unicast_if():I32 => @pony_os_sockopt_option(I32(342)) + fun ip_xfrm_policy():I32 => @pony_os_sockopt_option(I32(343)) + fun local_connwait():I32 => @pony_os_sockopt_option(I32(344)) + fun local_creds():I32 => @pony_os_sockopt_option(I32(345)) + fun local_peercred():I32 => @pony_os_sockopt_option(I32(346)) + fun local_peerepid():I32 => @pony_os_sockopt_option(I32(347)) + fun local_peereuuid():I32 => @pony_os_sockopt_option(I32(348)) + fun local_peerpid():I32 => @pony_os_sockopt_option(I32(349)) + fun local_peeruuid():I32 => @pony_os_sockopt_option(I32(350)) + fun local_vendor():I32 => @pony_os_sockopt_option(I32(351)) + fun max_tcpoptlen():I32 => @pony_os_sockopt_option(I32(352)) + fun mcast_block_source():I32 => @pony_os_sockopt_option(I32(353)) + fun mcast_exclude():I32 => @pony_os_sockopt_option(I32(354)) + fun mcast_include():I32 => @pony_os_sockopt_option(I32(355)) + fun mcast_join_group():I32 => @pony_os_sockopt_option(I32(356)) + fun mcast_join_source_group():I32 => @pony_os_sockopt_option(I32(357)) + fun mcast_leave_group():I32 => @pony_os_sockopt_option(I32(358)) + fun mcast_leave_source_group():I32 => @pony_os_sockopt_option(I32(359)) + fun mcast_msfilter():I32 => @pony_os_sockopt_option(I32(360)) + fun mcast_unblock_source():I32 => @pony_os_sockopt_option(I32(361)) + fun mcast_undefined():I32 => @pony_os_sockopt_option(I32(362)) + fun mrt_add_bw_upcall():I32 => @pony_os_sockopt_option(I32(363)) + fun mrt_add_mfc():I32 => @pony_os_sockopt_option(I32(364)) + fun mrt_add_vif():I32 => @pony_os_sockopt_option(I32(365)) + fun mrt_api_config():I32 => @pony_os_sockopt_option(I32(366)) + fun mrt_api_flags_all():I32 => @pony_os_sockopt_option(I32(367)) + fun mrt_api_support():I32 => @pony_os_sockopt_option(I32(368)) + fun mrt_assert():I32 => @pony_os_sockopt_option(I32(369)) + fun mrt_del_bw_upcall():I32 => @pony_os_sockopt_option(I32(370)) + fun mrt_del_mfc():I32 => @pony_os_sockopt_option(I32(371)) + fun mrt_del_vif():I32 => @pony_os_sockopt_option(I32(372)) + fun mrt_done():I32 => @pony_os_sockopt_option(I32(373)) + fun mrt_init():I32 => @pony_os_sockopt_option(I32(374)) + fun mrt_mfc_bw_upcall():I32 => @pony_os_sockopt_option(I32(375)) + fun mrt_mfc_flags_all():I32 => @pony_os_sockopt_option(I32(376)) + fun mrt_mfc_flags_border_vif():I32 => @pony_os_sockopt_option(I32(377)) + fun mrt_mfc_flags_disable_wrongvif():I32 => @pony_os_sockopt_option(I32(378)) + fun mrt_mfc_rp():I32 => @pony_os_sockopt_option(I32(379)) + fun mrt_pim():I32 => @pony_os_sockopt_option(I32(380)) + fun mrt_version():I32 => @pony_os_sockopt_option(I32(381)) + fun msg_notification():I32 => @pony_os_sockopt_option(I32(382)) + fun msg_socallbck():I32 => @pony_os_sockopt_option(I32(383)) + fun ndrvproto_ndrv():I32 => @pony_os_sockopt_option(I32(384)) + fun ndrv_addmulticast():I32 => @pony_os_sockopt_option(I32(385)) + fun ndrv_deldmxspec():I32 => @pony_os_sockopt_option(I32(386)) + fun ndrv_delmulticast():I32 => @pony_os_sockopt_option(I32(387)) + fun ndrv_demuxtype_ethertype():I32 => @pony_os_sockopt_option(I32(388)) + fun ndrv_demuxtype_sap():I32 => @pony_os_sockopt_option(I32(389)) + fun ndrv_demuxtype_snap():I32 => @pony_os_sockopt_option(I32(390)) + fun ndrv_dmux_max_descr():I32 => @pony_os_sockopt_option(I32(391)) + fun ndrv_protocol_desc_vers():I32 => @pony_os_sockopt_option(I32(392)) + fun ndrv_setdmxspec():I32 => @pony_os_sockopt_option(I32(393)) + fun netlink_add_membership():I32 => @pony_os_sockopt_option(I32(394)) + fun netlink_audit():I32 => @pony_os_sockopt_option(I32(395)) + fun netlink_broadcast_error():I32 => @pony_os_sockopt_option(I32(396)) + fun netlink_cap_ack():I32 => @pony_os_sockopt_option(I32(397)) + fun netlink_connector():I32 => @pony_os_sockopt_option(I32(398)) + fun netlink_crypto():I32 => @pony_os_sockopt_option(I32(399)) + fun netlink_dnrtmsg():I32 => @pony_os_sockopt_option(I32(400)) + fun netlink_drop_membership():I32 => @pony_os_sockopt_option(I32(401)) + fun netlink_ecryptfs():I32 => @pony_os_sockopt_option(I32(402)) + fun netlink_fib_lookup():I32 => @pony_os_sockopt_option(I32(403)) + fun netlink_firewall():I32 => @pony_os_sockopt_option(I32(404)) + fun netlink_generic():I32 => @pony_os_sockopt_option(I32(405)) + fun netlink_inet_diag():I32 => @pony_os_sockopt_option(I32(406)) + fun netlink_ip6_fw():I32 => @pony_os_sockopt_option(I32(407)) + fun netlink_iscsi():I32 => @pony_os_sockopt_option(I32(408)) + fun netlink_kobject_uevent():I32 => @pony_os_sockopt_option(I32(409)) + fun netlink_listen_all_nsid():I32 => @pony_os_sockopt_option(I32(410)) + fun netlink_list_memberships():I32 => @pony_os_sockopt_option(I32(411)) + fun netlink_netfilter():I32 => @pony_os_sockopt_option(I32(412)) + fun netlink_nflog():I32 => @pony_os_sockopt_option(I32(413)) + fun netlink_no_enobufs():I32 => @pony_os_sockopt_option(I32(414)) + fun netlink_pktinfo():I32 => @pony_os_sockopt_option(I32(415)) + fun netlink_rdma():I32 => @pony_os_sockopt_option(I32(416)) + fun netlink_route():I32 => @pony_os_sockopt_option(I32(417)) + fun netlink_rx_ring():I32 => @pony_os_sockopt_option(I32(418)) + fun netlink_scsitransport():I32 => @pony_os_sockopt_option(I32(419)) + fun netlink_selinux():I32 => @pony_os_sockopt_option(I32(420)) + fun netlink_sock_diag():I32 => @pony_os_sockopt_option(I32(421)) + fun netlink_tx_ring():I32 => @pony_os_sockopt_option(I32(422)) + fun netlink_unused():I32 => @pony_os_sockopt_option(I32(423)) + fun netlink_usersock():I32 => @pony_os_sockopt_option(I32(424)) + fun netlink_xfrm():I32 => @pony_os_sockopt_option(I32(425)) + fun netrom_idle():I32 => @pony_os_sockopt_option(I32(426)) + fun netrom_kill():I32 => @pony_os_sockopt_option(I32(427)) + fun netrom_n2():I32 => @pony_os_sockopt_option(I32(428)) + fun netrom_neigh():I32 => @pony_os_sockopt_option(I32(429)) + fun netrom_node():I32 => @pony_os_sockopt_option(I32(430)) + fun netrom_paclen():I32 => @pony_os_sockopt_option(I32(431)) + fun netrom_t1():I32 => @pony_os_sockopt_option(I32(432)) + fun netrom_t2():I32 => @pony_os_sockopt_option(I32(433)) + fun netrom_t4():I32 => @pony_os_sockopt_option(I32(434)) + fun nrdv_multicast_addrs_per_sock():I32 => @pony_os_sockopt_option(I32(435)) + fun pvd_config():I32 => @pony_os_sockopt_option(I32(436)) + fun rds_cancel_sent_to():I32 => @pony_os_sockopt_option(I32(437)) + fun rds_cmsg_atomic_cswp():I32 => @pony_os_sockopt_option(I32(438)) + fun rds_cmsg_atomic_fadd():I32 => @pony_os_sockopt_option(I32(439)) + fun rds_cmsg_cong_update():I32 => @pony_os_sockopt_option(I32(440)) + fun rds_cmsg_masked_atomic_cswp():I32 => @pony_os_sockopt_option(I32(441)) + fun rds_cmsg_masked_atomic_fadd():I32 => @pony_os_sockopt_option(I32(442)) + fun rds_cmsg_rdma_args():I32 => @pony_os_sockopt_option(I32(443)) + fun rds_cmsg_rdma_dest():I32 => @pony_os_sockopt_option(I32(444)) + fun rds_cmsg_rdma_map():I32 => @pony_os_sockopt_option(I32(445)) + fun rds_cmsg_rdma_status():I32 => @pony_os_sockopt_option(I32(446)) + fun rds_cong_monitor():I32 => @pony_os_sockopt_option(I32(447)) + fun rds_cong_monitor_size():I32 => @pony_os_sockopt_option(I32(448)) + fun rds_free_mr():I32 => @pony_os_sockopt_option(I32(449)) + fun rds_get_mr():I32 => @pony_os_sockopt_option(I32(450)) + fun rds_get_mr_for_dest():I32 => @pony_os_sockopt_option(I32(451)) + fun rds_ib_abi_version():I32 => @pony_os_sockopt_option(I32(452)) + fun rds_ib_gid_len():I32 => @pony_os_sockopt_option(I32(453)) + fun rds_info_connections():I32 => @pony_os_sockopt_option(I32(454)) + fun rds_info_connection_flag_connected():I32 => @pony_os_sockopt_option(I32(455)) + fun rds_info_connection_flag_connecting():I32 => @pony_os_sockopt_option(I32(456)) + fun rds_info_connection_flag_sending():I32 => @pony_os_sockopt_option(I32(457)) + fun rds_info_connection_stats():I32 => @pony_os_sockopt_option(I32(458)) + fun rds_info_counters():I32 => @pony_os_sockopt_option(I32(459)) + fun rds_info_first():I32 => @pony_os_sockopt_option(I32(460)) + fun rds_info_ib_connections():I32 => @pony_os_sockopt_option(I32(461)) + fun rds_info_iwarp_connections():I32 => @pony_os_sockopt_option(I32(462)) + fun rds_info_last():I32 => @pony_os_sockopt_option(I32(463)) + fun rds_info_message_flag_ack():I32 => @pony_os_sockopt_option(I32(464)) + fun rds_info_message_flag_fast_ack():I32 => @pony_os_sockopt_option(I32(465)) + fun rds_info_recv_messages():I32 => @pony_os_sockopt_option(I32(466)) + fun rds_info_retrans_messages():I32 => @pony_os_sockopt_option(I32(467)) + fun rds_info_send_messages():I32 => @pony_os_sockopt_option(I32(468)) + fun rds_info_sockets():I32 => @pony_os_sockopt_option(I32(469)) + fun rds_info_tcp_sockets():I32 => @pony_os_sockopt_option(I32(470)) + fun rds_rdma_canceled():I32 => @pony_os_sockopt_option(I32(471)) + fun rds_rdma_dontwait():I32 => @pony_os_sockopt_option(I32(472)) + fun rds_rdma_dropped():I32 => @pony_os_sockopt_option(I32(473)) + fun rds_rdma_fence():I32 => @pony_os_sockopt_option(I32(474)) + fun rds_rdma_invalidate():I32 => @pony_os_sockopt_option(I32(475)) + fun rds_rdma_notify_me():I32 => @pony_os_sockopt_option(I32(476)) + fun rds_rdma_other_error():I32 => @pony_os_sockopt_option(I32(477)) + fun rds_rdma_readwrite():I32 => @pony_os_sockopt_option(I32(478)) + fun rds_rdma_remote_error():I32 => @pony_os_sockopt_option(I32(479)) + fun rds_rdma_silent():I32 => @pony_os_sockopt_option(I32(480)) + fun rds_rdma_success():I32 => @pony_os_sockopt_option(I32(481)) + fun rds_rdma_use_once():I32 => @pony_os_sockopt_option(I32(482)) + fun rds_recverr():I32 => @pony_os_sockopt_option(I32(483)) + fun rds_trans_count():I32 => @pony_os_sockopt_option(I32(484)) + fun rds_trans_ib():I32 => @pony_os_sockopt_option(I32(485)) + fun rds_trans_iwarp():I32 => @pony_os_sockopt_option(I32(486)) + fun rds_trans_none():I32 => @pony_os_sockopt_option(I32(487)) + fun rds_trans_tcp():I32 => @pony_os_sockopt_option(I32(488)) + fun rose_access_barred():I32 => @pony_os_sockopt_option(I32(489)) + fun rose_defer():I32 => @pony_os_sockopt_option(I32(490)) + fun rose_dte_originated():I32 => @pony_os_sockopt_option(I32(491)) + fun rose_holdback():I32 => @pony_os_sockopt_option(I32(492)) + fun rose_idle():I32 => @pony_os_sockopt_option(I32(493)) + fun rose_invalid_facility():I32 => @pony_os_sockopt_option(I32(494)) + fun rose_local_procedure():I32 => @pony_os_sockopt_option(I32(495)) + fun rose_max_digis():I32 => @pony_os_sockopt_option(I32(496)) + fun rose_mtu():I32 => @pony_os_sockopt_option(I32(497)) + fun rose_network_congestion():I32 => @pony_os_sockopt_option(I32(498)) + fun rose_not_obtainable():I32 => @pony_os_sockopt_option(I32(499)) + fun rose_number_busy():I32 => @pony_os_sockopt_option(I32(500)) + fun rose_out_of_order():I32 => @pony_os_sockopt_option(I32(501)) + fun rose_qbitincl():I32 => @pony_os_sockopt_option(I32(502)) + fun rose_remote_procedure():I32 => @pony_os_sockopt_option(I32(503)) + fun rose_ship_absent():I32 => @pony_os_sockopt_option(I32(504)) + fun rose_t1():I32 => @pony_os_sockopt_option(I32(505)) + fun rose_t2():I32 => @pony_os_sockopt_option(I32(506)) + fun rose_t3():I32 => @pony_os_sockopt_option(I32(507)) + fun scm_hci_raw_direction():I32 => @pony_os_sockopt_option(I32(508)) + fun scm_timestamp():I32 => @pony_os_sockopt_option(I32(509)) + fun scm_timestamping():I32 => @pony_os_sockopt_option(I32(510)) + fun scm_timestampns():I32 => @pony_os_sockopt_option(I32(511)) + fun scm_wifi_status():I32 => @pony_os_sockopt_option(I32(512)) + fun sctp_abort_association():I32 => @pony_os_sockopt_option(I32(513)) + fun sctp_adaptation_layer():I32 => @pony_os_sockopt_option(I32(514)) + fun sctp_adaption_layer():I32 => @pony_os_sockopt_option(I32(515)) + fun sctp_add_streams():I32 => @pony_os_sockopt_option(I32(516)) + fun sctp_add_vrf_id():I32 => @pony_os_sockopt_option(I32(517)) + fun sctp_asconf():I32 => @pony_os_sockopt_option(I32(518)) + fun sctp_asconf_ack():I32 => @pony_os_sockopt_option(I32(519)) + fun sctp_asconf_supported():I32 => @pony_os_sockopt_option(I32(520)) + fun sctp_associnfo():I32 => @pony_os_sockopt_option(I32(521)) + fun sctp_authentication():I32 => @pony_os_sockopt_option(I32(522)) + fun sctp_auth_active_key():I32 => @pony_os_sockopt_option(I32(523)) + fun sctp_auth_chunk():I32 => @pony_os_sockopt_option(I32(524)) + fun sctp_auth_deactivate_key():I32 => @pony_os_sockopt_option(I32(525)) + fun sctp_auth_delete_key():I32 => @pony_os_sockopt_option(I32(526)) + fun sctp_auth_key():I32 => @pony_os_sockopt_option(I32(527)) + fun sctp_auth_supported():I32 => @pony_os_sockopt_option(I32(528)) + fun sctp_autoclose():I32 => @pony_os_sockopt_option(I32(529)) + fun sctp_auto_asconf():I32 => @pony_os_sockopt_option(I32(530)) + fun sctp_badcrc():I32 => @pony_os_sockopt_option(I32(531)) + fun sctp_bindx_add_addr():I32 => @pony_os_sockopt_option(I32(532)) + fun sctp_bindx_rem_addr():I32 => @pony_os_sockopt_option(I32(533)) + fun sctp_blk_logging_enable():I32 => @pony_os_sockopt_option(I32(534)) + fun sctp_bound():I32 => @pony_os_sockopt_option(I32(535)) + fun sctp_cause_cookie_in_shutdown():I32 => @pony_os_sockopt_option(I32(536)) + fun sctp_cause_deleting_last_addr():I32 => @pony_os_sockopt_option(I32(537)) + fun sctp_cause_deleting_src_addr():I32 => @pony_os_sockopt_option(I32(538)) + fun sctp_cause_illegal_asconf_ack():I32 => @pony_os_sockopt_option(I32(539)) + fun sctp_cause_invalid_param():I32 => @pony_os_sockopt_option(I32(540)) + fun sctp_cause_invalid_stream():I32 => @pony_os_sockopt_option(I32(541)) + fun sctp_cause_missing_param():I32 => @pony_os_sockopt_option(I32(542)) + fun sctp_cause_nat_colliding_state():I32 => @pony_os_sockopt_option(I32(543)) + fun sctp_cause_nat_missing_state():I32 => @pony_os_sockopt_option(I32(544)) + fun sctp_cause_no_error():I32 => @pony_os_sockopt_option(I32(545)) + fun sctp_cause_no_user_data():I32 => @pony_os_sockopt_option(I32(546)) + fun sctp_cause_out_of_resc():I32 => @pony_os_sockopt_option(I32(547)) + fun sctp_cause_protocol_violation():I32 => @pony_os_sockopt_option(I32(548)) + fun sctp_cause_request_refused():I32 => @pony_os_sockopt_option(I32(549)) + fun sctp_cause_resource_shortage():I32 => @pony_os_sockopt_option(I32(550)) + fun sctp_cause_restart_w_newaddr():I32 => @pony_os_sockopt_option(I32(551)) + fun sctp_cause_stale_cookie():I32 => @pony_os_sockopt_option(I32(552)) + fun sctp_cause_unrecog_chunk():I32 => @pony_os_sockopt_option(I32(553)) + fun sctp_cause_unrecog_param():I32 => @pony_os_sockopt_option(I32(554)) + fun sctp_cause_unresolvable_addr():I32 => @pony_os_sockopt_option(I32(555)) + fun sctp_cause_unsupported_hmacid():I32 => @pony_os_sockopt_option(I32(556)) + fun sctp_cause_user_initiated_abt():I32 => @pony_os_sockopt_option(I32(557)) + fun sctp_cc_hstcp():I32 => @pony_os_sockopt_option(I32(558)) + fun sctp_cc_htcp():I32 => @pony_os_sockopt_option(I32(559)) + fun sctp_cc_option():I32 => @pony_os_sockopt_option(I32(560)) + fun sctp_cc_opt_rtcc_setmode():I32 => @pony_os_sockopt_option(I32(561)) + fun sctp_cc_opt_steady_step():I32 => @pony_os_sockopt_option(I32(562)) + fun sctp_cc_opt_use_dccc_ecn():I32 => @pony_os_sockopt_option(I32(563)) + fun sctp_cc_rfc2581():I32 => @pony_os_sockopt_option(I32(564)) + fun sctp_cc_rtcc():I32 => @pony_os_sockopt_option(I32(565)) + fun sctp_closed():I32 => @pony_os_sockopt_option(I32(566)) + fun sctp_clr_stat_log():I32 => @pony_os_sockopt_option(I32(567)) + fun sctp_cmt_base():I32 => @pony_os_sockopt_option(I32(568)) + fun sctp_cmt_max():I32 => @pony_os_sockopt_option(I32(569)) + fun sctp_cmt_mptcp():I32 => @pony_os_sockopt_option(I32(570)) + fun sctp_cmt_off():I32 => @pony_os_sockopt_option(I32(571)) + fun sctp_cmt_on_off():I32 => @pony_os_sockopt_option(I32(572)) + fun sctp_cmt_rpv1():I32 => @pony_os_sockopt_option(I32(573)) + fun sctp_cmt_rpv2():I32 => @pony_os_sockopt_option(I32(574)) + fun sctp_cmt_use_dac():I32 => @pony_os_sockopt_option(I32(575)) + fun sctp_connect_x():I32 => @pony_os_sockopt_option(I32(576)) + fun sctp_connect_x_complete():I32 => @pony_os_sockopt_option(I32(577)) + fun sctp_connect_x_delayed():I32 => @pony_os_sockopt_option(I32(578)) + fun sctp_context():I32 => @pony_os_sockopt_option(I32(579)) + fun sctp_cookie_ack():I32 => @pony_os_sockopt_option(I32(580)) + fun sctp_cookie_echo():I32 => @pony_os_sockopt_option(I32(581)) + fun sctp_cookie_echoed():I32 => @pony_os_sockopt_option(I32(582)) + fun sctp_cookie_wait():I32 => @pony_os_sockopt_option(I32(583)) + fun sctp_cwnd_logging_enable():I32 => @pony_os_sockopt_option(I32(584)) + fun sctp_cwnd_monitor_enable():I32 => @pony_os_sockopt_option(I32(585)) + fun sctp_cwr_in_same_window():I32 => @pony_os_sockopt_option(I32(586)) + fun sctp_cwr_reduce_override():I32 => @pony_os_sockopt_option(I32(587)) + fun sctp_data():I32 => @pony_os_sockopt_option(I32(588)) + fun sctp_data_first_frag():I32 => @pony_os_sockopt_option(I32(589)) + fun sctp_data_frag_mask():I32 => @pony_os_sockopt_option(I32(590)) + fun sctp_data_last_frag():I32 => @pony_os_sockopt_option(I32(591)) + fun sctp_data_middle_frag():I32 => @pony_os_sockopt_option(I32(592)) + fun sctp_data_not_frag():I32 => @pony_os_sockopt_option(I32(593)) + fun sctp_data_sack_immediately():I32 => @pony_os_sockopt_option(I32(594)) + fun sctp_data_unordered():I32 => @pony_os_sockopt_option(I32(595)) + fun sctp_default_prinfo():I32 => @pony_os_sockopt_option(I32(596)) + fun sctp_default_send_param():I32 => @pony_os_sockopt_option(I32(597)) + fun sctp_default_sndinfo():I32 => @pony_os_sockopt_option(I32(598)) + fun sctp_delayed_sack():I32 => @pony_os_sockopt_option(I32(599)) + fun sctp_del_vrf_id():I32 => @pony_os_sockopt_option(I32(600)) + fun sctp_disable_fragments():I32 => @pony_os_sockopt_option(I32(601)) + fun sctp_ecn_cwr():I32 => @pony_os_sockopt_option(I32(602)) + fun sctp_ecn_echo():I32 => @pony_os_sockopt_option(I32(603)) + fun sctp_ecn_supported():I32 => @pony_os_sockopt_option(I32(604)) + fun sctp_enable_change_assoc_req():I32 => @pony_os_sockopt_option(I32(605)) + fun sctp_enable_reset_assoc_req():I32 => @pony_os_sockopt_option(I32(606)) + fun sctp_enable_reset_stream_req():I32 => @pony_os_sockopt_option(I32(607)) + fun sctp_enable_stream_reset():I32 => @pony_os_sockopt_option(I32(608)) + fun sctp_enable_value_mask():I32 => @pony_os_sockopt_option(I32(609)) + fun sctp_established():I32 => @pony_os_sockopt_option(I32(610)) + fun sctp_event():I32 => @pony_os_sockopt_option(I32(611)) + fun sctp_events():I32 => @pony_os_sockopt_option(I32(612)) + fun sctp_explicit_eor():I32 => @pony_os_sockopt_option(I32(613)) + fun sctp_flight_logging_enable():I32 => @pony_os_sockopt_option(I32(614)) + fun sctp_forward_cum_tsn():I32 => @pony_os_sockopt_option(I32(615)) + fun sctp_fragment_interleave():I32 => @pony_os_sockopt_option(I32(616)) + fun sctp_frag_level_0():I32 => @pony_os_sockopt_option(I32(617)) + fun sctp_frag_level_1():I32 => @pony_os_sockopt_option(I32(618)) + fun sctp_frag_level_2():I32 => @pony_os_sockopt_option(I32(619)) + fun sctp_from_middle_box():I32 => @pony_os_sockopt_option(I32(620)) + fun sctp_fr_logging_enable():I32 => @pony_os_sockopt_option(I32(621)) + fun sctp_get_addr_len():I32 => @pony_os_sockopt_option(I32(622)) + fun sctp_get_asoc_vrf():I32 => @pony_os_sockopt_option(I32(623)) + fun sctp_get_assoc_id_list():I32 => @pony_os_sockopt_option(I32(624)) + fun sctp_get_assoc_number():I32 => @pony_os_sockopt_option(I32(625)) + fun sctp_get_local_addresses():I32 => @pony_os_sockopt_option(I32(626)) + fun sctp_get_local_addr_size():I32 => @pony_os_sockopt_option(I32(627)) + fun sctp_get_nonce_values():I32 => @pony_os_sockopt_option(I32(628)) + fun sctp_get_packet_log():I32 => @pony_os_sockopt_option(I32(629)) + fun sctp_get_peer_addresses():I32 => @pony_os_sockopt_option(I32(630)) + fun sctp_get_peer_addr_info():I32 => @pony_os_sockopt_option(I32(631)) + fun sctp_get_remote_addr_size():I32 => @pony_os_sockopt_option(I32(632)) + fun sctp_get_sndbuf_use():I32 => @pony_os_sockopt_option(I32(633)) + fun sctp_get_stat_log():I32 => @pony_os_sockopt_option(I32(634)) + fun sctp_get_vrf_ids():I32 => @pony_os_sockopt_option(I32(635)) + fun sctp_had_no_tcb():I32 => @pony_os_sockopt_option(I32(636)) + fun sctp_heartbeat_ack():I32 => @pony_os_sockopt_option(I32(637)) + fun sctp_heartbeat_request():I32 => @pony_os_sockopt_option(I32(638)) + fun sctp_hmac_ident():I32 => @pony_os_sockopt_option(I32(639)) + fun sctp_idata():I32 => @pony_os_sockopt_option(I32(640)) + fun sctp_iforward_cum_tsn():I32 => @pony_os_sockopt_option(I32(641)) + fun sctp_initiation():I32 => @pony_os_sockopt_option(I32(642)) + fun sctp_initiation_ack():I32 => @pony_os_sockopt_option(I32(643)) + fun sctp_initmsg():I32 => @pony_os_sockopt_option(I32(644)) + fun sctp_interleaving_supported():I32 => @pony_os_sockopt_option(I32(645)) + fun sctp_i_want_mapped_v4_addr():I32 => @pony_os_sockopt_option(I32(646)) + fun sctp_last_packet_tracing():I32 => @pony_os_sockopt_option(I32(647)) + fun sctp_listen():I32 => @pony_os_sockopt_option(I32(648)) + fun sctp_local_auth_chunks():I32 => @pony_os_sockopt_option(I32(649)) + fun sctp_lock_logging_enable():I32 => @pony_os_sockopt_option(I32(650)) + fun sctp_log_at_send_2_outq():I32 => @pony_os_sockopt_option(I32(651)) + fun sctp_log_at_send_2_sctp():I32 => @pony_os_sockopt_option(I32(652)) + fun sctp_log_maxburst_enable():I32 => @pony_os_sockopt_option(I32(653)) + fun sctp_log_rwnd_enable():I32 => @pony_os_sockopt_option(I32(654)) + fun sctp_log_sack_arrivals_enable():I32 => @pony_os_sockopt_option(I32(655)) + fun sctp_log_try_advance():I32 => @pony_os_sockopt_option(I32(656)) + fun sctp_ltrace_chunk_enable():I32 => @pony_os_sockopt_option(I32(657)) + fun sctp_ltrace_error_enable():I32 => @pony_os_sockopt_option(I32(658)) + fun sctp_map_logging_enable():I32 => @pony_os_sockopt_option(I32(659)) + fun sctp_maxburst():I32 => @pony_os_sockopt_option(I32(660)) + fun sctp_maxseg():I32 => @pony_os_sockopt_option(I32(661)) + fun sctp_max_burst():I32 => @pony_os_sockopt_option(I32(662)) + fun sctp_max_cookie_life():I32 => @pony_os_sockopt_option(I32(663)) + fun sctp_max_cwnd():I32 => @pony_os_sockopt_option(I32(664)) + fun sctp_max_hb_interval():I32 => @pony_os_sockopt_option(I32(665)) + fun sctp_max_sack_delay():I32 => @pony_os_sockopt_option(I32(666)) + fun sctp_mbcnt_logging_enable():I32 => @pony_os_sockopt_option(I32(667)) + fun sctp_mbuf_logging_enable():I32 => @pony_os_sockopt_option(I32(668)) + fun sctp_mobility_base():I32 => @pony_os_sockopt_option(I32(669)) + fun sctp_mobility_fasthandoff():I32 => @pony_os_sockopt_option(I32(670)) + fun sctp_mobility_prim_deleted():I32 => @pony_os_sockopt_option(I32(671)) + fun sctp_nagle_logging_enable():I32 => @pony_os_sockopt_option(I32(672)) + fun sctp_nodelay():I32 => @pony_os_sockopt_option(I32(673)) + fun sctp_nrsack_supported():I32 => @pony_os_sockopt_option(I32(674)) + fun sctp_nr_selective_ack():I32 => @pony_os_sockopt_option(I32(675)) + fun sctp_operation_error():I32 => @pony_os_sockopt_option(I32(676)) + fun sctp_packed():I32 => @pony_os_sockopt_option(I32(677)) + fun sctp_packet_dropped():I32 => @pony_os_sockopt_option(I32(678)) + fun sctp_packet_log_size():I32 => @pony_os_sockopt_option(I32(679)) + fun sctp_packet_truncated():I32 => @pony_os_sockopt_option(I32(680)) + fun sctp_pad_chunk():I32 => @pony_os_sockopt_option(I32(681)) + fun sctp_partial_delivery_point():I32 => @pony_os_sockopt_option(I32(682)) + fun sctp_pcb_copy_flags():I32 => @pony_os_sockopt_option(I32(683)) + fun sctp_pcb_flags_accepting():I32 => @pony_os_sockopt_option(I32(684)) + fun sctp_pcb_flags_adaptationevnt():I32 => @pony_os_sockopt_option(I32(685)) + fun sctp_pcb_flags_assoc_resetevnt():I32 => @pony_os_sockopt_option(I32(686)) + fun sctp_pcb_flags_authevnt():I32 => @pony_os_sockopt_option(I32(687)) + fun sctp_pcb_flags_autoclose():I32 => @pony_os_sockopt_option(I32(688)) + fun sctp_pcb_flags_auto_asconf():I32 => @pony_os_sockopt_option(I32(689)) + fun sctp_pcb_flags_blocking_io():I32 => @pony_os_sockopt_option(I32(690)) + fun sctp_pcb_flags_boundall():I32 => @pony_os_sockopt_option(I32(691)) + fun sctp_pcb_flags_bound_v6():I32 => @pony_os_sockopt_option(I32(692)) + fun sctp_pcb_flags_close_ip():I32 => @pony_os_sockopt_option(I32(693)) + fun sctp_pcb_flags_connected():I32 => @pony_os_sockopt_option(I32(694)) + fun sctp_pcb_flags_donot_heartbeat():I32 => @pony_os_sockopt_option(I32(695)) + fun sctp_pcb_flags_dont_wake():I32 => @pony_os_sockopt_option(I32(696)) + fun sctp_pcb_flags_do_asconf():I32 => @pony_os_sockopt_option(I32(697)) + fun sctp_pcb_flags_do_not_pmtud():I32 => @pony_os_sockopt_option(I32(698)) + fun sctp_pcb_flags_dryevnt():I32 => @pony_os_sockopt_option(I32(699)) + fun sctp_pcb_flags_explicit_eor():I32 => @pony_os_sockopt_option(I32(700)) + fun sctp_pcb_flags_ext_rcvinfo():I32 => @pony_os_sockopt_option(I32(701)) + fun sctp_pcb_flags_frag_interleave():I32 => @pony_os_sockopt_option(I32(702)) + fun sctp_pcb_flags_interleave_strms():I32 => @pony_os_sockopt_option(I32(703)) + fun sctp_pcb_flags_in_tcppool():I32 => @pony_os_sockopt_option(I32(704)) + fun sctp_pcb_flags_multiple_asconfs():I32 => @pony_os_sockopt_option(I32(705)) + fun sctp_pcb_flags_needs_mapped_v4():I32 => @pony_os_sockopt_option(I32(706)) + fun sctp_pcb_flags_nodelay():I32 => @pony_os_sockopt_option(I32(707)) + fun sctp_pcb_flags_no_fragment():I32 => @pony_os_sockopt_option(I32(708)) + fun sctp_pcb_flags_pdapievnt():I32 => @pony_os_sockopt_option(I32(709)) + fun sctp_pcb_flags_portreuse():I32 => @pony_os_sockopt_option(I32(710)) + fun sctp_pcb_flags_recvassocevnt():I32 => @pony_os_sockopt_option(I32(711)) + fun sctp_pcb_flags_recvdataioevnt():I32 => @pony_os_sockopt_option(I32(712)) + fun sctp_pcb_flags_recvnsendfailevnt():I32 => @pony_os_sockopt_option(I32(713)) + fun sctp_pcb_flags_recvnxtinfo():I32 => @pony_os_sockopt_option(I32(714)) + fun sctp_pcb_flags_recvpaddrevnt():I32 => @pony_os_sockopt_option(I32(715)) + fun sctp_pcb_flags_recvpeererr():I32 => @pony_os_sockopt_option(I32(716)) + fun sctp_pcb_flags_recvrcvinfo():I32 => @pony_os_sockopt_option(I32(717)) + fun sctp_pcb_flags_recvsendfailevnt():I32 => @pony_os_sockopt_option(I32(718)) + fun sctp_pcb_flags_recvshutdownevnt():I32 => @pony_os_sockopt_option(I32(719)) + fun sctp_pcb_flags_socket_allgone():I32 => @pony_os_sockopt_option(I32(720)) + fun sctp_pcb_flags_socket_cant_read():I32 => @pony_os_sockopt_option(I32(721)) + fun sctp_pcb_flags_socket_gone():I32 => @pony_os_sockopt_option(I32(722)) + fun sctp_pcb_flags_stream_changeevnt():I32 => @pony_os_sockopt_option(I32(723)) + fun sctp_pcb_flags_stream_resetevnt():I32 => @pony_os_sockopt_option(I32(724)) + fun sctp_pcb_flags_tcptype():I32 => @pony_os_sockopt_option(I32(725)) + fun sctp_pcb_flags_udptype():I32 => @pony_os_sockopt_option(I32(726)) + fun sctp_pcb_flags_unbound():I32 => @pony_os_sockopt_option(I32(727)) + fun sctp_pcb_flags_wakeinput():I32 => @pony_os_sockopt_option(I32(728)) + fun sctp_pcb_flags_wakeoutput():I32 => @pony_os_sockopt_option(I32(729)) + fun sctp_pcb_flags_was_aborted():I32 => @pony_os_sockopt_option(I32(730)) + fun sctp_pcb_flags_was_connected():I32 => @pony_os_sockopt_option(I32(731)) + fun sctp_pcb_flags_zero_copy_active():I32 => @pony_os_sockopt_option(I32(732)) + fun sctp_pcb_status():I32 => @pony_os_sockopt_option(I32(733)) + fun sctp_peeloff():I32 => @pony_os_sockopt_option(I32(734)) + fun sctp_peer_addr_params():I32 => @pony_os_sockopt_option(I32(735)) + fun sctp_peer_addr_thlds():I32 => @pony_os_sockopt_option(I32(736)) + fun sctp_peer_auth_chunks():I32 => @pony_os_sockopt_option(I32(737)) + fun sctp_pktdrop_supported():I32 => @pony_os_sockopt_option(I32(738)) + fun sctp_pluggable_cc():I32 => @pony_os_sockopt_option(I32(739)) + fun sctp_pluggable_ss():I32 => @pony_os_sockopt_option(I32(740)) + fun sctp_primary_addr():I32 => @pony_os_sockopt_option(I32(741)) + fun sctp_pr_assoc_status():I32 => @pony_os_sockopt_option(I32(742)) + fun sctp_pr_stream_status():I32 => @pony_os_sockopt_option(I32(743)) + fun sctp_pr_supported():I32 => @pony_os_sockopt_option(I32(744)) + fun sctp_reconfig_supported():I32 => @pony_os_sockopt_option(I32(745)) + fun sctp_recvnxtinfo():I32 => @pony_os_sockopt_option(I32(746)) + fun sctp_recvrcvinfo():I32 => @pony_os_sockopt_option(I32(747)) + fun sctp_recv_rwnd_logging_enable():I32 => @pony_os_sockopt_option(I32(748)) + fun sctp_remote_udp_encaps_port():I32 => @pony_os_sockopt_option(I32(749)) + fun sctp_reset_assoc():I32 => @pony_os_sockopt_option(I32(750)) + fun sctp_reset_streams():I32 => @pony_os_sockopt_option(I32(751)) + fun sctp_reuse_port():I32 => @pony_os_sockopt_option(I32(752)) + fun sctp_rtoinfo():I32 => @pony_os_sockopt_option(I32(753)) + fun sctp_rttvar_logging_enable():I32 => @pony_os_sockopt_option(I32(754)) + fun sctp_sack_cmt_dac():I32 => @pony_os_sockopt_option(I32(755)) + fun sctp_sack_logging_enable():I32 => @pony_os_sockopt_option(I32(756)) + fun sctp_sack_nonce_sum():I32 => @pony_os_sockopt_option(I32(757)) + fun sctp_sack_rwnd_logging_enable():I32 => @pony_os_sockopt_option(I32(758)) + fun sctp_sat_network_burst_incr():I32 => @pony_os_sockopt_option(I32(759)) + fun sctp_sat_network_min():I32 => @pony_os_sockopt_option(I32(760)) + fun sctp_sb_logging_enable():I32 => @pony_os_sockopt_option(I32(761)) + fun sctp_selective_ack():I32 => @pony_os_sockopt_option(I32(762)) + fun sctp_set_debug_level():I32 => @pony_os_sockopt_option(I32(763)) + fun sctp_set_dynamic_primary():I32 => @pony_os_sockopt_option(I32(764)) + fun sctp_set_initial_dbg_seq():I32 => @pony_os_sockopt_option(I32(765)) + fun sctp_set_peer_primary_addr():I32 => @pony_os_sockopt_option(I32(766)) + fun sctp_shutdown():I32 => @pony_os_sockopt_option(I32(767)) + fun sctp_shutdown_ack():I32 => @pony_os_sockopt_option(I32(768)) + fun sctp_shutdown_ack_sent():I32 => @pony_os_sockopt_option(I32(769)) + fun sctp_shutdown_complete():I32 => @pony_os_sockopt_option(I32(770)) + fun sctp_shutdown_pending():I32 => @pony_os_sockopt_option(I32(771)) + fun sctp_shutdown_received():I32 => @pony_os_sockopt_option(I32(772)) + fun sctp_shutdown_sent():I32 => @pony_os_sockopt_option(I32(773)) + fun sctp_smallest_pmtu():I32 => @pony_os_sockopt_option(I32(774)) + fun sctp_ss_default():I32 => @pony_os_sockopt_option(I32(775)) + fun sctp_ss_fair_bandwith():I32 => @pony_os_sockopt_option(I32(776)) + fun sctp_ss_first_come():I32 => @pony_os_sockopt_option(I32(777)) + fun sctp_ss_priority():I32 => @pony_os_sockopt_option(I32(778)) + fun sctp_ss_round_robin():I32 => @pony_os_sockopt_option(I32(779)) + fun sctp_ss_round_robin_packet():I32 => @pony_os_sockopt_option(I32(780)) + fun sctp_ss_value():I32 => @pony_os_sockopt_option(I32(781)) + fun sctp_status():I32 => @pony_os_sockopt_option(I32(782)) + fun sctp_stream_reset():I32 => @pony_os_sockopt_option(I32(783)) + fun sctp_stream_reset_incoming():I32 => @pony_os_sockopt_option(I32(784)) + fun sctp_stream_reset_outgoing():I32 => @pony_os_sockopt_option(I32(785)) + fun sctp_str_logging_enable():I32 => @pony_os_sockopt_option(I32(786)) + fun sctp_threshold_logging():I32 => @pony_os_sockopt_option(I32(787)) + fun sctp_timeouts():I32 => @pony_os_sockopt_option(I32(788)) + fun sctp_use_ext_rcvinfo():I32 => @pony_os_sockopt_option(I32(789)) + fun sctp_vrf_id():I32 => @pony_os_sockopt_option(I32(790)) + fun sctp_wake_logging_enable():I32 => @pony_os_sockopt_option(I32(791)) + fun sock_cloexec():I32 => @pony_os_sockopt_option(I32(792)) + fun sock_dgram():I32 => @pony_os_sockopt_option(I32(793)) + fun sock_maxaddrlen():I32 => @pony_os_sockopt_option(I32(794)) + fun sock_nonblock():I32 => @pony_os_sockopt_option(I32(795)) + fun sock_raw():I32 => @pony_os_sockopt_option(I32(796)) + fun sock_rdm():I32 => @pony_os_sockopt_option(I32(797)) + fun sock_seqpacket():I32 => @pony_os_sockopt_option(I32(798)) + fun sock_stream():I32 => @pony_os_sockopt_option(I32(799)) + fun somaxconn():I32 => @pony_os_sockopt_option(I32(800)) + fun sonpx_setoptshut():I32 => @pony_os_sockopt_option(I32(801)) + fun so_acceptconn():I32 => @pony_os_sockopt_option(I32(802)) + fun so_acceptfilter():I32 => @pony_os_sockopt_option(I32(803)) + fun so_atmpvc():I32 => @pony_os_sockopt_option(I32(804)) + fun so_atmqos():I32 => @pony_os_sockopt_option(I32(805)) + fun so_atmsap():I32 => @pony_os_sockopt_option(I32(806)) + fun so_attach_bpf():I32 => @pony_os_sockopt_option(I32(807)) + fun so_attach_filter():I32 => @pony_os_sockopt_option(I32(808)) + fun so_bindtodevice():I32 => @pony_os_sockopt_option(I32(809)) + fun so_bintime():I32 => @pony_os_sockopt_option(I32(810)) + fun so_bpf_extensions():I32 => @pony_os_sockopt_option(I32(811)) + fun so_broadcast():I32 => @pony_os_sockopt_option(I32(812)) + fun so_bsdcompat():I32 => @pony_os_sockopt_option(I32(813)) + fun so_bsp_state():I32 => @pony_os_sockopt_option(I32(814)) + fun so_busy_poll():I32 => @pony_os_sockopt_option(I32(815)) + fun so_conaccess():I32 => @pony_os_sockopt_option(I32(816)) + fun so_condata():I32 => @pony_os_sockopt_option(I32(817)) + fun so_conditional_accept():I32 => @pony_os_sockopt_option(I32(818)) + fun so_connect_time():I32 => @pony_os_sockopt_option(I32(819)) + fun so_debug():I32 => @pony_os_sockopt_option(I32(820)) + fun so_detach_bpf():I32 => @pony_os_sockopt_option(I32(821)) + fun so_detach_filter():I32 => @pony_os_sockopt_option(I32(822)) + fun so_domain():I32 => @pony_os_sockopt_option(I32(823)) + fun so_dontlinger():I32 => @pony_os_sockopt_option(I32(824)) + fun so_dontroute():I32 => @pony_os_sockopt_option(I32(825)) + fun so_donttrunc():I32 => @pony_os_sockopt_option(I32(826)) + fun so_error():I32 => @pony_os_sockopt_option(I32(827)) + fun so_exclusiveaddruse():I32 => @pony_os_sockopt_option(I32(828)) + fun so_get_filter():I32 => @pony_os_sockopt_option(I32(829)) + fun so_group_id():I32 => @pony_os_sockopt_option(I32(830)) + fun so_group_priority():I32 => @pony_os_sockopt_option(I32(831)) + fun so_hci_raw_direction():I32 => @pony_os_sockopt_option(I32(832)) + fun so_hci_raw_filter():I32 => @pony_os_sockopt_option(I32(833)) + fun so_incoming_cpu():I32 => @pony_os_sockopt_option(I32(834)) + fun so_keepalive():I32 => @pony_os_sockopt_option(I32(835)) + fun so_l2cap_encrypted():I32 => @pony_os_sockopt_option(I32(836)) + fun so_l2cap_flush():I32 => @pony_os_sockopt_option(I32(837)) + fun so_l2cap_iflow():I32 => @pony_os_sockopt_option(I32(838)) + fun so_l2cap_imtu():I32 => @pony_os_sockopt_option(I32(839)) + fun so_l2cap_oflow():I32 => @pony_os_sockopt_option(I32(840)) + fun so_l2cap_omtu():I32 => @pony_os_sockopt_option(I32(841)) + fun so_label():I32 => @pony_os_sockopt_option(I32(842)) + fun so_linger():I32 => @pony_os_sockopt_option(I32(843)) + fun so_linger_sec():I32 => @pony_os_sockopt_option(I32(844)) + fun so_linkinfo():I32 => @pony_os_sockopt_option(I32(845)) + fun so_listenincqlen():I32 => @pony_os_sockopt_option(I32(846)) + fun so_listenqlen():I32 => @pony_os_sockopt_option(I32(847)) + fun so_listenqlimit():I32 => @pony_os_sockopt_option(I32(848)) + fun so_lock_filter():I32 => @pony_os_sockopt_option(I32(849)) + fun so_mark():I32 => @pony_os_sockopt_option(I32(850)) + fun so_max_msg_size():I32 => @pony_os_sockopt_option(I32(851)) + fun so_max_pacing_rate():I32 => @pony_os_sockopt_option(I32(852)) + fun so_multipoint():I32 => @pony_os_sockopt_option(I32(853)) + fun so_netsvc_marking_level():I32 => @pony_os_sockopt_option(I32(854)) + fun so_net_service_type():I32 => @pony_os_sockopt_option(I32(855)) + fun so_nke():I32 => @pony_os_sockopt_option(I32(856)) + fun so_noaddrerr():I32 => @pony_os_sockopt_option(I32(857)) + fun so_nofcs():I32 => @pony_os_sockopt_option(I32(858)) + fun so_nosigpipe():I32 => @pony_os_sockopt_option(I32(859)) + fun so_notifyconflict():I32 => @pony_os_sockopt_option(I32(860)) + fun so_no_check():I32 => @pony_os_sockopt_option(I32(861)) + fun so_no_ddp():I32 => @pony_os_sockopt_option(I32(862)) + fun so_no_offload():I32 => @pony_os_sockopt_option(I32(863)) + fun so_np_extensions():I32 => @pony_os_sockopt_option(I32(864)) + fun so_nread():I32 => @pony_os_sockopt_option(I32(865)) + fun so_numrcvpkt():I32 => @pony_os_sockopt_option(I32(866)) + fun so_nwrite():I32 => @pony_os_sockopt_option(I32(867)) + fun so_oobinline():I32 => @pony_os_sockopt_option(I32(868)) + fun so_original_dst():I32 => @pony_os_sockopt_option(I32(869)) + fun so_passcred():I32 => @pony_os_sockopt_option(I32(870)) + fun so_passsec():I32 => @pony_os_sockopt_option(I32(871)) + fun so_peek_off():I32 => @pony_os_sockopt_option(I32(872)) + fun so_peercred():I32 => @pony_os_sockopt_option(I32(873)) + fun so_peerlabel():I32 => @pony_os_sockopt_option(I32(874)) + fun so_peername():I32 => @pony_os_sockopt_option(I32(875)) + fun so_peersec():I32 => @pony_os_sockopt_option(I32(876)) + fun so_port_scalability():I32 => @pony_os_sockopt_option(I32(877)) + fun so_priority():I32 => @pony_os_sockopt_option(I32(878)) + fun so_protocol():I32 => @pony_os_sockopt_option(I32(879)) + fun so_protocol_info():I32 => @pony_os_sockopt_option(I32(880)) + fun so_prototype():I32 => @pony_os_sockopt_option(I32(881)) + fun so_proxyusr():I32 => @pony_os_sockopt_option(I32(882)) + fun so_randomport():I32 => @pony_os_sockopt_option(I32(883)) + fun so_rcvbuf():I32 => @pony_os_sockopt_option(I32(884)) + fun so_rcvbufforce():I32 => @pony_os_sockopt_option(I32(885)) + fun so_rcvlowat():I32 => @pony_os_sockopt_option(I32(886)) + fun so_rcvtimeo():I32 => @pony_os_sockopt_option(I32(887)) + fun so_rds_transport():I32 => @pony_os_sockopt_option(I32(888)) + fun so_reuseaddr():I32 => @pony_os_sockopt_option(I32(889)) + fun so_reuseport():I32 => @pony_os_sockopt_option(I32(890)) + fun so_reuseshareuid():I32 => @pony_os_sockopt_option(I32(891)) + fun so_rfcomm_fc_info():I32 => @pony_os_sockopt_option(I32(892)) + fun so_rfcomm_mtu():I32 => @pony_os_sockopt_option(I32(893)) + fun so_rxq_ovfl():I32 => @pony_os_sockopt_option(I32(894)) + fun so_sco_conninfo():I32 => @pony_os_sockopt_option(I32(895)) + fun so_sco_mtu():I32 => @pony_os_sockopt_option(I32(896)) + fun so_security_authentication():I32 => @pony_os_sockopt_option(I32(897)) + fun so_security_encryption_network():I32 => @pony_os_sockopt_option(I32(898)) + fun so_security_encryption_transport():I32 => @pony_os_sockopt_option(I32(899)) + fun so_select_err_queue():I32 => @pony_os_sockopt_option(I32(900)) + fun so_setclp():I32 => @pony_os_sockopt_option(I32(901)) + fun so_setfib():I32 => @pony_os_sockopt_option(I32(902)) + fun so_sndbuf():I32 => @pony_os_sockopt_option(I32(903)) + fun so_sndbufforce():I32 => @pony_os_sockopt_option(I32(904)) + fun so_sndlowat():I32 => @pony_os_sockopt_option(I32(905)) + fun so_sndtimeo():I32 => @pony_os_sockopt_option(I32(906)) + fun so_timestamp():I32 => @pony_os_sockopt_option(I32(907)) + fun so_timestamping():I32 => @pony_os_sockopt_option(I32(908)) + fun so_timestampns():I32 => @pony_os_sockopt_option(I32(909)) + fun so_timestamp_monotonic():I32 => @pony_os_sockopt_option(I32(910)) + fun so_type():I32 => @pony_os_sockopt_option(I32(911)) + fun so_upcallclosewait():I32 => @pony_os_sockopt_option(I32(912)) + fun so_update_accept_context():I32 => @pony_os_sockopt_option(I32(913)) + fun so_useloopback():I32 => @pony_os_sockopt_option(I32(914)) + fun so_user_cookie():I32 => @pony_os_sockopt_option(I32(915)) + fun so_vendor():I32 => @pony_os_sockopt_option(I32(916)) + fun so_vm_sockets_buffer_max_size():I32 => @pony_os_sockopt_option(I32(917)) + fun so_vm_sockets_buffer_min_size():I32 => @pony_os_sockopt_option(I32(918)) + fun so_vm_sockets_buffer_size():I32 => @pony_os_sockopt_option(I32(919)) + fun so_vm_sockets_connect_timeout():I32 => @pony_os_sockopt_option(I32(920)) + fun so_vm_sockets_nonblock_txrx():I32 => @pony_os_sockopt_option(I32(921)) + fun so_vm_sockets_peer_host_vm_id():I32 => @pony_os_sockopt_option(I32(922)) + fun so_vm_sockets_trusted():I32 => @pony_os_sockopt_option(I32(923)) + fun so_wantmore():I32 => @pony_os_sockopt_option(I32(924)) + fun so_wantoobflag():I32 => @pony_os_sockopt_option(I32(925)) + fun so_wifi_status():I32 => @pony_os_sockopt_option(I32(926)) + fun tcp6_mss():I32 => @pony_os_sockopt_option(I32(927)) + fun tcpci_flag_lossrecovery():I32 => @pony_os_sockopt_option(I32(928)) + fun tcpci_flag_reordering_detected():I32 => @pony_os_sockopt_option(I32(929)) + fun tcpci_opt_ecn():I32 => @pony_os_sockopt_option(I32(930)) + fun tcpci_opt_sack():I32 => @pony_os_sockopt_option(I32(931)) + fun tcpci_opt_timestamps():I32 => @pony_os_sockopt_option(I32(932)) + fun tcpci_opt_wscale():I32 => @pony_os_sockopt_option(I32(933)) + fun tcpf_ca_cwr():I32 => @pony_os_sockopt_option(I32(934)) + fun tcpf_ca_disorder():I32 => @pony_os_sockopt_option(I32(935)) + fun tcpf_ca_loss():I32 => @pony_os_sockopt_option(I32(936)) + fun tcpf_ca_open():I32 => @pony_os_sockopt_option(I32(937)) + fun tcpf_ca_recovery():I32 => @pony_os_sockopt_option(I32(938)) + fun tcpi_opt_ecn():I32 => @pony_os_sockopt_option(I32(939)) + fun tcpi_opt_ecn_seen():I32 => @pony_os_sockopt_option(I32(940)) + fun tcpi_opt_sack():I32 => @pony_os_sockopt_option(I32(941)) + fun tcpi_opt_syn_data():I32 => @pony_os_sockopt_option(I32(942)) + fun tcpi_opt_timestamps():I32 => @pony_os_sockopt_option(I32(943)) + fun tcpi_opt_toe():I32 => @pony_os_sockopt_option(I32(944)) + fun tcpi_opt_wscale():I32 => @pony_os_sockopt_option(I32(945)) + fun tcpolen_cc():I32 => @pony_os_sockopt_option(I32(946)) + fun tcpolen_cc_appa():I32 => @pony_os_sockopt_option(I32(947)) + fun tcpolen_eol():I32 => @pony_os_sockopt_option(I32(948)) + fun tcpolen_fastopen_req():I32 => @pony_os_sockopt_option(I32(949)) + fun tcpolen_fast_open_empty():I32 => @pony_os_sockopt_option(I32(950)) + fun tcpolen_fast_open_max():I32 => @pony_os_sockopt_option(I32(951)) + fun tcpolen_fast_open_min():I32 => @pony_os_sockopt_option(I32(952)) + fun tcpolen_maxseg():I32 => @pony_os_sockopt_option(I32(953)) + fun tcpolen_nop():I32 => @pony_os_sockopt_option(I32(954)) + fun tcpolen_pad():I32 => @pony_os_sockopt_option(I32(955)) + fun tcpolen_sack():I32 => @pony_os_sockopt_option(I32(956)) + fun tcpolen_sackhdr():I32 => @pony_os_sockopt_option(I32(957)) + fun tcpolen_sack_permitted():I32 => @pony_os_sockopt_option(I32(958)) + fun tcpolen_signature():I32 => @pony_os_sockopt_option(I32(959)) + fun tcpolen_timestamp():I32 => @pony_os_sockopt_option(I32(960)) + fun tcpolen_tstamp_appa():I32 => @pony_os_sockopt_option(I32(961)) + fun tcpolen_window():I32 => @pony_os_sockopt_option(I32(962)) + fun tcpopt_cc():I32 => @pony_os_sockopt_option(I32(963)) + fun tcpopt_ccecho():I32 => @pony_os_sockopt_option(I32(964)) + fun tcpopt_ccnew():I32 => @pony_os_sockopt_option(I32(965)) + fun tcpopt_eol():I32 => @pony_os_sockopt_option(I32(966)) + fun tcpopt_fastopen():I32 => @pony_os_sockopt_option(I32(967)) + fun tcpopt_fast_open():I32 => @pony_os_sockopt_option(I32(968)) + fun tcpopt_maxseg():I32 => @pony_os_sockopt_option(I32(969)) + fun tcpopt_multipath():I32 => @pony_os_sockopt_option(I32(970)) + fun tcpopt_nop():I32 => @pony_os_sockopt_option(I32(971)) + fun tcpopt_pad():I32 => @pony_os_sockopt_option(I32(972)) + fun tcpopt_sack():I32 => @pony_os_sockopt_option(I32(973)) + fun tcpopt_sack_hdr():I32 => @pony_os_sockopt_option(I32(974)) + fun tcpopt_sack_permitted():I32 => @pony_os_sockopt_option(I32(975)) + fun tcpopt_sack_permit_hdr():I32 => @pony_os_sockopt_option(I32(976)) + fun tcpopt_signature():I32 => @pony_os_sockopt_option(I32(977)) + fun tcpopt_timestamp():I32 => @pony_os_sockopt_option(I32(978)) + fun tcpopt_tstamp_hdr():I32 => @pony_os_sockopt_option(I32(979)) + fun tcpopt_window():I32 => @pony_os_sockopt_option(I32(980)) + fun tcp_ca_name_max():I32 => @pony_os_sockopt_option(I32(981)) + fun tcp_ccalgoopt():I32 => @pony_os_sockopt_option(I32(982)) + fun tcp_cc_info():I32 => @pony_os_sockopt_option(I32(983)) + fun tcp_congestion():I32 => @pony_os_sockopt_option(I32(984)) + fun tcp_connectiontimeout():I32 => @pony_os_sockopt_option(I32(985)) + fun tcp_connection_info():I32 => @pony_os_sockopt_option(I32(986)) + fun tcp_cookie_in_always():I32 => @pony_os_sockopt_option(I32(987)) + fun tcp_cookie_max():I32 => @pony_os_sockopt_option(I32(988)) + fun tcp_cookie_min():I32 => @pony_os_sockopt_option(I32(989)) + fun tcp_cookie_out_never():I32 => @pony_os_sockopt_option(I32(990)) + fun tcp_cookie_pair_size():I32 => @pony_os_sockopt_option(I32(991)) + fun tcp_cookie_transactions():I32 => @pony_os_sockopt_option(I32(992)) + fun tcp_cork():I32 => @pony_os_sockopt_option(I32(993)) + fun tcp_defer_accept():I32 => @pony_os_sockopt_option(I32(994)) + fun tcp_enable_ecn():I32 => @pony_os_sockopt_option(I32(995)) + fun tcp_fastopen():I32 => @pony_os_sockopt_option(I32(996)) + fun tcp_function_blk():I32 => @pony_os_sockopt_option(I32(997)) + fun tcp_function_name_len_max():I32 => @pony_os_sockopt_option(I32(998)) + fun tcp_info():I32 => @pony_os_sockopt_option(I32(999)) + fun tcp_keepalive():I32 => @pony_os_sockopt_option(I32(1000)) + fun tcp_keepcnt():I32 => @pony_os_sockopt_option(I32(1001)) + fun tcp_keepidle():I32 => @pony_os_sockopt_option(I32(1002)) + fun tcp_keepinit():I32 => @pony_os_sockopt_option(I32(1003)) + fun tcp_keepintvl():I32 => @pony_os_sockopt_option(I32(1004)) + fun tcp_linger2():I32 => @pony_os_sockopt_option(I32(1005)) + fun tcp_maxburst():I32 => @pony_os_sockopt_option(I32(1006)) + fun tcp_maxhlen():I32 => @pony_os_sockopt_option(I32(1007)) + fun tcp_maxolen():I32 => @pony_os_sockopt_option(I32(1008)) + fun tcp_maxseg():I32 => @pony_os_sockopt_option(I32(1009)) + fun tcp_maxwin():I32 => @pony_os_sockopt_option(I32(1010)) + fun tcp_max_sack():I32 => @pony_os_sockopt_option(I32(1011)) + fun tcp_max_winshift():I32 => @pony_os_sockopt_option(I32(1012)) + fun tcp_md5sig():I32 => @pony_os_sockopt_option(I32(1013)) + fun tcp_md5sig_maxkeylen():I32 => @pony_os_sockopt_option(I32(1014)) + fun tcp_minmss():I32 => @pony_os_sockopt_option(I32(1015)) + fun tcp_mss():I32 => @pony_os_sockopt_option(I32(1016)) + fun tcp_mss_default():I32 => @pony_os_sockopt_option(I32(1017)) + fun tcp_mss_desired():I32 => @pony_os_sockopt_option(I32(1018)) + fun tcp_nodelay():I32 => @pony_os_sockopt_option(I32(1019)) + fun tcp_noopt():I32 => @pony_os_sockopt_option(I32(1020)) + fun tcp_nopush():I32 => @pony_os_sockopt_option(I32(1021)) + fun tcp_notsent_lowat():I32 => @pony_os_sockopt_option(I32(1022)) + fun tcp_pcap_in():I32 => @pony_os_sockopt_option(I32(1023)) + fun tcp_pcap_out():I32 => @pony_os_sockopt_option(I32(1024)) + fun tcp_queue_seq():I32 => @pony_os_sockopt_option(I32(1025)) + fun tcp_quickack():I32 => @pony_os_sockopt_option(I32(1026)) + fun tcp_repair():I32 => @pony_os_sockopt_option(I32(1027)) + fun tcp_repair_options():I32 => @pony_os_sockopt_option(I32(1028)) + fun tcp_repair_queue():I32 => @pony_os_sockopt_option(I32(1029)) + fun tcp_rxt_conndroptime():I32 => @pony_os_sockopt_option(I32(1030)) + fun tcp_rxt_findrop():I32 => @pony_os_sockopt_option(I32(1031)) + fun tcp_saved_syn():I32 => @pony_os_sockopt_option(I32(1032)) + fun tcp_save_syn():I32 => @pony_os_sockopt_option(I32(1033)) + fun tcp_sendmoreacks():I32 => @pony_os_sockopt_option(I32(1034)) + fun tcp_syncnt():I32 => @pony_os_sockopt_option(I32(1035)) + fun tcp_s_data_in():I32 => @pony_os_sockopt_option(I32(1036)) + fun tcp_s_data_out():I32 => @pony_os_sockopt_option(I32(1037)) + fun tcp_thin_dupack():I32 => @pony_os_sockopt_option(I32(1038)) + fun tcp_thin_linear_timeouts():I32 => @pony_os_sockopt_option(I32(1039)) + fun tcp_timestamp():I32 => @pony_os_sockopt_option(I32(1040)) + fun tcp_user_timeout():I32 => @pony_os_sockopt_option(I32(1041)) + fun tcp_vendor():I32 => @pony_os_sockopt_option(I32(1042)) + fun tcp_window_clamp():I32 => @pony_os_sockopt_option(I32(1043)) + fun tipc_addr_id():I32 => @pony_os_sockopt_option(I32(1044)) + fun tipc_addr_mcast():I32 => @pony_os_sockopt_option(I32(1045)) + fun tipc_addr_name():I32 => @pony_os_sockopt_option(I32(1046)) + fun tipc_addr_nameseq():I32 => @pony_os_sockopt_option(I32(1047)) + fun tipc_cfg_srv():I32 => @pony_os_sockopt_option(I32(1048)) + fun tipc_cluster_scope():I32 => @pony_os_sockopt_option(I32(1049)) + fun tipc_conn_shutdown():I32 => @pony_os_sockopt_option(I32(1050)) + fun tipc_conn_timeout():I32 => @pony_os_sockopt_option(I32(1051)) + fun tipc_critical_importance():I32 => @pony_os_sockopt_option(I32(1052)) + fun tipc_destname():I32 => @pony_os_sockopt_option(I32(1053)) + fun tipc_dest_droppable():I32 => @pony_os_sockopt_option(I32(1054)) + fun tipc_errinfo():I32 => @pony_os_sockopt_option(I32(1055)) + fun tipc_err_no_name():I32 => @pony_os_sockopt_option(I32(1056)) + fun tipc_err_no_node():I32 => @pony_os_sockopt_option(I32(1057)) + fun tipc_err_no_port():I32 => @pony_os_sockopt_option(I32(1058)) + fun tipc_err_overload():I32 => @pony_os_sockopt_option(I32(1059)) + fun tipc_high_importance():I32 => @pony_os_sockopt_option(I32(1060)) + fun tipc_importance():I32 => @pony_os_sockopt_option(I32(1061)) + fun tipc_link_state():I32 => @pony_os_sockopt_option(I32(1062)) + fun tipc_low_importance():I32 => @pony_os_sockopt_option(I32(1063)) + fun tipc_max_bearer_name():I32 => @pony_os_sockopt_option(I32(1064)) + fun tipc_max_if_name():I32 => @pony_os_sockopt_option(I32(1065)) + fun tipc_max_link_name():I32 => @pony_os_sockopt_option(I32(1066)) + fun tipc_max_media_name():I32 => @pony_os_sockopt_option(I32(1067)) + fun tipc_max_user_msg_size():I32 => @pony_os_sockopt_option(I32(1068)) + fun tipc_medium_importance():I32 => @pony_os_sockopt_option(I32(1069)) + fun tipc_node_recvq_depth():I32 => @pony_os_sockopt_option(I32(1070)) + fun tipc_node_scope():I32 => @pony_os_sockopt_option(I32(1071)) + fun tipc_ok():I32 => @pony_os_sockopt_option(I32(1072)) + fun tipc_published():I32 => @pony_os_sockopt_option(I32(1073)) + fun tipc_reserved_types():I32 => @pony_os_sockopt_option(I32(1074)) + fun tipc_retdata():I32 => @pony_os_sockopt_option(I32(1075)) + fun tipc_sock_recvq_depth():I32 => @pony_os_sockopt_option(I32(1076)) + fun tipc_src_droppable():I32 => @pony_os_sockopt_option(I32(1077)) + fun tipc_subscr_timeout():I32 => @pony_os_sockopt_option(I32(1078)) + fun tipc_sub_cancel():I32 => @pony_os_sockopt_option(I32(1079)) + fun tipc_sub_ports():I32 => @pony_os_sockopt_option(I32(1080)) + fun tipc_sub_service():I32 => @pony_os_sockopt_option(I32(1081)) + fun tipc_top_srv():I32 => @pony_os_sockopt_option(I32(1082)) + fun tipc_wait_forever():I32 => @pony_os_sockopt_option(I32(1083)) + fun tipc_withdrawn():I32 => @pony_os_sockopt_option(I32(1084)) + fun tipc_zone_scope():I32 => @pony_os_sockopt_option(I32(1085)) + fun ttcp_client_snd_wnd():I32 => @pony_os_sockopt_option(I32(1086)) + fun udp_cork():I32 => @pony_os_sockopt_option(I32(1087)) + fun udp_encap():I32 => @pony_os_sockopt_option(I32(1088)) + fun udp_encap_espinudp():I32 => @pony_os_sockopt_option(I32(1089)) + fun udp_encap_espinudp_maxfraglen():I32 => @pony_os_sockopt_option(I32(1090)) + fun udp_encap_espinudp_non_ike():I32 => @pony_os_sockopt_option(I32(1091)) + fun udp_encap_espinudp_port():I32 => @pony_os_sockopt_option(I32(1092)) + fun udp_encap_l2tpinudp():I32 => @pony_os_sockopt_option(I32(1093)) + fun udp_nocksum():I32 => @pony_os_sockopt_option(I32(1094)) + fun udp_no_check6_rx():I32 => @pony_os_sockopt_option(I32(1095)) + fun udp_no_check6_tx():I32 => @pony_os_sockopt_option(I32(1096)) + fun udp_vendor():I32 => @pony_os_sockopt_option(I32(1097)) + fun so_rcvtimeo_old():I32 => @pony_os_sockopt_option(I32(1098)) + fun so_rcvtimeo_new():I32 => @pony_os_sockopt_option(I32(1099)) + fun so_sndtimeo_old():I32 => @pony_os_sockopt_option(I32(1100)) + fun so_sndtimeo_new():I32 => @pony_os_sockopt_option(I32(1101)) diff --git a/corral/_vendor/lori/pony_asio.pony b/corral/_vendor/lori/pony_asio.pony new file mode 100644 index 0000000..d7203df --- /dev/null +++ b/corral/_vendor/lori/pony_asio.pony @@ -0,0 +1,57 @@ +use @pony_asio_event_create[AsioEventID](owner: AsioEventNotify, fd: U32, + flags: U32, nsec: U64, noisy: Bool) +use @pony_asio_event_destroy[None](event: AsioEventID) +use @pony_asio_event_fd[U32](event: AsioEventID) +use @pony_asio_event_get_disposable[Bool](event: AsioEventID) +use @pony_asio_event_resubscribe_read[None](event: AsioEventID) +use @pony_asio_event_resubscribe_write[None](event: AsioEventID) +use @pony_asio_event_set_readable[None](event: AsioEventID, readable: Bool) +use @pony_asio_event_set_writeable[None](event: AsioEventID, writeable: Bool) +use @pony_asio_event_setnsec[U32](event: AsioEventID, nsec: U64) +use @pony_asio_event_unsubscribe[None](event: AsioEventID) + +primitive PonyAsio + fun create_event(the_actor: AsioEventNotify, fd: U32): AsioEventID => + let asio_flags = ifdef windows then + AsioEvent.read_write() + else + AsioEvent.read_write_oneshot() + end + + @pony_asio_event_create(the_actor, fd, asio_flags, 0, true) + + fun destroy(event: AsioEventID) => + @pony_asio_event_destroy(event) + + fun event_fd(event: AsioEventID): U32 => + @pony_asio_event_fd(event) + + fun get_disposable(event: AsioEventID): Bool => + @pony_asio_event_get_disposable(event) + + fun resubscribe_read(event: AsioEventID) => + @pony_asio_event_resubscribe_read(event) + + fun resubscribe_write(event: AsioEventID) => + @pony_asio_event_resubscribe_write(event) + + fun set_readable(event: AsioEventID) => + @pony_asio_event_set_readable(event, true) + + fun set_unreadable(event: AsioEventID) => + @pony_asio_event_set_readable(event, false) + + fun set_writeable(event: AsioEventID) => + @pony_asio_event_set_writeable(event, true) + + fun set_unwriteable(event: AsioEventID) => + @pony_asio_event_set_writeable(event, false) + + fun create_timer_event(the_actor: AsioEventNotify, nsec: U64): AsioEventID => + @pony_asio_event_create(the_actor, 0, AsioEvent.timer(), nsec, true) + + fun set_timer(event: AsioEventID, nsec: U64) => + @pony_asio_event_setnsec(event, nsec) + + fun unsubscribe(event: AsioEventID) => + @pony_asio_event_unsubscribe(event) diff --git a/corral/_vendor/lori/pony_tcp.pony b/corral/_vendor/lori/pony_tcp.pony new file mode 100644 index 0000000..b1533dd --- /dev/null +++ b/corral/_vendor/lori/pony_tcp.pony @@ -0,0 +1,175 @@ +use @pony_os_accept[I32](event: AsioEventID) +use @pony_os_connect_tcp[U32](the_actor: AsioEventNotify, + host: Pointer[U8] tag, + port: Pointer[U8] tag, + from: Pointer[U8] tag, + asio_flags: U32) +use @pony_os_connect_tcp4[U32](the_actor: AsioEventNotify, + host: Pointer[U8] tag, + port: Pointer[U8] tag, + from: Pointer[U8] tag, + asio_flags: U32) +use @pony_os_connect_tcp6[U32](the_actor: AsioEventNotify, + host: Pointer[U8] tag, + port: Pointer[U8] tag, + from: Pointer[U8] tag, + asio_flags: U32) +use @pony_os_keepalive[None](fd: U32, secs: U32) +use @pony_os_listen_tcp[AsioEventID](the_actor: AsioEventNotify, + host: Pointer[U8] tag, + port: Pointer[U8] tag) +use @pony_os_listen_tcp4[AsioEventID](the_actor: AsioEventNotify, + host: Pointer[U8] tag, + port: Pointer[U8] tag) +use @pony_os_listen_tcp6[AsioEventID](the_actor: AsioEventNotify, + host: Pointer[U8] tag, + port: Pointer[U8] tag) +use @pony_os_peername[Bool](fd: U32, ip: net.NetAddress ref) +use @pony_os_recv[USize](event: AsioEventID, + buffer: Pointer[U8] tag, + offset: USize) ? +use @pony_os_send[USize](event: AsioEventID, + buffer: Pointer[U8] tag, + from_offset: USize) ? +use @pony_os_socket_close[None](fd: U32) +use @pony_os_socket_shutdown[None](fd: U32) +use @pony_os_sockname[Bool](fd: U32, ip: net.NetAddress ref) +use @pony_os_writev[USize](ev: AsioEventID, + wsa: Pointer[(USize, Pointer[U8] tag)] tag, + wsacnt: I32) ? if windows +use @pony_os_writev[USize](ev: AsioEventID, + iov: Pointer[(Pointer[U8] tag, USize)] tag, + iovcnt: I32) ? if not windows +use @pony_os_writev_max[I32]() + +use net = "net" + +primitive PonyTCP + fun listen(the_actor: AsioEventNotify, + host: String, + port: String, + ip_version: IPVersion = DualStack) + : AsioEventID + => + match ip_version + | IP4 => + @pony_os_listen_tcp4(the_actor, host.cstring(), port.cstring()) + | IP6 => + @pony_os_listen_tcp6(the_actor, host.cstring(), port.cstring()) + | DualStack => + @pony_os_listen_tcp(the_actor, host.cstring(), port.cstring()) + end + + fun accept(event: AsioEventID): I32 => + @pony_os_accept(event) + + fun close(fd: U32) => + @pony_os_socket_close(fd) + + fun connect(the_actor: AsioEventNotify, + host: String, + port: String, + from: String, + asio_flags: U32, + ip_version: IPVersion = DualStack) + : U32 + => + match ip_version + | IP4 => + @pony_os_connect_tcp4(the_actor, + host.cstring(), + port.cstring(), + from.cstring(), + asio_flags) + | IP6 => + @pony_os_connect_tcp6(the_actor, + host.cstring(), + port.cstring(), + from.cstring(), + asio_flags) + | DualStack => + @pony_os_connect_tcp(the_actor, + host.cstring(), + port.cstring(), + from.cstring(), + asio_flags) + end + + fun keepalive(fd: U32, secs: U32) => + @pony_os_keepalive(fd, secs) + + fun peername(fd: U32, ip: net.NetAddress ref): Bool => + @pony_os_peername(fd, ip) + + fun receive(event: AsioEventID, + buffer: Pointer[U8] tag, + offset: USize) + : USize ? + => + @pony_os_recv(event, buffer, offset)? + + fun send(event: AsioEventID, + buffer: ByteSeq, + from_offset: USize = 0) + : USize ? + => + @pony_os_send(event, + buffer.cpointer(from_offset), + buffer.size() - from_offset)? + + fun shutdown(fd: U32) => + @pony_os_socket_shutdown(fd) + + fun sockname(fd: U32, ip: net.NetAddress ref): Bool => + @pony_os_sockname(fd, ip) + + fun writev(event: AsioEventID, data: Array[ByteSeq] box, + from: USize, count: USize, + first_buffer_byte_offset: USize = 0): USize ? + => + """ + Send `count` buffers from `data` starting at index `from` via writev. + Builds the platform-specific IOV array (iovec on POSIX, WSABUF on + Windows) internally. + + `first_buffer_byte_offset` skips bytes in `data(from)` for partial + write resume. + + Returns bytes sent (POSIX) or buffer count submitted (Windows). + """ + ifdef windows then + let wsa = Array[(USize, Pointer[U8] tag)](count) + var i = from + while i < (from + count) do + let entry = data(i)? + if (i == from) and (first_buffer_byte_offset > 0) then + wsa.push((entry.size() - first_buffer_byte_offset, + entry.cpointer(first_buffer_byte_offset))) + else + wsa.push((entry.size(), entry.cpointer())) + end + i = i + 1 + end + @pony_os_writev(event, wsa.cpointer(), count.i32())? + else + let iov = Array[(Pointer[U8] tag, USize)](count) + var i = from + while i < (from + count) do + let entry = data(i)? + if (i == from) and (first_buffer_byte_offset > 0) then + iov.push((entry.cpointer(first_buffer_byte_offset), + entry.size() - first_buffer_byte_offset)) + else + iov.push((entry.cpointer(), entry.size())) + end + i = i + 1 + end + @pony_os_writev(event, iov.cpointer(), count.i32())? + end + + fun writev_max(): I32 => + """ + Maximum number of IOV entries per writev call. IOV_MAX on POSIX, 1 on + Windows (Windows submits all entries at once, not in batches). + """ + @pony_os_writev_max() diff --git a/corral/_vendor/lori/read_buffer.pony b/corral/_vendor/lori/read_buffer.pony new file mode 100644 index 0000000..462de2c --- /dev/null +++ b/corral/_vendor/lori/read_buffer.pony @@ -0,0 +1,30 @@ +primitive ReadBufferResized + """A successful read buffer operation.""" + +primitive ReadBufferResizeBelowBufferSize + """ + The requested read buffer size or minimum is smaller than the current + buffer-until value. The buffer-until value sets a hard floor — the buffer must + be able to hold at least that many bytes to satisfy the framing contract. + """ + +primitive ReadBufferResizeBelowUsed + """ + The requested read buffer size is smaller than the amount of unprocessed + data currently in the buffer. Honoring the request would truncate data. + """ + +type ReadBufferResizeResult is + (ReadBufferResized | ReadBufferResizeBelowBufferSize | ReadBufferResizeBelowUsed) + +primitive BufferUntilSet + """A successful buffer_until operation.""" + +primitive BufferSizeAboveMinimum + """ + The requested `BufferSize` value exceeds the current read buffer minimum. Raise + the buffer minimum first, then set buffer_until. + """ + +type BufferUntilResult is + (BufferUntilSet | BufferSizeAboveMinimum) diff --git a/corral/_vendor/lori/read_buffer_size.pony b/corral/_vendor/lori/read_buffer_size.pony new file mode 100644 index 0000000..ba0ab77 --- /dev/null +++ b/corral/_vendor/lori/read_buffer_size.pony @@ -0,0 +1,47 @@ +use "constrained_types" + +primitive ReadBufferSizeValidator is Validator[USize] + """ + Validates that a read buffer size is at least 1. + + A buffer of 0 bytes cannot hold any data and would stall the read loop. + Used by `MakeReadBufferSize` to construct `ReadBufferSize` values. + """ + fun apply(value: USize): ValidationResult => + if value == 0 then + recover val + ValidationFailure("read buffer size must be greater than zero") + end + else + ValidationSuccess + end + +type ReadBufferSize is Constrained[USize, ReadBufferSizeValidator] + """ + A validated read buffer size in bytes. The value must be at least 1. + + Construct with `MakeReadBufferSize(bytes)`, which returns + `(ReadBufferSize | ValidationFailure)`. Pass to `TCPConnection` + constructors via the `read_buffer_size` parameter, or to + `set_read_buffer_minimum()` and `resize_read_buffer()`. + """ + +type MakeReadBufferSize is MakeConstrained[USize, ReadBufferSizeValidator] + """ + Factory for `ReadBufferSize` values. Returns + `(ReadBufferSize | ValidationFailure)`. + """ + +primitive DefaultReadBufferSize + """ + Returns a `ReadBufferSize` with the default buffer size of 16384 bytes + (16KB). + """ + fun apply(): ReadBufferSize => + match MakeReadBufferSize(16384) + | let r: ReadBufferSize => r + | let _: ValidationFailure => + // Known unreachable: 16384 is always valid. + _Unreachable() + apply() + end diff --git a/corral/_vendor/lori/send_token.pony b/corral/_vendor/lori/send_token.pony new file mode 100644 index 0000000..7490c8f --- /dev/null +++ b/corral/_vendor/lori/send_token.pony @@ -0,0 +1,34 @@ +class val SendToken is Equatable[SendToken] + """ + Identifies a send operation. Returned by `send()` on success and delivered + to `_on_sent()` when the data has been fully handed to the OS. + + Tokens use structural equality based on their ID, which is scoped per + connection. Applications managing multiple connections should pair tokens + with connection identity to avoid ambiguity. + """ + let id: USize + + new val _create(id': USize) => + id = id' + + fun eq(that: box->SendToken): Bool => + id == that.id + + fun ne(that: box->SendToken): Bool => + not eq(that) + +primitive SendErrorNotConnected + """ + The connection is not yet established or has already been closed. + """ + +primitive SendErrorNotWriteable + """ + The socket is not writeable. This happens during backpressure (a previous + send is still pending) or when the socket's send buffer is full. + Wait for `_on_unthrottled` before retrying. + """ + +type SendError is + (SendErrorNotConnected | SendErrorNotWriteable) diff --git a/corral/_vendor/lori/start_failure_reason.pony b/corral/_vendor/lori/start_failure_reason.pony new file mode 100644 index 0000000..d2abc08 --- /dev/null +++ b/corral/_vendor/lori/start_failure_reason.pony @@ -0,0 +1,8 @@ +primitive StartFailedSSL + """ + The SSL handshake failed before the server connection could start. This + covers both SSL session creation failures (e.g. bad `SSLContext`) and + handshake protocol errors before `_on_started` would have fired. + """ + +type StartFailureReason is StartFailedSSL diff --git a/corral/_vendor/lori/start_tls_error.pony b/corral/_vendor/lori/start_tls_error.pony new file mode 100644 index 0000000..841df29 --- /dev/null +++ b/corral/_vendor/lori/start_tls_error.pony @@ -0,0 +1,32 @@ +primitive StartTLSNotConnected + """ + The connection is not open. `start_tls()` requires an established plaintext + connection. + """ + +primitive StartTLSAlreadyTLS + """ + The connection already has an SSL session. TLS upgrade can only be performed + on a plaintext connection. + """ + +primitive StartTLSNotReady + """ + The connection is not in a state suitable for TLS upgrade. This can happen + when the connection is muted, has unprocessed data in the read buffer, or + has pending writes. Buffered read data is rejected to prevent a + man-in-the-middle from injecting pre-TLS data that the application would + process as post-TLS (CVE-2021-23222). + """ + +primitive StartTLSSessionFailed + """ + The SSL session could not be created from the provided `SSLContext`. The + connection is unchanged and remains a plaintext connection. + """ + +type StartTLSError is + ( StartTLSNotConnected + | StartTLSAlreadyTLS + | StartTLSNotReady + | StartTLSSessionFailed ) diff --git a/corral/_vendor/lori/tcp_connection.pony b/corral/_vendor/lori/tcp_connection.pony new file mode 100644 index 0000000..628fb2d --- /dev/null +++ b/corral/_vendor/lori/tcp_connection.pony @@ -0,0 +1,1943 @@ +use net = "net" +use "ssl/net" + +use @printf[I32](fmt: Pointer[U8] tag, ...) + +class TCPConnection + var _state: _ConnectionState ref = _ConnectionNone + var _shutdown: Bool = false + var _shutdown_peer: Bool = false + var _throttled: Bool = false + var _readable: Bool = false + var _writeable: Bool = false + var _muted: Bool = false + var _yield_read: Bool = false + // Happy Eyeballs + var _inflight_connections: U32 = 0 + + var _fd: U32 = -1 + var _event: AsioEventID = AsioEvent.none() + var _spawned_by: (TCPListenerActor | None) = None + let _lifecycle_event_receiver: (ClientLifecycleEventReceiver ref | ServerLifecycleEventReceiver ref | None) + let _enclosing: (TCPConnectionActor ref | None) + // COUPLING: _pending_first_buffer_offset points into the buffer owned by + // _pending_data(0). Trimming _pending_data without resetting the offset + // causes a dangling pointer. _manage_pending_buffer maintains both. + embed _pending_data: Array[ByteSeq] = _pending_data.create() + var _pending_writev_total: USize = 0 + var _pending_first_buffer_offset: USize = 0 + var _pending_sent: USize = 0 + var _read_buffer: Array[U8] iso = recover Array[U8] end + var _bytes_in_read_buffer: USize = 0 + var _read_buffer_size: USize = 16384 + var _read_buffer_min: USize = 16384 + var _buffer_until: (BufferSize | Streaming) = Streaming + + // Send token tracking + var _next_token_id: USize = 0 + var _pending_token: (SendToken | None) = None + + // Built-in SSL support + var _ssl: (SSL ref | None) = None + var _ssl_ready: Bool = false + var _ssl_failed: Bool = false + // Set when PonyTCP.connect returned > 0, meaning at least one TCP + // connection attempt was made. Used by the failure callback to distinguish + // DNS failure (no attempts) from TCP failure (all attempts failed). + var _had_inflight: Bool = false + // Set when _ssl_poll() sees SSLAuthFail before calling hard_close(). + // _hard_close_tls_upgrading() reads this to pass TLSAuthFailed vs + // TLSGeneralError. + var _ssl_auth_failed: Bool = false + + // Per-connection idle timeout via ASIO timer + var _timer_event: AsioEventID = AsioEvent.none() + var _idle_timeout_nsec: U64 = 0 + + // Per-connection connect timeout via ASIO timer (one-shot) + var _connect_timer_event: AsioEventID = AsioEvent.none() + var _connect_timeout_nsec: U64 = 0 + // COUPLING: Set by _fire_connect_timeout() before calling hard_close(). + // Read by _hard_close_connecting() and _hard_close_ssl_handshaking() to + // route the failure reason to ConnectionFailedTimeout. Same pattern as + // _ssl_auth_failed. + var _connect_timed_out: Bool = false + + // Per-connection user timer via ASIO timer (one-shot, no I/O reset) + var _user_timer_event: AsioEventID = AsioEvent.none() + var _next_timer_id: USize = 0 + var _user_timer_token: (TimerToken | None) = None + + // client startup state + var _host: String = "" + var _port: String = "" + var _from: String = "" + var _ip_version: IPVersion = DualStack + + new client(auth: TCPConnectAuth, + host: String, + port: String, + from: String, + enclosing: TCPConnectionActor ref, + ler: ClientLifecycleEventReceiver ref, + read_buffer_size: ReadBufferSize = DefaultReadBufferSize(), + ip_version: IPVersion = DualStack, + connection_timeout: (ConnectionTimeout | None) = None) + => + """ + Create a client-side plaintext connection. An optional `connection_timeout` + bounds the TCP Happy Eyeballs phase. If the timeout fires before + `_on_connected`, the connection fails with `ConnectionFailedTimeout`. + """ + _lifecycle_event_receiver = ler + _enclosing = enclosing + _host = host + _port = port + _from = from + _read_buffer_size = read_buffer_size() + _read_buffer_min = read_buffer_size() + _ip_version = ip_version + match connection_timeout + | let ct: ConnectionTimeout => _connect_timeout_nsec = ct() * 1_000_000 + end + + _resize_read_buffer_if_needed() + + enclosing._finish_initialization() + + new server(auth: TCPServerAuth, + fd': U32, + enclosing: TCPConnectionActor ref, + ler: ServerLifecycleEventReceiver ref, + read_buffer_size: ReadBufferSize = DefaultReadBufferSize()) + => + _fd = fd' + _lifecycle_event_receiver = ler + _enclosing = enclosing + _read_buffer_size = read_buffer_size() + _read_buffer_min = read_buffer_size() + + _resize_read_buffer_if_needed() + + enclosing._finish_initialization() + + new ssl_client(auth: TCPConnectAuth, + ssl_ctx: SSLContext val, + host: String, + port: String, + from: String, + enclosing: TCPConnectionActor ref, + ler: ClientLifecycleEventReceiver ref, + read_buffer_size: ReadBufferSize = DefaultReadBufferSize(), + ip_version: IPVersion = DualStack, + connection_timeout: (ConnectionTimeout | None) = None) + => + """ + Create a client-side SSL connection. The SSL session is created from the + provided SSLContext. If session creation fails, the connection reports + failure asynchronously via _on_connection_failure(ConnectionFailedSSL). + An optional `connection_timeout` bounds the connect-to-ready phase + (TCP Happy Eyeballs + TLS handshake). If the timeout fires before + `_on_connected`, the connection fails with `ConnectionFailedTimeout`. + """ + _lifecycle_event_receiver = ler + _enclosing = enclosing + _host = host + _port = port + _from = from + _read_buffer_size = read_buffer_size() + _read_buffer_min = read_buffer_size() + _ip_version = ip_version + match connection_timeout + | let ct: ConnectionTimeout => _connect_timeout_nsec = ct() * 1_000_000 + end + + try + _ssl = ssl_ctx.client(host)? + else + _ssl_failed = true + end + + _resize_read_buffer_if_needed() + + enclosing._finish_initialization() + + new ssl_server(auth: TCPServerAuth, + ssl_ctx: SSLContext val, + fd': U32, + enclosing: TCPConnectionActor ref, + ler: ServerLifecycleEventReceiver ref, + read_buffer_size: ReadBufferSize = DefaultReadBufferSize()) + => + """ + Create a server-side SSL connection. The SSL session is created from the + provided SSLContext. If session creation fails, the connection reports + failure asynchronously via _on_start_failure(StartFailedSSL) and closes the + fd. + """ + _fd = fd' + _lifecycle_event_receiver = ler + _enclosing = enclosing + _read_buffer_size = read_buffer_size() + _read_buffer_min = read_buffer_size() + + try + _ssl = ssl_ctx.server()? + else + _ssl_failed = true + end + + _resize_read_buffer_if_needed() + + enclosing._finish_initialization() + + new none() => + _enclosing = None + _lifecycle_event_receiver = None + + fun keepalive(secs: U32) => + """ + Sets the TCP keepalive timeout to approximately `secs` seconds. Exact + timing is OS dependent. If `secs` is zero, TCP keepalive is disabled. TCP + keepalive is disabled by default. This can only be set on a connected + socket. + """ + if _state.is_open() then + PonyTCP.keepalive(_fd, secs) + end + + fun set_nodelay(state: Bool): U32 => + """ + Turn Nagle on/off. Defaults to on (Nagle enabled, nodelay off). When + enabled (`state = true`), small writes are sent immediately without + waiting to coalesce — useful for latency-sensitive protocols. When + disabled (`state = false`), the OS may buffer small writes. + + Returns 0 on success, or a non-zero errno on failure. Only meaningful + on a connected socket — returns non-zero if the connection is not open. + """ + if not is_open() then return 1 end + _OSSocket.setsockopt_u32(_fd, OSSockOpt.ipproto_tcp(), + OSSockOpt.tcp_nodelay(), if state then 1 else 0 end) + + fun get_so_rcvbuf(): (U32, U32) => + """ + Get the OS receive buffer size for this socket. + + Returns a 2-tuple: (errno, value). On success, errno is 0 and value is + the buffer size in bytes. On failure, errno is non-zero and value should + be ignored. Only meaningful on a connected socket — returns (1, 0) if + the connection is not open. + """ + if not is_open() then return (1, 0) end + _OSSocket.get_so_rcvbuf(_fd) + + fun set_so_rcvbuf(bufsize: U32): U32 => + """ + Set the OS receive buffer size for this socket. The OS may round the + requested size up to a minimum or clamp it to a maximum. + + Returns 0 on success, or a non-zero errno on failure. Only meaningful + on a connected socket — returns non-zero if the connection is not open. + """ + if not is_open() then return 1 end + _OSSocket.set_so_rcvbuf(_fd, bufsize) + + fun get_so_sndbuf(): (U32, U32) => + """ + Get the OS send buffer size for this socket. + + Returns a 2-tuple: (errno, value). On success, errno is 0 and value is + the buffer size in bytes. On failure, errno is non-zero and value should + be ignored. Only meaningful on a connected socket — returns (1, 0) if + the connection is not open. + """ + if not is_open() then return (1, 0) end + _OSSocket.get_so_sndbuf(_fd) + + fun set_so_sndbuf(bufsize: U32): U32 => + """ + Set the OS send buffer size for this socket. The OS may round the + requested size up to a minimum or clamp it to a maximum. + + Returns 0 on success, or a non-zero errno on failure. Only meaningful + on a connected socket — returns non-zero if the connection is not open. + """ + if not is_open() then return 1 end + _OSSocket.set_so_sndbuf(_fd, bufsize) + + fun getsockopt(level: I32, option_name: I32, + option_max_size: USize = 4): (U32, Array[U8] iso^) + => + """ + General interface to `getsockopt(2)` for accessing any socket option. + + The `option_max_size` argument is the maximum number of bytes the caller + expects the kernel to return. This method allocates a buffer of that size + before calling `getsockopt(2)`. + + Returns a 2-tuple: on success, `(0, data)` where `data` is the bytes + returned by the kernel, sized to the actual length the kernel wrote. On + failure, `(errno, undefined)` — the second element must be ignored. Only + meaningful on a connected socket — returns `(1, empty)` if the connection + is not open. + + For commonly-tuned options, prefer the dedicated convenience methods + (`set_nodelay`, `get_so_rcvbuf`, etc.). Do not change the socket's + non-blocking mode — lori's event-driven I/O requires non-blocking + sockets. + """ + if not is_open() then return (1, recover Array[U8] end) end + _OSSocket.getsockopt(_fd, level, option_name, option_max_size) + + fun getsockopt_u32(level: I32, option_name: I32): (U32, U32) => + """ + Wrapper for `getsockopt(2)` where the kernel returns a C `uint32_t`. + + Returns a 2-tuple: on success, `(0, value)`. On failure, + `(errno, undefined)` — the second element must be ignored. Only + meaningful on a connected socket — returns `(1, 0)` if the connection + is not open. + + For commonly-tuned options, prefer the dedicated convenience methods + (`get_so_rcvbuf`, `get_so_sndbuf`, etc.). Do not change the socket's + non-blocking mode — lori's event-driven I/O requires non-blocking + sockets. + """ + if not is_open() then return (1, 0) end + _OSSocket.getsockopt_u32(_fd, level, option_name) + + fun setsockopt(level: I32, option_name: I32, option: Array[U8]): U32 => + """ + General interface to `setsockopt(2)` for setting any socket option. + + The caller is responsible for the correct size, byte contents, and + byte order of the `option` array for the requested `level` and + `option_name`. + + Returns 0 on success, or the value of `errno` on failure. Only + meaningful on a connected socket — returns non-zero if the connection + is not open. + + For commonly-tuned options, prefer the dedicated convenience methods + (`set_nodelay`, `set_so_rcvbuf`, etc.). Do not change the socket's + non-blocking mode — lori's event-driven I/O requires non-blocking + sockets. + """ + if not is_open() then return 1 end + _OSSocket.setsockopt(_fd, level, option_name, option) + + fun setsockopt_u32(level: I32, option_name: I32, option: U32): U32 => + """ + Wrapper for `setsockopt(2)` where the kernel expects a C `uint32_t`. + + Returns 0 on success, or the value of `errno` on failure. Only + meaningful on a connected socket — returns non-zero if the connection + is not open. + + For commonly-tuned options, prefer the dedicated convenience methods + (`set_nodelay`, `set_so_rcvbuf`, etc.). Do not change the socket's + non-blocking mode — lori's event-driven I/O requires non-blocking + sockets. + """ + if not is_open() then return 1 end + _OSSocket.setsockopt_u32(_fd, level, option_name, option) + + fun ref idle_timeout(duration: (IdleTimeout | None)) => + """ + Set or disable the idle timeout. Idle timeout is disabled by default. + + When `duration` is an `IdleTimeout`, the timer fires when no successful + send or receive occurs for that duration, delivering + `_on_idle_timeout()` to the lifecycle event receiver. When `duration` + is `None`, the idle timeout is disabled. + + The timer automatically re-arms after each firing until disabled or + the connection closes. + + Can be called before the connection is established — the value is + stored and the timer starts when the connection is ready. + + This is independent of TCP keepalive (`keepalive()`). TCP keepalive + is a transport-level probe that detects dead peers. Idle timeout is + application-level inactivity detection — it fires whether or not the + peer is alive. + """ + match \exhaustive\ duration + | let t: IdleTimeout => + _idle_timeout_nsec = t() * 1_000_000 + // _SSLHandshaking.is_open() = false blocks arming; the timer starts + // at ssl_handshake_complete. _TLSUpgrading.is_open() = true allows + // arming — the timer is already running from the plaintext phase. + if _state.is_open() then + if _timer_event.is_null() then + _arm_idle_timer() + else + _reset_idle_timer() + end + end + | None => + _idle_timeout_nsec = 0 + if _state.is_open() then + _cancel_idle_timer() + end + end + + fun ref set_timer(duration: TimerDuration): (TimerToken | SetTimerError) => + """ + Create a one-shot timer that fires `_on_timer()` after the configured + duration. Returns a `TimerToken` on success, or a `SetTimerError` on + failure. + + Unlike `idle_timeout()`, this timer has no I/O-reset behavior — it fires + unconditionally after the duration elapses, regardless of send/receive + activity. There is no automatic re-arming; call `set_timer()` again from + `_on_timer()` for repetition. + + Only one user timer can be active at a time. Setting a timer while one is + already active returns `SetTimerAlreadyActive` — call `cancel_timer()` + first. This prevents silent token invalidation. + + Requires the connection to be application-level connected: `is_open()` must + be true and the initial SSL handshake (if any) must have completed. TLS + upgrades via `start_tls()` do not block timer creation. + + The timer survives `close()` (graceful shutdown) but is cancelled by + `hard_close()`. + """ + // _SSLHandshaking.is_open() = false blocks timers during initial SSL + // handshake. _TLSUpgrading.is_open() = true allows them — the + // application already received _on_connected/_on_started. + if not is_open() then return SetTimerNotOpen end + if _user_timer_token isnt None then return SetTimerAlreadyActive end + + let nsec = duration() * 1_000_000 + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => + _user_timer_event = PonyAsio.create_timer_event(e, nsec) + | None => + _Unreachable() + end + let token = TimerToken._create(_next_timer_id = _next_timer_id + 1) + _user_timer_token = token + token + + fun ref cancel_timer(token: TimerToken) => + """ + Cancel an active timer. No-op if the token doesn't match the active timer + (already fired, already cancelled, wrong token). Safe to call with stale + tokens. + + No connection state check — timers can be cancelled during graceful + shutdown (`_Closing`) since they remain active until `hard_close()`. + """ + match _user_timer_token + | let t: TimerToken if t == token => + PonyAsio.unsubscribe(_user_timer_event) + _user_timer_event = AsioEvent.none() + _user_timer_token = None + end + + fun ref set_read_buffer_minimum(new_min: ReadBufferSize): + (ReadBufferResized | ReadBufferResizeBelowBufferSize) + => + """ + Set the shrink-back floor for the read buffer to exactly `new_min` bytes. + When the read buffer is empty and larger than the minimum, it shrinks back + to this size automatically. If the current buffer allocation is smaller + than `new_min`, the buffer is grown to match. + + Returns `ReadBufferResizeBelowBufferSize` if `new_min` is less than the + current buffer-until value. + """ + let min = new_min() + + if min < _user_buffer_until() then + return ReadBufferResizeBelowBufferSize + end + + _read_buffer_min = min + + if _read_buffer_size < min then + _read_buffer_size = min + _read_buffer.undefined(_read_buffer_size) + end + + ReadBufferResized + + fun ref resize_read_buffer(size': ReadBufferSize): ReadBufferResizeResult => + """ + Force the read buffer to exactly `size'` bytes, reallocating if different. + If `size'` is below the current minimum, the minimum is lowered to match. + + Returns `ReadBufferResizeBelowBufferSize` if `size'` is less than the + current buffer-until value, or `ReadBufferResizeBelowUsed` if `size'` is + less than the amount of unprocessed data currently in the buffer. + """ + let size = size'() + + if size < _user_buffer_until() then + return ReadBufferResizeBelowBufferSize + end + + if size < _bytes_in_read_buffer then + return ReadBufferResizeBelowUsed + end + + if size < _read_buffer_min then + _read_buffer_min = size + end + + _read_buffer_size = size + + let old_buffer = _read_buffer = recover Array[U8] end + _read_buffer = recover iso + let a = Array[U8](size) + a.undefined(size) + if _bytes_in_read_buffer > 0 then + (consume old_buffer).copy_to(a, 0, 0, _bytes_in_read_buffer) + end + a + end + + ReadBufferResized + + fun local_address(): net.NetAddress => + """ + Return the local IP address. If this TCPConnection is closed then the + address returned is invalid. + """ + recover + let ip: net.NetAddress ref = net.NetAddress + PonyTCP.sockname(_fd, ip) + ip + end + + fun remote_address(): net.NetAddress => + """ + Return the remote IP address. If this TCPConnection is closed then the + address returned is invalid. + """ + recover + let ip: net.NetAddress ref = net.NetAddress + PonyTCP.peername(_fd, ip) + ip + end + + fun ref mute() => + """ + Temporarily suspend reading off this TCPConnection until such time as + `unmute` is called. + """ + _muted = true + + fun ref unmute() => + """ + Start reading off this TCPConnection again after having been muted. + """ + _muted = false + // Trigger a read in case we ignored any previous ASIO notifications + _queue_read() + + fun ref yield_read() => + """ + Request the read loop to exit after the current `_on_received` callback + returns, giving other actors a chance to run. Reading resumes automatically + in the next scheduler turn — no explicit `unmute()` is needed. + + Call this from within `_on_received()` to implement application-level yield + policies (e.g. yield after N messages, after N bytes, or after a time + threshold). Unlike `mute()`/`unmute()`, which persistently stop reading + until reversed, `yield_read()` is a one-shot pause: the read loop resumes + on its own. + + For SSL connections, `yield_read()` operates at TCP-read granularity. All + SSL-decrypted messages from a single TCP read are delivered before the yield + takes effect, because the inner dispatch loop runs exactly once per TCP read + when SSL is active. + """ + _yield_read = true + + fun _user_buffer_until(): USize => + """ + The user's requested buffer-until value, regardless of whether SSL is + active. Returns 0 when `Streaming`, since 0 < any valid buffer min — the + correct behavior for invariant checks when no buffer-until constraint is + active. + """ + match \exhaustive\ _buffer_until + | let e: BufferSize => e() + | Streaming => 0 + end + + fun _tcp_buffer_until(): (BufferSize | Streaming) => + """ + The buffer-until value for the TCP read layer. When SSL is active, returns + `Streaming` because SSL record framing doesn't align with application + framing — the TCP layer reads all available data and lets `_ssl_poll()` + handle chunking via `_buffer_until`. When SSL is not active, returns the + user's `_buffer_until` value directly. + """ + match _ssl + | let _: SSL box => Streaming + | None => _buffer_until + end + + fun ref buffer_until(qty: (BufferSize | Streaming)): BufferUntilResult => + """ + Set the number of bytes to buffer before delivering data via + `_on_received`. When `qty` is `Streaming`, all available data is delivered + as it arrives. + + Returns `BufferSizeAboveMinimum` if `qty` exceeds the current read + buffer minimum. Raise the buffer minimum first, then set buffer_until. + """ + match qty + | let e: BufferSize => + if e() > _read_buffer_min then + return BufferSizeAboveMinimum + end + end + + match \exhaustive\ _lifecycle_event_receiver + | let _: EitherLifecycleEventReceiver => + _buffer_until = qty + | None => + _Unreachable() + end + + BufferUntilSet + + fun ref close() => + """ + Attempt to perform a graceful shutdown. Don't accept new writes. + + During the connecting phase (Happy Eyeballs in progress), transitions to + `_UnconnectedClosing` to drain inflight connection attempts. Each + straggler event is cleaned up as it arrives. Once all inflight connections + have drained, `_on_connection_failure` fires. + + If the connection is established and not muted, we won't finish closing + until we get a zero length read. If the connection is muted, perform a + hard close and shut down immediately. + """ + if _muted then + hard_close() + else + _state.close(this) + end + + fun ref hard_close() => + """ + When an error happens, do a non-graceful close. + """ + _state.hard_close(this) + + fun ref _hard_close_connecting() => + """ + Hard close during the connecting phase. Disposes SSL, fires the + appropriate failure callback, and cancels the idle, connect, and user + timers. The caller must set `_state = _Closed` before calling this. + """ + _shutdown = true + _shutdown_peer = true + match _ssl + | let ssl: SSL ref => + ssl.dispose() + _ssl = None + end + match _lifecycle_event_receiver + | let c: ClientLifecycleEventReceiver ref => + let reason = if _connect_timed_out then + ConnectionFailedTimeout + elseif _had_inflight then + ConnectionFailedTCP + else + ConnectionFailedDNS + end + c._on_connection_failure(reason) + end + _cancel_idle_timer() + _cancel_connect_timer() + _cancel_user_timer() + + fun ref _hard_close_cleanup() => + """ + Common teardown for hard-closing an established connection. Handles + shutdown flags, send_failed for pending token, clearing pending buffers, + cancelling all timers, unsubscribing the event, closing the fd, and + disposing SSL. Order is load-bearing: timer cancel before event + unsubscribe, SSL dispose after fd close. + + Does NOT set `_ssl = None` — the connection is terminal and nothing + accesses it afterward. The caller must set `_state = _Closed` before + calling this. + """ + _shutdown = true + _shutdown_peer = true + + // Fire _on_send_failed for any accepted-but-undelivered send before + // clearing the pending buffer. This is deferred via _notify_send_failed + // so it arrives in a subsequent turn, after _on_closed. + match (_pending_token, _enclosing) + | (let t: SendToken, let e: TCPConnectionActor ref) => + e._notify_send_failed(t) + end + + _pending_data.clear() + _pending_writev_total = 0 + _pending_first_buffer_offset = 0 + ifdef windows then + _pending_sent = 0 + end + _pending_token = None + + _cancel_idle_timer() + _cancel_connect_timer() + _cancel_user_timer() + PonyAsio.unsubscribe(_event) + _set_unreadable() + _set_unwriteable() + + // On windows, this will also cancel all outstanding IOCP operations. + PonyTCP.close(_fd) + _fd = -1 + + match _ssl + | let ssl: SSL ref => + ssl.dispose() + end + + fun ref _spawner_notification() => + """ + Notify the spawning listener (if any) that this server connection has + closed. For client connections, this is a no-op. + """ + match _lifecycle_event_receiver + | let e: ServerLifecycleEventReceiver ref => + match \exhaustive\ _spawned_by + | let spawner: TCPListenerActor => + spawner._connection_closed() + _spawned_by = None + | None => + // It is possible that we didn't yet receive the message giving us + // our spawner. Do nothing in that case. + None + end + end + + fun ref _hard_close_connected() => + """ + Hard close for an established connection where the application has been + notified (i.e., _on_connected/_on_started has already fired). Only + reachable from `_Open` and `_Closing` — handshake states have their own + hard-close methods. Fires `_on_closed` and notifies the spawner. The + caller must set `_state = _Closed` before calling this. + """ + _hard_close_cleanup() + + match \exhaustive\ _lifecycle_event_receiver + | let s: EitherLifecycleEventReceiver ref => + s._on_closed() + | None => + _Unreachable() + end + + _spawner_notification() + + fun ref _hard_close_ssl_handshaking() => + """ + Hard close during the initial SSL handshake (state: `_SSLHandshaking`). + The application has not been notified — fires `_on_connection_failure` + (client) or `_on_start_failure` (server). The caller must set + `_state = _Closed` before calling this. + """ + _hard_close_cleanup() + + match \exhaustive\ _lifecycle_event_receiver + | let s: EitherLifecycleEventReceiver ref => + match \exhaustive\ s + | let c: ClientLifecycleEventReceiver ref => + if _connect_timed_out then + c._on_connection_failure(ConnectionFailedTimeout) + else + c._on_connection_failure(ConnectionFailedSSL) + end + | let srv: ServerLifecycleEventReceiver ref => + srv._on_start_failure(StartFailedSSL) + end + | None => + _Unreachable() + end + + _spawner_notification() + + fun ref _hard_close_tls_upgrading() => + """ + Hard close during a TLS upgrade handshake (state: `_TLSUpgrading`). + The application was already notified of the plaintext connection, so + `_on_tls_failure` fires followed by `_on_closed`. The caller must set + `_state = _Closed` before calling this. + """ + _hard_close_cleanup() + + let reason = if _ssl_auth_failed then + TLSAuthFailed + else + TLSGeneralError + end + + match \exhaustive\ _lifecycle_event_receiver + | let s: EitherLifecycleEventReceiver ref => + s._on_tls_failure(reason) + s._on_closed() + | None => + _Unreachable() + end + + _spawner_notification() + + fun is_open(): Bool => + _state.is_open() + + fun is_closed(): Bool => + _state.is_closed() + + fun is_writeable(): Bool => + """ + Returns whether the connection can currently accept a `send()` call. + Checks that the state allows sends and the socket is writeable. + """ + _state.sends_allowed() and _writeable + + fun ref start_tls(ssl_ctx: SSLContext val, host: String = ""): + (None | StartTLSError) + => + """ + Initiate a TLS handshake on an established plaintext connection. Returns + `None` when the handshake has been started, or a `StartTLSError` if the + upgrade cannot proceed (the connection is unchanged in that case). + + Preconditions: the connection must be open, not already TLS, not muted, + have no unprocessed data in the read buffer, and have no pending writes. + The read buffer check prevents a man-in-the-middle from injecting pre-TLS + data that the application would process as post-TLS (CVE-2021-23222). + + On success, `_on_tls_ready()` fires when the handshake completes. During + the handshake, `send()` returns `SendErrorNotConnected`. If the handshake + fails, `_on_tls_failure` fires followed by `_on_closed()`. + + The `host` parameter is used for SNI (Server Name Indication) on client + connections. Pass an empty string for server connections or when SNI is + not needed. + """ + _state.start_tls(this, ssl_ctx, host) + + fun ref _do_start_tls(ssl_ctx: SSLContext val, host: String): + (None | StartTLSError) + => + match _ssl + | let _: SSL ref => return StartTLSAlreadyTLS + end + + // On POSIX, _has_pending_writes() checks whether any data remains + // unsent (writev is synchronous — returns bytes written or EWOULDBLOCK). + // On Windows IOCP, submitted-but-unconfirmed writes are already in the + // kernel's send buffer, so only un-submitted entries block TLS upgrade. + // After send("OK") + _iocp_submit_pending(), _pending_writev_total > 0 + // but _pending_data.size() == _pending_sent — the data is in the kernel + // and TLS can safely proceed. + let has_unsent_writes: Bool = ifdef windows then + _pending_data.size() > _pending_sent + else + _has_pending_writes() + end + + if _muted or (_bytes_in_read_buffer > 0) or has_unsent_writes then + return StartTLSNotReady + end + + let ssl = try + match \exhaustive\ _lifecycle_event_receiver + | let _: ClientLifecycleEventReceiver ref => + ssl_ctx.client(host)? + | let _: ServerLifecycleEventReceiver ref => + ssl_ctx.server()? + | None => + _Unreachable() + return StartTLSSessionFailed + end + else + return StartTLSSessionFailed + end + + _ssl = consume ssl + _state = _TLSUpgrading + _ssl_flush_sends() + None + + fun ref send(data: (ByteSeq | ByteSeqIter)): (SendToken | SendError) => + """ + Send data on this connection. Accepts a single buffer (`ByteSeq`) or + multiple buffers (`ByteSeqIter`). When multiple buffers are provided, + they are sent in a single writev syscall — avoiding both per-buffer + syscall overhead and the cost of copying into a contiguous buffer. + + Returns a `SendToken` on success, or a `SendError` explaining the + failure. When successful, `_on_sent(token)` will fire in a subsequent + behavior turn once the data has been fully handed to the OS. + """ + _state.send(this, data) + + fun ref _do_send(data: (ByteSeq | ByteSeqIter)): (SendToken | SendError) => + // Only reachable from _Open.send() — the handshake states return + // SendErrorNotConnected directly without calling this method. + if not _writeable then + return SendErrorNotWriteable + end + + _next_token_id = _next_token_id + 1 + let token = SendToken._create(_next_token_id) + + match \exhaustive\ _ssl + | let ssl: SSL ref => + match \exhaustive\ data + | let d: ByteSeq => + try ssl.write(d)? end + | let d: ByteSeqIter => + for v in d.values() do + try ssl.write(v)? end + end + end + _ssl_flush_sends() + + // Check if SSL error triggered close + if not is_open() then + return SendErrorNotConnected + end + | None => + match \exhaustive\ data + | let d: ByteSeq => + _enqueue(d) + | let d: ByteSeqIter => + for v in d.values() do + _enqueue(v) + end + end + ifdef windows then + _iocp_submit_pending() + else + _send_pending_writes() + end + end + + _reset_idle_timer() + + // Determine when to fire _on_sent + if not _has_pending_writes() then + // All data sent to OS immediately; defer _on_sent + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => + e._notify_sent(token) + | None => + _Unreachable() + end + else + // Partial write; _on_sent fires when pending list drains + _pending_token = token + end + + token + + fun ref _initiate_shutdown() => + """ + Send FIN to the peer if not already shutdown and no inflight connections + remain. Called when entering _Closing or when inflight connections drain + during _Closing. + """ + if not _shutdown and (_inflight_connections == 0) then + _shutdown = true + PonyTCP.shutdown(_fd) + end + + fun ref _check_shutdown_complete() => + """ + If both sides have shut down, perform a hard close. + """ + if _shutdown and _shutdown_peer then + hard_close() + end + + fun ref _enqueue(data: ByteSeq) => + """ + Add a buffer to the pending write queue. Callers must call the + platform-specific flush after enqueuing: `_send_pending_writes()` on + POSIX, `_iocp_submit_pending()` on Windows. + + Uses `not is_closed()` rather than `is_open()` because `_ssl_flush_sends()` + calls `_enqueue()` during `_SSLHandshaking` (where `is_open() = false`) + to push handshake protocol data. The wider guard allows handshake data + through while still blocking enqueue after the connection closes. + """ + if data.size() == 0 then return end + if not is_closed() then + _pending_data.push(data) + _pending_writev_total = _pending_writev_total + data.size() + end + + fun ref _manage_pending_buffer(bytes_sent: USize): USize => + """ + Account for `bytes_sent` by walking `_pending_data` entries. Returns + the number of fully-sent entries. Updates `_pending_first_buffer_offset`, + `_pending_writev_total`, and trims `_pending_data`. + """ + if bytes_sent == 0 then return 0 end + + var remaining = bytes_sent + var num_fully_sent: USize = 0 + var new_offset: USize = 0 + + while remaining > 0 do + try + let entry = _pending_data(num_fully_sent)? + let start = if num_fully_sent == 0 then + _pending_first_buffer_offset + else + USize(0) + end + let effective_size = entry.size() - start + + if effective_size <= remaining then + // Fully sent + num_fully_sent = num_fully_sent + 1 + remaining = remaining - effective_size + else + // Partially sent — this entry becomes the new entry 0 after trim + new_offset = start + remaining + remaining = 0 + end + else + _Unreachable() + end + end + + _pending_writev_total = _pending_writev_total - bytes_sent + _pending_data.trim_in_place(num_fully_sent) + _pending_first_buffer_offset = new_offset + + num_fully_sent + + fun ref _send_pending_writes() => + """ + Flush pending write data using writev. + This is POSIX only. + """ + ifdef posix then + let writev_batch_size: USize = PonyTCP.writev_max().usize() + + while _writeable and (_pending_writev_total > 0) do + try + // Determine batch size and byte count + let num_to_send: USize = + _pending_data.size().min(writev_batch_size) + + let bytes_to_send: USize = + if num_to_send == _pending_data.size() then + _pending_writev_total + else + var total: USize = 0 + var i: USize = 0 + while i < num_to_send do + let s = _pending_data(i)?.size() + total = total + + if i == 0 then s - _pending_first_buffer_offset else s end + i = i + 1 + end + total + end + + // writev syscall — returns bytes sent, 0 on EWOULDBLOCK + let len = PonyTCP.writev(_event, _pending_data, + 0, num_to_send, _pending_first_buffer_offset)? + + if len < bytes_to_send then + _manage_pending_buffer(len) + _apply_backpressure() + else + _manage_pending_buffer(bytes_to_send) + end + else + // writev error — non-graceful shutdown + hard_close() + return + end + end + + if _pending_writev_total == 0 then + _release_backpressure() + + match _pending_token + | let t: SendToken => + _pending_token = None + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => + e._notify_sent(t) + | None => + _Unreachable() + end + end + end + else + _Unreachable() + end + + fun ref _iocp_submit_pending() => + """ + Submit all pending write buffers to IOCP in a single WSASend. + Only one IOCP write is outstanding at a time — if a previous WSASend + hasn't completed yet, this is a no-op and the data waits in + `_pending_data` until `_write_completed` resubmits. + This is Windows only. + """ + ifdef windows then + if _pending_sent > 0 then return end + + let num_to_send = _pending_data.size() + if num_to_send == 0 then return end + + try + let len = PonyTCP.writev(_event, _pending_data, + 0, num_to_send, _pending_first_buffer_offset)? + + if len == 0 then + _apply_backpressure() + else + _pending_sent = len + end + else + hard_close() + end + else + _Unreachable() + end + + fun ref _write_completed(len: U32) => + """ + The OS has informed us that `len` bytes of pending writes have completed. + This occurs only with IOCP on Windows. + + A single WSASend call covers all submitted entries. When the IOCP + completion fires, the entire operation is done — none of the entries + are in-flight anymore. We reset `_pending_sent` to 0 and resubmit + any remaining data. + """ + ifdef windows then + if len == 0 then + hard_close() + return + end + + _manage_pending_buffer(len.usize()) + // The WSASend IOCP operation has completed. All entries covered by + // this operation are no longer in-flight. + _pending_sent = 0 + + if _pending_writev_total == 0 then + _release_backpressure() + + match _pending_token + | let t: SendToken => + _pending_token = None + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => + e._notify_sent(t) + | None => + _Unreachable() + end + end + else + // Resubmit remaining data + _iocp_submit_pending() + if _pending_sent < 16 then + _release_backpressure() + end + end + else + _Unreachable() + end + + fun ref _deliver_received(s: EitherLifecycleEventReceiver ref, + data: Array[U8] iso) + => + """ + Route incoming data through SSL decryption (if present) or directly + to the lifecycle event receiver. + """ + match \exhaustive\ _ssl + | let ssl: SSL ref => + ssl.receive(consume data) + _ssl_poll(s) + | None => + s._on_received(consume data) + end + + fun ref _read() => + ifdef posix then + _reset_idle_timer() + match \exhaustive\ _lifecycle_event_receiver + | let s: EitherLifecycleEventReceiver ref => + try + var total_bytes_read: USize = 0 + + while _readable do + // exit if muted + if _muted then + return + end + + // Handle any data already in the read buffer + while not _muted and _there_is_buffered_read_data() do + let bytes_to_consume = match \exhaustive\ _tcp_buffer_until() + | let e: BufferSize => e() + | Streaming => _bytes_in_read_buffer + end + + let x = _read_buffer = recover Array[U8] end + (let data', _read_buffer) = (consume x).chop(bytes_to_consume) + _bytes_in_read_buffer = _bytes_in_read_buffer - bytes_to_consume + + _deliver_received(s, consume data') + + // COUPLING: This check must remain immediately after + // _deliver_received() — moving it would change when the yield + // takes effect relative to application callbacks. + if _yield_read then + _yield_read = false + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => e._read_again() + | None => _Unreachable() + end + return + end + end + + // Yield after reading a buffer's worth of data to allow GC and + // other actors to run. _queue_read() schedules _read_again to + // resume. + if total_bytes_read >= _read_buffer_size then + _queue_read() + return + end + + _resize_read_buffer_if_needed() + + let bytes_read = PonyTCP.receive(_event, + _read_buffer.cpointer(_bytes_in_read_buffer), + _read_buffer.size() - _bytes_in_read_buffer)? + + if bytes_read == 0 then + // would block. try again later + _set_unreadable() + PonyAsio.resubscribe_read(_event) + return + end + + _bytes_in_read_buffer = _bytes_in_read_buffer + bytes_read + total_bytes_read = total_bytes_read + bytes_read + end + else + // The socket has been closed from the other side. + hard_close() + end + | None => + _Unreachable() + end + else + _Unreachable() + end + + fun ref _iocp_read() => + ifdef windows then + try + PonyTCP.receive(_event, + _read_buffer.cpointer(_bytes_in_read_buffer), + _read_buffer.size() - _bytes_in_read_buffer)? + else + close() + end + else + _Unreachable() + end + + fun ref _read_completed(len: U32) => + """ + The OS has informed us that `len` bytes of data has been read and is now + available. + """ + ifdef windows then + _reset_idle_timer() + match \exhaustive\ _lifecycle_event_receiver + | let s: EitherLifecycleEventReceiver ref => + if len == 0 then + // The socket has been closed from the other side, or a hard close has + // cancelled the queued read. + _set_unreadable() + _shutdown_peer = true + close() + return + end + + // Handle the data + _bytes_in_read_buffer = _bytes_in_read_buffer + len.usize() + + while not _muted and _there_is_buffered_read_data() + do + // get data to be distributed and update `_bytes_in_read_buffer` + let chop_at = match \exhaustive\ _tcp_buffer_until() + | let e: BufferSize => e() + | Streaming => _bytes_in_read_buffer + end + (let data, _read_buffer) = (consume _read_buffer).chop(chop_at) + _bytes_in_read_buffer = _bytes_in_read_buffer - chop_at + + _deliver_received(s, consume data) + + // COUPLING: This check must remain immediately after + // _deliver_received() — moving it would change when the yield + // takes effect relative to application callbacks. + if _yield_read then + _yield_read = false + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => e._read_again() + | None => _Unreachable() + end + return + end + + _resize_read_buffer_if_needed() + end + + _resize_read_buffer_if_needed() + _queue_read() + | None => + _Unreachable() + end + else + _Unreachable() + end + + fun ref _windows_resume_read() => + """ + Resume reading after a yield on Windows. Processes any buffered data first, + then submits an IOCP read for new data. Without this, yielding with + unprocessed buffered data and calling `_queue_read()` directly would leave + the buffered data unprocessed until new data arrives from the peer — which + might be never. + + Called via `_do_read_again()` from the `_read_again()` behavior, which is + deferred. The state machine guards against calling this after hard_close(): + `_Closed.read_again()` is a no-op. `_Closing.read_again()` correctly + calls this because the socket is still connected and we need an IOCP read + to detect the peer's FIN. + """ + ifdef windows then + match \exhaustive\ _lifecycle_event_receiver + | let s: EitherLifecycleEventReceiver ref => + while not _muted and _there_is_buffered_read_data() do + let chop_at = match \exhaustive\ _tcp_buffer_until() + | let e: BufferSize => e() + | Streaming => _bytes_in_read_buffer + end + (let data, _read_buffer) = (consume _read_buffer).chop(chop_at) + _bytes_in_read_buffer = _bytes_in_read_buffer - chop_at + + _deliver_received(s, consume data) + + // COUPLING: This check must remain immediately after + // _deliver_received() — moving it would change when the yield + // takes effect relative to application callbacks. + if _yield_read then + _yield_read = false + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => e._read_again() + | None => _Unreachable() + end + return + end + + _resize_read_buffer_if_needed() + end + + _resize_read_buffer_if_needed() + _queue_read() + | None => + _Unreachable() + end + else + _Unreachable() + end + + fun _there_is_buffered_read_data(): Bool => + match \exhaustive\ _tcp_buffer_until() + | let e: BufferSize => _bytes_in_read_buffer >= e() + | Streaming => _bytes_in_read_buffer > 0 + end + + fun ref _resize_read_buffer_if_needed() => + """ + Resize the read buffer if it's smaller than the buffer-until threshold, or + shrink it back to the minimum when empty and oversized. + """ + let needs_grow = match \exhaustive\ _tcp_buffer_until() + | let e: BufferSize => _read_buffer.size() <= e() + | Streaming => _read_buffer.size() == 0 + end + if needs_grow then + _read_buffer.undefined(_read_buffer_size) + elseif (_bytes_in_read_buffer == 0) + and (_read_buffer_size > _read_buffer_min) + then + _read_buffer_size = _read_buffer_min + _read_buffer = recover iso + let a = Array[U8](_read_buffer_size) + a.undefined(_read_buffer_size) + a + end + end + + fun ref _queue_read() => + ifdef posix then + // Trigger a read in case we ignored any previous ASIO notifications + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => + e._read_again() + return + | None => + _Unreachable() + end + else + _iocp_read() + end + + fun ref _apply_backpressure() => + match \exhaustive\ _lifecycle_event_receiver + | let s: EitherLifecycleEventReceiver => + if not _throttled then + _throttled = true + // throttled means we are also unwriteable + // being unthrottled doesn't however mean we are writable + _set_unwriteable() + ifdef not windows then + PonyAsio.resubscribe_write(_event) + end + s._on_throttled() + end + | None => + _Unreachable() + end + + fun ref _release_backpressure() => + match \exhaustive\ _lifecycle_event_receiver + | let s: EitherLifecycleEventReceiver => + if _throttled then + _throttled = false + s._on_unthrottled() + end + | None => + _Unreachable() + end + + fun ref _fire_on_sent(token: SendToken) => + """ + Dispatch _on_sent to the lifecycle event receiver. Called from + _notify_sent behavior on TCPConnectionActor. + """ + match \exhaustive\ _lifecycle_event_receiver + | let s: EitherLifecycleEventReceiver ref => + s._on_sent(token) + | None => + _Unreachable() + end + + fun ref _fire_on_send_failed(token: SendToken) => + """ + Dispatch _on_send_failed to the lifecycle event receiver. Called from + _notify_send_failed behavior on TCPConnectionActor. + """ + match \exhaustive\ _lifecycle_event_receiver + | let s: EitherLifecycleEventReceiver ref => + s._on_send_failed(token) + | None => + _Unreachable() + end + + fun ref _arm_idle_timer() => + """ + Create the ASIO timer event for idle timeout. Called when the connection + establishes and `_idle_timeout_nsec > 0`, or when `idle_timeout()` is + called on an established connection. + + Idempotent — if a timer already exists, this is a no-op. Prevents ASIO + timer event leaks from double-arm scenarios. + """ + if _idle_timeout_nsec == 0 then return end + if not _timer_event.is_null() then return end + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => + _timer_event = PonyAsio.create_timer_event(e, _idle_timeout_nsec) + | None => + _Unreachable() + end + + fun ref _reset_idle_timer() => + """ + Reset the idle timer to the configured duration. Called on I/O activity + (successful send, data received). Only resets an existing timer — does + not create one. + """ + if not _timer_event.is_null() then + PonyAsio.set_timer(_timer_event, _idle_timeout_nsec) + end + + fun ref _cancel_idle_timer() => + """ + Cancel the idle timer. Unsubscribes and clears `_timer_event` + immediately. The stale disposable notification (if any) no longer + matches `_timer_event` and is destroyed by `_event_notify`'s else + branch disposable check. + """ + if not _timer_event.is_null() then + PonyAsio.unsubscribe(_timer_event) + _timer_event = AsioEvent.none() + _idle_timeout_nsec = 0 + end + + fun ref _fire_idle_timeout() => + """ + Dispatch _on_idle_timeout to the lifecycle event receiver, then re-arm + the timer if the connection is still open and the timeout is still + configured. + """ + match \exhaustive\ _lifecycle_event_receiver + | let s: EitherLifecycleEventReceiver ref => + s._on_idle_timeout() + | None => + _Unreachable() + end + if is_open() and (_idle_timeout_nsec > 0) then + _reset_idle_timer() + end + + fun ref _arm_connect_timer() => + """ + Create the ASIO timer event for the connect timeout. Called after + `PonyTCP.connect` succeeds (at least one connection attempt is inflight). + No-op when `_connect_timeout_nsec == 0` (no timeout configured). + """ + if _connect_timeout_nsec == 0 then return end + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => + _connect_timer_event = + PonyAsio.create_timer_event(e, _connect_timeout_nsec) + | None => + _Unreachable() + end + + fun ref _cancel_connect_timer() => + """ + Cancel the connect timeout timer. Unsubscribes and clears + `_connect_timer_event` immediately. Stale disposable notifications + no longer match `_connect_timer_event` and are destroyed by + `_event_notify`'s else branch disposable check. + """ + if not _connect_timer_event.is_null() then + PonyAsio.unsubscribe(_connect_timer_event) + _connect_timer_event = AsioEvent.none() + _connect_timeout_nsec = 0 + end + + fun ref _fire_connect_timeout() => + """ + The connect timeout has fired. Sets `_connect_timed_out` so that + `hard_close()` routes the failure to `ConnectionFailedTimeout`, then + cancels the timer and hard-closes the connection. + """ + _connect_timed_out = true + _cancel_connect_timer() + hard_close() + + fun ref _fire_user_timer() => + """ + Dispatch `_on_timer` to the lifecycle event receiver. Called from + `_event_notify` when the user timer event fires. + + The token and event are cleared before the callback. If the callback + calls `set_timer()`, it creates a fresh ASIO event. The old event's + disposable notification arrives later, doesn't match + `_user_timer_event`, and is destroyed by `_event_notify`'s else + branch disposable check. + """ + let token = _user_timer_token + _user_timer_token = None + PonyAsio.unsubscribe(_user_timer_event) + _user_timer_event = AsioEvent.none() + match (token, _lifecycle_event_receiver) + | (let t: TimerToken, let s: EitherLifecycleEventReceiver ref) => + s._on_timer(t) + | (None, _) => + _Unreachable() + | (_, None) => + _Unreachable() + end + + fun ref _cancel_user_timer() => + """ + Cancel the user timer without firing the callback. Called from both + hard-close paths during cleanup. Stale disposable notifications no + longer match `_user_timer_event` and are destroyed by + `_event_notify`'s else branch disposable check. + """ + if not _user_timer_event.is_null() then + PonyAsio.unsubscribe(_user_timer_event) + _user_timer_event = AsioEvent.none() + _user_timer_token = None + end + + fun ref _ssl_flush_sends() => + """ + Flush any pending encrypted data from the SSL session to the wire. + Called after SSL operations that may produce output (handshake, write). + Enqueues all SSL chunks, then flushes once via writev. + """ + match _ssl + | let ssl: SSL ref => + try + while ssl.can_send() do + _enqueue(ssl.send()?) + end + end + ifdef windows then + _iocp_submit_pending() + else + _send_pending_writes() + end + end + + fun ref _ssl_poll(s: EitherLifecycleEventReceiver ref) => + """ + Check SSL state after receiving data. Handles handshake completion, + error detection, decrypted data delivery, and protocol data flushing. + """ + match _ssl + | let ssl: SSL ref => + match ssl.state() + | SSLReady => + if not _ssl_ready then + _ssl_ready = true + _state.ssl_handshake_complete(this, s) + end + | SSLAuthFail => + _ssl_auth_failed = true + hard_close() + return + | SSLError => + hard_close() + return + end + + // Read all available decrypted data + let ssl_read_buffer_until: USize = match \exhaustive\ _buffer_until + | let e: BufferSize => e() + | Streaming => 0 + end + while true do + match \exhaustive\ ssl.read(ssl_read_buffer_until) + | let d: Array[U8] iso => s._on_received(consume d) + | None => break + end + end + + // Flush any SSL protocol data (handshake responses, etc.) + _ssl_flush_sends() + end + + fun _has_pending_writes(): Bool => + _pending_writev_total > 0 + + fun ref read_again() => + _state.read_again(this) + + fun ref _dispatch_io_event(flags: U32, arg: U32) => + """ + Common I/O dispatch logic for socket events. Shared by all states that + have a connected socket and need to process I/O notifications. + """ + if AsioEvent.writeable(flags) then + _set_writeable() + ifdef windows then + _write_completed(arg) + else + _send_pending_writes() + end + end + + if AsioEvent.readable(flags) then + _set_readable() + ifdef windows then + _read_completed(arg) + else + _read() + end + end + + fun ref _do_read_again() => + ifdef posix then + _read() + else + _windows_resume_read() + end + + fun ref _set_state(state: _ConnectionState ref) => + _state = state + + fun ref _decrement_inflight(): U32 => + _inflight_connections = _inflight_connections - 1 + _inflight_connections + + fun ref _establish_connection(event: AsioEventID, fd: U32) => + """ + Called by _ClientConnecting when a Happy Eyeballs connection succeeds. + Promotes the event to the connection's own event, transitions to the + appropriate state, and sets up the connection for I/O. + """ + _event = event + _fd = fd + _set_writeable() + _set_readable() + + match \exhaustive\ _ssl + | let _: SSL ref => + _state = _SSLHandshaking + // Flush ClientHello to initiate SSL handshake. + // _on_connected() and _arm_idle_timer() deferred until + // ssl_handshake_complete. + _ssl_flush_sends() + | None => + _state = _Open + _arm_idle_timer() + _cancel_connect_timer() + match _lifecycle_event_receiver + | let c: ClientLifecycleEventReceiver ref => + c._on_connected() + end + end + + ifdef windows then + _queue_read() + else + _read() + if _has_pending_writes() then + _send_pending_writes() + end + end + + fun ref _connecting_event_failed(event: AsioEventID, fd: U32) => + """ + Called by _ClientConnecting when a Happy Eyeballs connection attempt + fails. Closes the fd and fires the connecting callback. Only + unsubscribes if the event hasn't already been unsubscribed — on + non-Windows systems, a race can cause the event to already be + disposable by the time we process it (see stdlib TCPConnection). + """ + // The message flags and the event struct's disposable status can + // disagree: a stale message may carry writeable/readable flags while + // the event struct has already been marked disposable by a prior + // unsubscribe. Check the struct before unsubscribing. + if not PonyAsio.get_disposable(event) then + PonyAsio.unsubscribe(event) + end + PonyTCP.close(fd) + _connecting_callback() + + fun ref _straggler_cleanup(event: AsioEventID) => + """ + Clean up a Happy Eyeballs straggler event after the winner has been + chosen. Unsubscribes (if not already disposable) and closes the fd. + Does NOT decrement _inflight_connections — caller handles that. + """ + // The message flags and the event struct's disposable status can + // disagree: a stale message may carry writeable/readable flags while + // the event struct has already been marked disposable by a prior + // unsubscribe. Check the struct before unsubscribing. + if not PonyAsio.get_disposable(event) then + PonyAsio.unsubscribe(event) + end + PonyTCP.close(PonyAsio.event_fd(event)) + + fun ref _event_notify(event: AsioEventID, flags: U32, arg: U32) => + // Explicit dispatch on event identity. Timer identity checks must come + // before `event is _event`. The else branch checks disposable first + // (stale timer disposables, straggler disposables), otherwise dispatches + // to foreign_event for Happy Eyeballs stragglers. + if event is _connect_timer_event then + _fire_connect_timeout() + elseif event is _timer_event then + _fire_idle_timeout() + elseif event is _user_timer_event then + _fire_user_timer() + elseif event is _event then + _state.own_event(this, flags, arg) + // A callback during own_event (e.g., _read_completed(0) → close()) can + // transition to _Closing and set _shutdown/_shutdown_peer, but + // _Open.own_event() won't check for shutdown completion. This ensures + // the check runs after every own-event dispatch, regardless of which + // state handled it. + _check_shutdown_complete() + if AsioEvent.disposable(flags) then + PonyAsio.destroy(event) + _event = AsioEvent.none() + end + else + // AsioEvent.disposable(flags) + if AsioEvent.disposable(flags) then + PonyAsio.destroy(event) + else + _state.foreign_event(this, event, flags, arg) + end + + // if AsioEvent.disposable(flags) then + // PonyAsio.destroy(event) + // end + end + + fun ref _connecting_callback() => + match \exhaustive\ _lifecycle_event_receiver + | let c: ClientLifecycleEventReceiver ref => + if _inflight_connections > 0 then + c._on_connecting(_inflight_connections) + else + hard_close() + end + | let s: ServerLifecycleEventReceiver ref => + _Unreachable() + | None => + _Unreachable() + end + + fun _is_socket_connected(fd: U32): Bool => + ifdef windows then + (let errno: U32, let value: U32) = _OSSocket.get_so_connect_time(fd) + (errno == 0) and (value != 0xffffffff) + else + (let errno: U32, let value: U32) = _OSSocket.get_so_error(fd) + (errno == 0) and (value == 0) + end + + fun ref _register_spawner(listener: TCPListenerActor) => + if _spawned_by is None then + if not _state.is_closed() then + // We were connected by the time the spawner was registered, + // so, let's let it know we were connected + _spawned_by = listener + else + // We were closed by the time the spawner was registered, + // so, let's let it know we were closed, And leave our "spawned by" as + // None. + listener._connection_closed() + end + else + _Unreachable() + end + + fun ref _finish_initialization() => + match \exhaustive\ _lifecycle_event_receiver + | let s: ServerLifecycleEventReceiver ref => + _complete_server_initialization(s) + | let c: ClientLifecycleEventReceiver ref => + _complete_client_initialization(c) + | None => + _Unreachable() + end + + fun ref _complete_client_initialization( + s: ClientLifecycleEventReceiver ref) + => + if _ssl_failed then + _state = _Closed + s._on_connection_failure(ConnectionFailedSSL) + return + end + + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => + _state = _ClientConnecting + + let asio_flags = ifdef windows then + AsioEvent.read_write() + else + AsioEvent.read_write_oneshot() + end + + _inflight_connections = PonyTCP.connect(e, _host, _port, _from, + asio_flags where ip_version = _ip_version) + _had_inflight = _inflight_connections > 0 + if _had_inflight then + _arm_connect_timer() + end + _connecting_callback() + | None => + _Unreachable() + end + + fun ref _complete_server_initialization( + s: ServerLifecycleEventReceiver ref) + => + if _ssl_failed then + PonyTCP.close(_fd) + _fd = -1 + _state = _Closed + s._on_start_failure(StartFailedSSL) + return + end + + match \exhaustive\ _enclosing + | let e: TCPConnectionActor ref => + _event = PonyAsio.create_event(e, _fd) + _set_readable() + _set_writeable() + + match \exhaustive\ _ssl + | let _: SSL ref => + _state = _SSLHandshaking + // Flush any initial SSL data (usually no-op for servers). + // _on_started() and _arm_idle_timer() deferred until + // ssl_handshake_complete. + _ssl_flush_sends() + | None => + _state = _Open + _arm_idle_timer() + s._on_started() + end + + // Queue up reads as we are now connected + // But might have been in a race with ASIO + _queue_read() + | None => + _Unreachable() + end + + fun ref _set_readable() => + _readable = true + PonyAsio.set_readable(_event) + + fun ref _set_unreadable() => + _readable = false + PonyAsio.set_unreadable(_event) + + fun ref _set_writeable() => + _writeable = true + PonyAsio.set_writeable(_event) + + fun ref _set_unwriteable() => + _writeable = false + PonyAsio.set_unwriteable(_event) diff --git a/corral/_vendor/lori/tcp_connection_actor.pony b/corral/_vendor/lori/tcp_connection_actor.pony new file mode 100644 index 0000000..827d6be --- /dev/null +++ b/corral/_vendor/lori/tcp_connection_actor.pony @@ -0,0 +1,42 @@ +trait tag TCPConnectionActor is AsioEventNotify + fun ref _connection(): TCPConnection + + be dispose() => + """ + Close connection + """ + // hard_close() — disposal is unconditional teardown, not graceful shutdown. + // See #229 for the edge-triggered race that makes close() unreliable here. + _connection().hard_close() + + be _event_notify(event: AsioEventID, flags: U32, arg: U32) => + _connection()._event_notify(event, flags, arg) + + be _read_again() => + """ + Resume reading. On POSIX, re-enters the read loop which processes buffered + data and reads from the socket. On Windows, processes buffered data first + then submits a new IOCP read. + """ + _connection().read_again() + + be _register_spawner(listener: TCPListenerActor) => + """ + Register the listener as the spawner of this connection + """ + _connection()._register_spawner(listener) + + be _notify_sent(token: SendToken) => + """ + Deferred delivery of _on_sent to the lifecycle event receiver. + """ + _connection()._fire_on_sent(token) + + be _notify_send_failed(token: SendToken) => + """ + Deferred delivery of _on_send_failed to the lifecycle event receiver. + """ + _connection()._fire_on_send_failed(token) + + be _finish_initialization() => + _connection()._finish_initialization() diff --git a/corral/_vendor/lori/tcp_listener.pony b/corral/_vendor/lori/tcp_listener.pony new file mode 100644 index 0000000..d950d54 --- /dev/null +++ b/corral/_vendor/lori/tcp_listener.pony @@ -0,0 +1,170 @@ +use "collections" +use net = "net" + +class TCPListener + let _host: String + let _port: String + let _limit: (MaxSpawn | None) + let _ip_version: IPVersion + var _open_connections: U32 = 0 + var _paused: Bool = false + var _event: AsioEventID = AsioEvent.none() + var _fd: U32 = -1 + var _listening: Bool = false + var _enclosing: (TCPListenerActor ref | None) + + new create(auth: TCPListenAuth, host: String, port: String, + enclosing: TCPListenerActor ref, ip_version: IPVersion = DualStack, + limit: (MaxSpawn | None) = DefaultMaxSpawn()) + => + _host = host + _port = port + _ip_version = ip_version + _limit = limit + _enclosing = enclosing + enclosing._finish_initialization() + + new none() => + _host = "" + _port = "" + _limit = None + _ip_version = DualStack + _enclosing = None + + fun ref close() => + match \exhaustive\ _enclosing + | let e: TCPListenerActor ref => + // TODO: when in debug mode we should blow up if listener is closed + if _listening then + _listening = false + + if not _event.is_null() then + PonyAsio.unsubscribe(_event) + PonyTCP.close(_fd) + _fd = -1 + e._on_closed() + end + end + | None => + _Unreachable() + end + + fun local_address(): net.NetAddress => + """ + Return the local IP address. If this TCPListener is closed then the + address returned is invalid. + """ + recover + let ip: net.NetAddress ref = net.NetAddress + PonyTCP.sockname(_fd, ip) + ip + end + + fun ref _event_notify(event: AsioEventID, flags: U32, arg: U32) => + if event isnt _event then + return + end + + if AsioEvent.readable(flags) then + _accept(arg) + end + + if AsioEvent.disposable(flags) then + PonyAsio.destroy(_event) + _event = AsioEvent.none() + _listening = false + end + + fun ref _accept(arg: U32 = 0) => + match \exhaustive\ _enclosing + | let e: TCPListenerActor ref => + if _listening then + ifdef windows then + // Unsubscribe if we get an invalid socket in an event + if arg == -1 then + PonyAsio.unsubscribe(_event) + return + end + + try + if arg > 0 then + let opened = e._on_accept(arg)? + opened._register_spawner(e) + _open_connections = _open_connections + 1 + end + + if not _at_connection_limit() then + PonyTCP.accept(_event) + else + _paused = true + end + else + PonyTCP.close(arg) + end + else + while not _at_connection_limit() do + var fd = PonyTCP.accept(_event) + + // 0: would block, -1: error + if fd <= 0 then + return + end + + try + let opened = e._on_accept(fd.u32())? + opened._register_spawner(e) + _open_connections = _open_connections + 1 + else + PonyTCP.close(fd.u32()) + end + end + + _paused = true + end + else + // It's possible that after closing, we got an event for a connection + // attempt. If that is the case or the listener is otherwise not open, + // return and do not start a new connection + ifdef windows then + if arg == -1 then + PonyAsio.unsubscribe(_event) + return + end + + if arg > 0 then + PonyTCP.close(arg) + end + end + return + end + | None => + _Unreachable() + end + + fun _at_connection_limit(): Bool => + match \exhaustive\ _limit + | let l: MaxSpawn => _open_connections >= l() + | None => false + end + + fun ref _connection_closed() => + _open_connections = _open_connections - 1 + if _paused and not _at_connection_limit() then + _paused = false + _accept() + end + + fun ref _finish_initialization() => + match \exhaustive\ _enclosing + | let e: TCPListenerActor ref => + _event = PonyTCP.listen(e, _host, _port where ip_version = _ip_version) + if not _event.is_null() then + _fd = PonyAsio.event_fd(_event) + _listening = true + e._on_listening() + else + e._on_listen_failure() + end + | None => + _Unreachable() + end diff --git a/corral/_vendor/lori/tcp_listener_actor.pony b/corral/_vendor/lori/tcp_listener_actor.pony new file mode 100644 index 0000000..9122346 --- /dev/null +++ b/corral/_vendor/lori/tcp_listener_actor.pony @@ -0,0 +1,40 @@ +trait tag TCPListenerActor is AsioEventNotify + fun ref _listener(): TCPListener + + fun ref _on_accept(fd: U32): TCPConnectionActor ? + """ + Called when a connection is accepted + """ + + fun ref _on_closed() => + """ + Called after the listener is closed + """ + None + + fun ref _on_listen_failure() => + """ + Called if we are unable to open the listener + """ + None + + fun ref _on_listening() => + """ + Called once the listener is ready to accept connections + """ + None + + be dispose() => + """ + Stop listening + """ + _listener().close() + + be _event_notify(event: AsioEventID, flags: U32, arg: U32) => + _listener()._event_notify(event, flags, arg) + + be _connection_closed() => + _listener()._connection_closed() + + be _finish_initialization() => + _listener()._finish_initialization() diff --git a/corral/_vendor/lori/timer_duration.pony b/corral/_vendor/lori/timer_duration.pony new file mode 100644 index 0000000..75ab4da --- /dev/null +++ b/corral/_vendor/lori/timer_duration.pony @@ -0,0 +1,52 @@ +use "constrained_types" + +primitive TimerDurationValidator is Validator[U64] + """ + Validates that a timer duration is within the allowed range. + + The minimum value is 1 millisecond. The maximum value is + 18,446,744,073,709 milliseconds (~213,503 days) — the largest value + that can be converted to nanoseconds without overflowing U64. + + Used by `MakeTimerDuration` to construct `TimerDuration` values. + """ + fun apply(value: U64): ValidationResult => + if value == 0 then + recover val + ValidationFailure( + "timer duration must be greater than zero") + end + elseif value > _max_millis() then + recover val + ValidationFailure( + "timer duration must be at most " + + _max_millis().string() + + " milliseconds") + end + else + ValidationSuccess + end + + fun _max_millis(): U64 => + """ + The maximum timer duration in milliseconds. Values above this would + overflow U64 when converted to nanoseconds internally. + """ + U64.max_value() / 1_000_000 + +type TimerDuration is Constrained[U64, TimerDurationValidator] + """ + A validated timer duration in milliseconds. The allowed range is + 1 to 18,446,744,073,709 milliseconds (~213,503 days). The upper bound + ensures the value can be safely converted to nanoseconds without + overflowing U64. + + Construct with `MakeTimerDuration(milliseconds)`, which returns + `(TimerDuration | ValidationFailure)`. Pass to `set_timer()` to create + a one-shot timer. + """ + +type MakeTimerDuration is MakeConstrained[U64, TimerDurationValidator] + """ + Factory for `TimerDuration` values. Returns `(TimerDuration | ValidationFailure)`. + """ diff --git a/corral/_vendor/lori/timer_token.pony b/corral/_vendor/lori/timer_token.pony new file mode 100644 index 0000000..d66527b --- /dev/null +++ b/corral/_vendor/lori/timer_token.pony @@ -0,0 +1,36 @@ +class val TimerToken is Equatable[TimerToken] + """ + Identifies a timer operation. Returned by `set_timer()` on success and + delivered to `_on_timer()` when the timer fires. + + Tokens use structural equality based on their ID, which is scoped per + connection. Applications managing multiple connections should pair tokens + with connection identity to avoid ambiguity. + """ + let id: USize + + new val _create(id': USize) => + id = id' + + fun eq(that: box->TimerToken): Bool => + id == that.id + + fun ne(that: box->TimerToken): Bool => + not eq(that) + +primitive SetTimerNotOpen + """ + The connection is not application-level connected. Either the connection + is not open, or an initial SSL handshake is still in progress (before + `_on_connected`/`_on_started` has fired). + """ + +primitive SetTimerAlreadyActive + """ + A timer is already active. Cancel it with `cancel_timer()` before setting + a new one. This prevents silent token invalidation — see `send()` returning + `SendError` for the same design rationale. + """ + +type SetTimerError is + (SetTimerNotOpen | SetTimerAlreadyActive) diff --git a/corral/_vendor/lori/tls_failure_reason.pony b/corral/_vendor/lori/tls_failure_reason.pony new file mode 100644 index 0000000..4c0cfd1 --- /dev/null +++ b/corral/_vendor/lori/tls_failure_reason.pony @@ -0,0 +1,13 @@ +primitive TLSAuthFailed + """ + The TLS handshake failed due to an authentication error (certificate + validation failure, untrusted CA, hostname mismatch, etc.). + """ + +primitive TLSGeneralError + """ + The TLS handshake failed due to a protocol error other than authentication + (unexpected message, unsupported version, internal SSL error, etc.). + """ + +type TLSFailureReason is (TLSAuthFailed | TLSGeneralError) diff --git a/corral/_vendor/ssl/crypto/constant_time_compare.pony b/corral/_vendor/ssl/crypto/constant_time_compare.pony new file mode 100755 index 0000000..5c69c13 --- /dev/null +++ b/corral/_vendor/ssl/crypto/constant_time_compare.pony @@ -0,0 +1,21 @@ +primitive ConstantTimeCompare + fun apply[S: ByteSeq box = ByteSeq box](xs: S, ys: S): Bool => + """ + Return true if the two ByteSeqs, xs and ys, have equal contents. The time + taken is independent of the contents. + """ + if xs.size() != ys.size() then + false + else + var v = U8(0) + var i: USize = 0 + while i < xs.size() do + try + v = v or (xs(i)? xor ys(i)?) + else + return false + end + i = i + 1 + end + v == 0 + end diff --git a/corral/_vendor/ssl/crypto/crypto.pony b/corral/_vendor/ssl/crypto/crypto.pony new file mode 100755 index 0000000..74eaa78 --- /dev/null +++ b/corral/_vendor/ssl/crypto/crypto.pony @@ -0,0 +1,13 @@ +""" +The crypto package provides cryptographic primitives built on OpenSSL: + +* One-shot hash functions (`MD5`, `SHA256`, etc.) and streaming digests + (`Digest`) +* HMAC message authentication (`HmacSha256`) +* PBKDF2 key derivation (`Pbkdf2Sha256`, requires OpenSSL 1.1.x or 3.0.x) +* Cryptographically secure random bytes (`RandBytes`) +* Constant-time comparison (`ConstantTimeCompare`) +""" + +use @pony_ctx[Pointer[None]]() +use @pony_alloc[Pointer[U8]](ctx: Pointer[None], size: USize) diff --git a/corral/_vendor/ssl/crypto/digest.pony b/corral/_vendor/ssl/crypto/digest.pony new file mode 100755 index 0000000..36b63df --- /dev/null +++ b/corral/_vendor/ssl/crypto/digest.pony @@ -0,0 +1,216 @@ +use "path:/usr/local/opt/libressl/lib" if osx and x86 +use "path:/opt/homebrew/opt/libressl/lib" if osx and arm +use "lib:crypto" +use "lib:bcrypt" if windows + +use @EVP_MD_CTX_new[Pointer[_EVPCTX]]() if "openssl_1.1.x" or "openssl_3.0.x" or "libressl" +use @EVP_DigestInit_ex[I32](ctx: Pointer[_EVPCTX] tag, t: Pointer[_EVPMD], impl: USize) +use @EVP_DigestUpdate[I32](ctx: Pointer[_EVPCTX] tag, d: Pointer[U8] tag, cnt: USize) +use @EVP_DigestFinal_ex[I32](ctx: Pointer[_EVPCTX] tag, md: Pointer[U8] tag, s: Pointer[USize]) +use @EVP_DigestFinalXOF[I32](ctx: Pointer[_EVPCTX] tag, md: Pointer[U8] tag, len: USize) if "openssl_3.0.x" +use @EVP_MD_CTX_free[None](ctx: Pointer[_EVPCTX]) if "openssl_1.1.x" or "openssl_3.0.x" or "libressl" + +use @EVP_md5[Pointer[_EVPMD]]() +use @EVP_ripemd160[Pointer[_EVPMD]]() +use @EVP_sha1[Pointer[_EVPMD]]() +use @EVP_sha224[Pointer[_EVPMD]]() +use @EVP_sha256[Pointer[_EVPMD]]() +use @EVP_sha384[Pointer[_EVPMD]]() +use @EVP_sha512[Pointer[_EVPMD]]() +use @EVP_shake128[Pointer[_EVPMD]]() +use @EVP_shake256[Pointer[_EVPMD]]() + +primitive _EVPMD +primitive _EVPCTX + +class Digest + """ + Produces a hash from the chunks of input. Feed the input with append() and + produce a final hash from the concatenation of the input with final(). + """ + let _digest_size: USize + let _ctx: Pointer[_EVPCTX] + let _variable_length: Bool + var _hash: (Array[U8] val | None) = None + + new md5() => + """ + Use the MD5 algorithm to calculate the hash. + """ + _variable_length = false + _digest_size = 16 + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + _ctx = @EVP_MD_CTX_new() + else + compile_error "You must select an SSL version to use." + end + @EVP_DigestInit_ex(_ctx, @EVP_md5(), USize(0)) + + new ripemd160() => + """ + Use the RIPEMD160 algorithm to calculate the hash. + """ + _variable_length = false + _digest_size = 20 + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + _ctx = @EVP_MD_CTX_new() + else + compile_error "You must select an SSL version to use." + end + @EVP_DigestInit_ex(_ctx, @EVP_ripemd160(), USize(0)) + + new sha1() => + """ + Use the SHA1 algorithm to calculate the hash. + """ + _variable_length = false + _digest_size = 20 + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + _ctx = @EVP_MD_CTX_new() + else + compile_error "You must select an SSL version to use." + end + @EVP_DigestInit_ex(_ctx, @EVP_sha1(), USize(0)) + + new sha224() => + """ + Use the SHA256 algorithm to calculate the hash. + """ + _variable_length = false + _digest_size = 28 + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + _ctx = @EVP_MD_CTX_new() + else + compile_error "You must select an SSL version to use." + end + @EVP_DigestInit_ex(_ctx, @EVP_sha224(), USize(0)) + + new sha256() => + """ + Use the SHA256 algorithm to calculate the hash. + """ + _variable_length = false + _digest_size = 32 + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + _ctx = @EVP_MD_CTX_new() + else + compile_error "You must select an SSL version to use." + end + @EVP_DigestInit_ex(_ctx, @EVP_sha256(), USize(0)) + + new sha384() => + """ + Use the SHA384 algorithm to calculate the hash. + """ + _variable_length = false + _digest_size = 48 + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + _ctx = @EVP_MD_CTX_new() + else + compile_error "You must select an SSL version to use." + end + @EVP_DigestInit_ex(_ctx, @EVP_sha384(), USize(0)) + + new sha512() => + """ + Use the SHA512 algorithm to calculate the hash. + """ + _variable_length = false + _digest_size = 64 + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + _ctx = @EVP_MD_CTX_new() + else + compile_error "You must select an SSL version to use." + end + @EVP_DigestInit_ex(_ctx, @EVP_sha512(), USize(0)) + + new shake128(size': USize = 16) => + """ + Use the SHAKE128 algorithm to calculate the hash. + + SHAKE128 is an extendable output function (XOF) that can produce + variable-length output. The `size'` parameter controls the output length + in bytes (default: 16). Variable-length output requires OpenSSL 3.0.x; + on OpenSSL 1.1.x, the default size is always used. + """ + ifdef "openssl_1.1.x" or "openssl_3.0.x" then + ifdef "openssl_3.0.x" then + _variable_length = true + _digest_size = size' + else + _variable_length = false + _digest_size = 16 + end + _ctx = @EVP_MD_CTX_new() + @EVP_DigestInit_ex(_ctx, @EVP_shake128(), USize(0)) + else + compile_error "shake128 is only supported with OpenSSL 1.1.x or 3.0.x" + end + + new shake256(size': USize = 32) => + """ + Use the SHAKE256 algorithm to calculate the hash. + + SHAKE256 is an extendable output function (XOF) that can produce + variable-length output. The `size'` parameter controls the output length + in bytes (default: 32). Variable-length output requires OpenSSL 3.0.x; + on OpenSSL 1.1.x, the default size is always used. + """ + ifdef "openssl_1.1.x" or "openssl_3.0.x" then + ifdef "openssl_3.0.x" then + _variable_length = true + _digest_size = size' + else + _variable_length = false + _digest_size = 32 + end + _ctx = @EVP_MD_CTX_new() + @EVP_DigestInit_ex(_ctx, @EVP_shake256(), USize(0)) + else + compile_error "shake256 is only supported with OpenSSL 1.1.x or 3.0.x" + end + + fun ref append(input: ByteSeq) ? => + """ + Update the Digest object with input. Throw an error if final() has been + called. + """ + if _hash isnt None then error end + @EVP_DigestUpdate(_ctx, input.cpointer(), input.size()) + + fun ref final(): Array[U8] val => + """ + Return the digest of the strings passed to the append() method. + """ + match _hash + | let h: Array[U8] val => h + else + let size = _digest_size + let digest = + recover String.from_cpointer( + @pony_alloc(@pony_ctx(), size), size) + end + if not _variable_length then + @EVP_DigestFinal_ex(_ctx, digest.cpointer(), Pointer[USize]) + else + ifdef "openssl_3.0.x" then + @EVP_DigestFinalXOF(_ctx, digest.cpointer(), size) + else + @EVP_DigestFinal_ex(_ctx, digest.cpointer(), Pointer[USize]) + end + end + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + @EVP_MD_CTX_free(_ctx) + else + compile_error "You must select an SSL version to use." + end + let h = (consume digest).array() + _hash = h + h + end + + fun digest_size(): USize => + """ + Return the size of the message digest in bytes. + """ + _digest_size diff --git a/corral/_vendor/ssl/crypto/hash_fn.pony b/corral/_vendor/ssl/crypto/hash_fn.pony new file mode 100755 index 0000000..e442349 --- /dev/null +++ b/corral/_vendor/ssl/crypto/hash_fn.pony @@ -0,0 +1,134 @@ +use "path:/usr/local/opt/libressl/lib" if osx and x86 +use "path:/opt/homebrew/opt/libressl/lib" if osx and arm +use "lib:crypto" + +use @MD4[Pointer[U8]](d: Pointer[U8] tag, n: USize, md: Pointer[U8]) +use @MD5[Pointer[U8]](d: Pointer[U8] tag, n: USize, md: Pointer[U8]) +use @RIPEMD160[Pointer[U8]](d: Pointer[U8] tag, n: USize, md: Pointer[U8]) +use @SHA1[Pointer[U8]](d: Pointer[U8] tag, n: USize, md: Pointer[U8]) +use @SHA224[Pointer[U8]](d: Pointer[U8] tag, n: USize, md: Pointer[U8]) +use @SHA256[Pointer[U8]](d: Pointer[U8] tag, n: USize, md: Pointer[U8]) +use @SHA384[Pointer[U8]](d: Pointer[U8] tag, n: USize, md: Pointer[U8]) +use @SHA512[Pointer[U8]](d: Pointer[U8] tag, n: USize, md: Pointer[U8]) + +use "format" + +interface HashFn + """ + Produces a fixed-length byte array based on the input sequence. + """ + fun tag apply(input: ByteSeq): Array[U8] val + +primitive MD4 is HashFn + fun tag apply(input: ByteSeq): Array[U8] val => + """ + Compute the MD4 message digest conforming to RFC 1320 + """ + recover + let size: USize = 16 + let digest = @pony_alloc(@pony_ctx(), size) + @MD4(input.cpointer(), input.size(), digest) + Array[U8].from_cpointer(digest, size) + end + +primitive MD5 is HashFn + fun tag apply(input: ByteSeq): Array[U8] val => + """ + Compute the MD5 message digest conforming to RFC 1321 + """ + recover + let size: USize = 16 + let digest = @pony_alloc(@pony_ctx(), size) + @MD5(input.cpointer(), input.size(), digest) + Array[U8].from_cpointer(digest, size) + end + +primitive RIPEMD160 is HashFn + fun tag apply(input: ByteSeq): Array[U8] val => + """ + Compute the RIPEMD160 message digest conforming to ISO/IEC 10118-3 + """ + recover + let size: USize = 20 + let digest = @pony_alloc(@pony_ctx(), size) + @RIPEMD160(input.cpointer(), input.size(), digest) + Array[U8].from_cpointer(digest, size) + end + +primitive SHA1 is HashFn + fun tag apply(input: ByteSeq): Array[U8] val => + """ + Compute the SHA1 message digest conforming to US Federal Information + Processing Standard FIPS PUB 180-4 + """ + recover + let size: USize = 20 + let digest = @pony_alloc(@pony_ctx(), size) + @SHA1(input.cpointer(), input.size(), digest) + Array[U8].from_cpointer(digest, size) + end + +primitive SHA224 is HashFn + fun tag apply(input: ByteSeq): Array[U8] val => + """ + Compute the SHA224 message digest conforming to US Federal Information + Processing Standard FIPS PUB 180-4 + """ + recover + let size: USize = 28 + let digest = @pony_alloc(@pony_ctx(), size) + @SHA224(input.cpointer(), input.size(), digest) + Array[U8].from_cpointer(digest, size) + end + +primitive SHA256 is HashFn + fun tag apply(input: ByteSeq): Array[U8] val => + """ + Compute the SHA256 message digest conforming to US Federal Information + Processing Standard FIPS PUB 180-4 + """ + recover + let size: USize = 32 + let digest = @pony_alloc(@pony_ctx(), size) + @SHA256(input.cpointer(), input.size(), digest) + Array[U8].from_cpointer(digest, size) + end + +primitive SHA384 is HashFn + fun tag apply(input: ByteSeq): Array[U8] val => + """ + Compute the SHA384 message digest conforming to US Federal Information + Processing Standard FIPS PUB 180-4 + """ + recover + let size: USize = 48 + let digest = @pony_alloc(@pony_ctx(), size) + @SHA384(input.cpointer(), input.size(), digest) + Array[U8].from_cpointer(digest, size) + end + +primitive SHA512 is HashFn + fun tag apply(input: ByteSeq): Array[U8] val => + """ + Compute the SHA512 message digest conforming to US Federal Information + Processing Standard FIPS PUB 180-4 + """ + recover + let size: USize = 64 + let digest = @pony_alloc(@pony_ctx(), size) + @SHA512(input.cpointer(), input.size(), digest) + Array[U8].from_cpointer(digest, size) + end + +primitive ToHexString + fun tag apply(bs: Array[U8] val): String => + """ + Return the lower-case hexadecimal string representation of the given Array + of U8. + """ + let out = recover String(bs.size() * 2) end + for c in bs.values() do + out.append(Format.int[U8](c where + fmt = FormatHexSmallBare, width = 2, fill = '0')) + end + consume out diff --git a/corral/_vendor/ssl/crypto/hmac_sha256.pony b/corral/_vendor/ssl/crypto/hmac_sha256.pony new file mode 100644 index 0000000..72fb72b --- /dev/null +++ b/corral/_vendor/ssl/crypto/hmac_sha256.pony @@ -0,0 +1,30 @@ +use "path:/usr/local/opt/libressl/lib" if osx and x86 +use "path:/opt/homebrew/opt/libressl/lib" if osx and arm +use "lib:crypto" + +use @HMAC[Pointer[U8]]( + evp_md: Pointer[_EVPMD], + key: Pointer[U8] tag, key_len: I32, + data: Pointer[U8] tag, data_len: USize, + md: Pointer[U8] tag, md_len: Pointer[U32]) + +primitive HmacSha256 + """ + Compute HMAC using SHA-256 as the hash function, as defined in RFC 2104. + + Returns a 32-byte message authentication code. + + ```pony + let mac = HmacSha256("secret-key", "Hello, World!") + ``` + """ + fun tag apply(key: ByteSeq, data: ByteSeq): Array[U8] val => + recover + // Use Array.init instead of pony_alloc + from_cpointer to avoid + // intermittent GC buffer corruption. See ponyc#4831. + let size: USize = 32 + let arr = Array[U8].init(0, size) + @HMAC(@EVP_sha256(), key.cpointer(), key.size().i32(), + data.cpointer(), data.size(), arr.cpointer(), Pointer[U32]) + arr + end diff --git a/corral/_vendor/ssl/crypto/pbkdf2_sha256.pony b/corral/_vendor/ssl/crypto/pbkdf2_sha256.pony new file mode 100644 index 0000000..9c97213 --- /dev/null +++ b/corral/_vendor/ssl/crypto/pbkdf2_sha256.pony @@ -0,0 +1,43 @@ +use "path:/usr/local/opt/libressl/lib" if osx and x86 +use "path:/opt/homebrew/opt/libressl/lib" if osx and arm +use "lib:crypto" + +use @PKCS5_PBKDF2_HMAC[I32]( + pass: Pointer[U8] tag, passlen: I32, + salt: Pointer[U8] tag, saltlen: I32, iter: I32, + digest: Pointer[_EVPMD], + keylen: I32, out: Pointer[U8] tag) if "openssl_1.1.x" or "openssl_3.0.x" or "libressl" + +primitive Pbkdf2Sha256 + """ + Derive a key from a password using PBKDF2 with HMAC-SHA-256 as the PRF, + as defined in RFC 2898. + + Returns a key of the requested length, or raises an error if the derivation + fails (e.g., zero iterations). + + Supported on OpenSSL 1.1.x, OpenSSL 3.0.x, and LibreSSL. + + ```pony + let key = Pbkdf2Sha256("password", "salt", 4096, 32)? + ``` + """ + fun tag apply(password: ByteSeq, salt: ByteSeq, iterations: U32, + key_length: USize): Array[U8] val ? + => + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + recover + let arr = Array[U8].init(0, key_length) + let rc = @PKCS5_PBKDF2_HMAC( + password.cpointer(), password.size().i32(), + salt.cpointer(), salt.size().i32(), + iterations.i32(), + @EVP_sha256(), + key_length.i32(), + arr.cpointer()) + if rc != 1 then error end + arr + end + else + compile_error "You must select an SSL version to use." + end diff --git a/corral/_vendor/ssl/crypto/rand_bytes.pony b/corral/_vendor/ssl/crypto/rand_bytes.pony new file mode 100644 index 0000000..04277d5 --- /dev/null +++ b/corral/_vendor/ssl/crypto/rand_bytes.pony @@ -0,0 +1,25 @@ +use "path:/usr/local/opt/libressl/lib" if osx and x86 +use "path:/opt/homebrew/opt/libressl/lib" if osx and arm +use "lib:crypto" + +use @RAND_bytes[I32](buf: Pointer[U8] tag, num: I32) + +primitive RandBytes + """ + Generate cryptographically secure random bytes using OpenSSL's CSPRNG. + + Returns an array of the requested number of random bytes, or raises an + error if the CSPRNG cannot generate secure output (e.g., insufficient + entropy during early system startup). + + ```pony + let nonce = RandBytes(24)? + ``` + """ + fun tag apply(size: USize): Array[U8] val ? => + recover + let arr = Array[U8].init(0, size) + let rc = @RAND_bytes(arr.cpointer(), size.i32()) + if rc != 1 then error end + arr + end diff --git a/corral/_vendor/ssl/net/_ssl_init.pony b/corral/_vendor/ssl/net/_ssl_init.pony new file mode 100755 index 0000000..1684caf --- /dev/null +++ b/corral/_vendor/ssl/net/_ssl_init.pony @@ -0,0 +1,33 @@ +use "path:/usr/local/opt/libressl/lib" if osx and x86 +use "path:/opt/homebrew/opt/libressl/lib" if osx and arm +use "lib:ssl" +use "lib:crypto" + +use @OPENSSL_init_ssl[I32](opts: U64, settings: Pointer[_OpenSslInitSettings]) +use @OPENSSL_INIT_new[Pointer[_OpenSslInitSettings]]() +use @OPENSSL_INIT_free[None](settings: Pointer[_OpenSslInitSettings]) + +primitive _OpenSslInitSettings + +// From https://github.com/ponylang/ponyc/issues/330 +primitive _OpenSslInitNoLoadSslStrings fun val apply(): U64 => 0x00100000 +primitive _OpenSslInitLoadSslStrings fun val apply(): U64 => 0x00200000 +primitive _OpenSslInitNoLoadCryptoStrings fun val apply(): U64 => 0x00000001 +primitive _OpenSslInitLoadCryptoStrings fun val apply(): U64 => 0x00000002 + +primitive _SSLInit + """ + This initialises SSL when the program begins. + """ + fun _init() => + ifdef "openssl_1.1.x" or "openssl_3.0.x" then + let settings = @OPENSSL_INIT_new() + @OPENSSL_init_ssl( + _OpenSslInitLoadSslStrings() + _OpenSslInitLoadCryptoStrings(), + settings) + @OPENSSL_INIT_free(settings) + elseif "libressl" then + @OPENSSL_init_ssl(0, Pointer[_OpenSslInitSettings]) + else + compile_error "You must select an SSL version to use." + end diff --git a/corral/_vendor/ssl/net/_ssl_versions.pony b/corral/_vendor/ssl/net/_ssl_versions.pony new file mode 100755 index 0000000..69b6bc4 --- /dev/null +++ b/corral/_vendor/ssl/net/_ssl_versions.pony @@ -0,0 +1,4 @@ +primitive _SslCtrlSetMinProtoVersion fun val apply(): I32 => 123 +primitive _SslCtrlSetMaxProtoVersion fun val apply(): I32 => 124 +primitive _SslCtrlGetMinProtoVersion fun val apply(): I32 => 130 +primitive _SslCtrlGetMaxProtoVersion fun val apply(): I32 => 131 diff --git a/corral/_vendor/ssl/net/alpn.pony b/corral/_vendor/ssl/net/alpn.pony new file mode 100755 index 0000000..6836b72 --- /dev/null +++ b/corral/_vendor/ssl/net/alpn.pony @@ -0,0 +1,114 @@ +use "net" + +interface ALPNProtocolNotify + fun ref alpn_negotiated(conn: TCPConnection, protocol: (String | None)): None + +type ALPNProtocolName is String val +primitive ALPNFatal +primitive ALPNNoAck +primitive ALPNWarning + +type ALPNMatchResult is (ALPNProtocolName | ALPNNoAck | ALPNWarning | ALPNFatal) +type _ALPNSelectCallback is @{( + Pointer[_SSL] tag, + Pointer[Pointer[U8] tag] tag, + Pointer[U8] tag, + Pointer[U8] box, + U32, + ALPNProtocolResolver box) + : I32} + +interface box ALPNProtocolResolver + """ + Controls the protocol name to be chosen for incomming SSLConnections using the ALPN extension. + """ + fun box resolve(advertised: Array[ALPNProtocolName] val): ALPNMatchResult + +class val ALPNStandardProtocolResolver is ALPNProtocolResolver + """ + Implements the standard protocol selection akin to the OpenSSL function `SSL_select_next_proto`. + """ + let supported: Array[ALPNProtocolName] val + let use_client_as_fallback: Bool + + new val create( + supported': Array[ALPNProtocolName] val, + use_client_as_fallback': Bool = true) + => + supported = supported' + use_client_as_fallback = use_client_as_fallback' + + fun box resolve(advertised: Array[ALPNProtocolName] val): ALPNMatchResult => + for sup_proto in supported.values() do + for adv_proto in advertised.values() do + if sup_proto == adv_proto then return sup_proto end + end + end + if use_client_as_fallback then + try return advertised(0)? end + end + + ALPNWarning + +primitive _ALPNMatchResultCode + fun ok(): I32 => 0 + fun warning(): I32 => 1 + fun fatal(): I32 => 2 + fun no_ack(): I32 => 3 + +primitive _ALPNProtocolList + fun from_array(protocols: Array[String] box): String ? => + """ + Try to pack the protocol names in `protocols` into a *protocol name list* + """ + if protocols.size() == 0 then + error + end + + let list = recover trn String end + + for proto in protocols.values() do + let len = proto.size() + if (len == 0) or (len > 255) then error end + + list.push(U8.from[USize](len)) + list.append(proto) + end + + list + + fun to_array(protocol_list: String box): Array[ALPNProtocolName] val ? => + """ + Try to unpack a *protocol name list* into an `Array[String]` + """ + let arr = recover trn Array[ALPNProtocolName] end + + var index = USize(1) + var remain = try protocol_list(0)? else error end + var buf = recover trn String end + + if remain == 0 then error end + + while index < protocol_list.size() do + let ch = try protocol_list(index)? else error end + if remain > 0 then + buf.push(ch) + remain = remain - 1 + end + + if remain == 0 then + let final_protocol: String = buf = recover String end + arr.push(final_protocol) + + let hasNextChar = index < (protocol_list.size() - 1) + if hasNextChar then + remain = try protocol_list(index + 1)? else error end + if remain == 0 then error end + index = index + 1 + end + end + index = index + 1 + end + + if remain > 0 then error end + arr diff --git a/corral/_vendor/ssl/net/net.pony b/corral/_vendor/ssl/net/net.pony new file mode 100644 index 0000000..b3f22a5 --- /dev/null +++ b/corral/_vendor/ssl/net/net.pony @@ -0,0 +1,3 @@ +""" +The net package contains SSL networking supporting for the Pony standard library `net` package. +""" diff --git a/corral/_vendor/ssl/net/ssl.pony b/corral/_vendor/ssl/net/ssl.pony new file mode 100755 index 0000000..7eba369 --- /dev/null +++ b/corral/_vendor/ssl/net/ssl.pony @@ -0,0 +1,284 @@ +use "net" + +use @SSL_ctrl[ILong]( + ssl: Pointer[_SSL], + op: I32, + arg: ILong, + parg: Pointer[None]) +use @SSL_new[Pointer[_SSL]](ctx: Pointer[_SSLContext] tag) +use @SSL_free[None](ssl: Pointer[_SSL] tag) +use @SSL_set_verify[None](ssl: Pointer[_SSL], mode: I32, cb: Pointer[U8]) +use @BIO_s_mem[Pointer[U8]]() +use @BIO_new[Pointer[_BIO]](typ: Pointer[U8]) +use @SSL_set_bio[None](ssl: Pointer[_SSL], rbio: Pointer[_BIO] tag, wbio: Pointer[_BIO] tag) +use @SSL_set_accept_state[None](ssl: Pointer[_SSL]) +use @SSL_set_connect_state[None](ssl: Pointer[_SSL]) +use @SSL_do_handshake[I32](ssl: Pointer[_SSL]) +use @SSL_get0_alpn_selected[None](ssl: Pointer[_SSL] tag, data: Pointer[Pointer[U8] iso], + len: Pointer[U32]) if "openssl_1.1.x" or "openssl_3.0.x" or "libressl" +use @SSL_pending[I32](ssl: Pointer[_SSL]) +use @SSL_read[I32](ssl: Pointer[_SSL], buf: Pointer[U8] tag, len: U32) +use @SSL_write[I32](ssl: Pointer[_SSL], buf: Pointer[U8] tag, len: U32) +use @BIO_read[I32](bio: Pointer[_BIO] tag, buf: Pointer[U8] tag, len: U32) +use @BIO_write[I32](bio: Pointer[_BIO] tag, buf: Pointer[U8] tag, len: U32) +use @SSL_get_error[I32](ssl: Pointer[_SSL], ret: I32) +use @BIO_ctrl_pending[USize](bio: Pointer[_BIO] tag) +use @SSL_has_pending[I32](ssl: Pointer[_SSL]) if "openssl_1.1.x" or "openssl_3.0.x" +use @SSL_get_peer_certificate[Pointer[X509]](ssl: Pointer[_SSL]) if "openssl_1.1.x" or "libressl" +use @SSL_get1_peer_certificate[Pointer[X509]](ssl: Pointer[_SSL]) if "openssl_3.0.x" + +primitive _SSL +primitive _BIO + +primitive SSLHandshake +primitive SSLAuthFail +primitive SSLReady +primitive SSLError + +type SSLState is (SSLHandshake | SSLAuthFail | SSLReady | SSLError) + +class SSL + """ + An SSL session manages handshakes, encryption and decryption. It is not tied + to any transport layer. + """ + let _hostname: String + let _verify: Bool + var _ssl: Pointer[_SSL] + var _input: Pointer[_BIO] tag + var _output: Pointer[_BIO] tag + var _state: SSLState = SSLHandshake + var _read_buf: Array[U8] iso = [] + + new _create( + ctx: Pointer[_SSLContext] tag, + server: Bool, + verify: Bool, + hostname: String = "") + ? + => + """ + Create a client or server SSL session from a context. + """ + if ctx.is_null() then error end + _hostname = hostname + _verify = verify + + _ssl = @SSL_new(ctx) + if _ssl.is_null() then error end + + let mode = if verify then I32(3) else I32(0) end + @SSL_set_verify(_ssl, mode, Pointer[U8]) + + _input = @BIO_new(@BIO_s_mem()) + if _input.is_null() then error end + + _output = @BIO_new(@BIO_s_mem()) + if _output.is_null() then error end + + @SSL_set_bio(_ssl, _input, _output) + + if + (_hostname.size() > 0) + and not DNS.is_ip4(_hostname) + and not DNS.is_ip6(_hostname) + then + // SSL_set_tlsext_host_name + @SSL_ctrl(_ssl, 55, 0, _hostname.cstring()) + end + + if server then + @SSL_set_accept_state(_ssl) + else + @SSL_set_connect_state(_ssl) + @SSL_do_handshake(_ssl) + end + + fun box alpn_selected(): (ALPNProtocolName | None) => + """ + Get the protocol identifier negotiated via ALPN + """ + var ptr: Pointer[U8] iso = recover Pointer[U8] end + var len = U32(0) + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + @SSL_get0_alpn_selected(_ssl, addressof ptr, addressof len) + end + + if ptr.is_null() then None + else + recover val String.copy_cpointer(consume ptr, USize.from[U32](len)) end + end + + fun state(): SSLState => + """ + Returns the SSL session state. + """ + _state + + fun ref read(expect: USize = 0): (Array[U8] iso^ | None) => + """ + Returns unencrypted bytes to be passed to the application. If `expect` is + non-zero, the number of bytes returned will be exactly `expect`. If no data + (or less than `expect` bytes) is available, this returns None. + """ + let offset = _read_buf.size() + + var len = if expect > 0 then + if offset >= expect then + return _read_buf = [] + end + + expect - offset + else + 1024 + end + + let max = if expect > 0 then expect - offset else USize.max_value() end + let pending = @SSL_pending(_ssl).usize() + + if pending > 0 then + if expect > 0 then + len = len.min(pending) + else + len = pending + end + + _read_buf.undefined(offset + len) + @SSL_read(_ssl, _read_buf.cpointer(offset), len.u32()) + else + _read_buf.undefined(offset + len) + let r = + @SSL_read(_ssl, _read_buf.cpointer(offset), len.u32()) + + if r <= 0 then + match @SSL_get_error(_ssl, r) + | 1 | 5 | 6 => + _state = SSLError + return None + | 2 => + // SSL buffer has more data but it is not yet decoded (or something) + _read_buf.truncate(offset) + return None + end + + _read_buf.truncate(offset) + else + _read_buf.truncate(offset + r.usize()) + end + end + + let ready = if expect == 0 then + _read_buf.size() > 0 + else + _read_buf.size() == expect + end + + if ready then + _read_buf = [] + else + // try and read again any pending data that SSL hasn't decoded yet + if @BIO_ctrl_pending(_input) > 0 then + read(expect) + else + ifdef "openssl_1.1.x" or "openssl_3.0.x" then + // try and read again any data already decoded from SSL that hasn't + // been read via `SSL_has_pending` that was added in 1.1 + // This mailing list post has a good description of what it is for: + // https://mta.openssl.org/pipermail/openssl-users/2017-January/005110.html + if @SSL_has_pending(_ssl) == 1 then + read(expect) + end + end + end + end + + fun ref write(data: ByteSeq) ? => + """ + When application data is sent, add it to the SSL session. Raises an error + if the handshake is not complete. + """ + if _state isnt SSLReady then error end + + if data.size() > 0 then + @SSL_write(_ssl, data.cpointer(), data.size().u32()) + end + + fun ref receive(data: ByteSeq) => + """ + When data is received, add it to the SSL session. + """ + @BIO_write(_input, data.cpointer(), data.size().u32()) + + if _state is SSLHandshake then + let r = @SSL_do_handshake(_ssl) + + if r > 0 then + _verify_hostname() + else + match @SSL_get_error(_ssl, r) + | 1 => _state = SSLAuthFail + | 5 | 6 => _state = SSLError + end + end + end + + fun ref can_send(): Bool => + """ + Returns true if there are encrypted bytes to be passed to the destination. + """ + @BIO_ctrl_pending(_output) > 0 + + fun ref send(): Array[U8] iso^ ? => + """ + Returns encrypted bytes to be passed to the destination. Raises an error + if no data is available. + """ + let len = @BIO_ctrl_pending(_output) + if len == 0 then error end + + let buf = recover Array[U8] .> undefined(len) end + @BIO_read(_output, buf.cpointer(), buf.size().u32()) + buf + + fun ref dispose() => + """ + Dispose of the session. + """ + if not _ssl.is_null() then + @SSL_free(_ssl) + _ssl = Pointer[_SSL] + end + + fun _final() => + """ + Dispose of the session. + """ + if not _ssl.is_null() then + @SSL_free(_ssl) + end + + fun ref _verify_hostname() => + """ + Verify that the certificate is valid for the given hostname. + """ + if _verify and (_hostname.size() > 0) then + let cert = ifdef "openssl_3.0.x" then + @SSL_get1_peer_certificate(_ssl) + elseif "openssl_1.1.x" or "libressl" then + @SSL_get_peer_certificate(_ssl) + else + compile_error "You must select an SSL version to use." + end + let ok = X509.valid_for_host(cert, _hostname) + + if not cert.is_null() then + @X509_free(cert) + end + + if not ok then + _state = SSLAuthFail + return + end + end + + _state = SSLReady diff --git a/corral/_vendor/ssl/net/ssl_connection.pony b/corral/_vendor/ssl/net/ssl_connection.pony new file mode 100755 index 0000000..07d9a59 --- /dev/null +++ b/corral/_vendor/ssl/net/ssl_connection.pony @@ -0,0 +1,203 @@ +use "collections" +use "net" + +class SSLConnection is TCPConnectionNotify + """ + Wrap another protocol in an SSL connection. + """ + let _notify: TCPConnectionNotify + let _ssl: SSL + var _connected: Bool = false + var _expect: USize = 0 + var _closed: Bool = false + let _pending: List[ByteSeq] = _pending.create() + var _accept_pending: Bool = false + + new iso create(notify: TCPConnectionNotify iso, ssl: SSL iso) => + """ + Initialise with a wrapped protocol and an SSL session. + """ + _notify = consume notify + _ssl = consume ssl + + fun ref accepted(conn: TCPConnection ref) => + """ + Swallow this event until the handshake is complete. + """ + _accept_pending = true + _poll(conn) + + fun ref connecting(conn: TCPConnection ref, count: U32) => + """ + Forward to the wrapped protocol. + """ + _notify.connecting(conn, count) + + fun ref connected(conn: TCPConnection ref) => + """ + Swallow this event until the handshake is complete. + """ + _poll(conn) + + fun ref connect_failed(conn: TCPConnection ref) => + """ + Forward to the wrapped protocol. + """ + _notify.connect_failed(conn) + + fun ref sent(conn: TCPConnection ref, data: ByteSeq): ByteSeq => + """ + Pass the data to the SSL session and check for both new application data + and new destination data. + """ + let notified = _notify.sent(conn, data) + if _connected then + try + _ssl.write(notified)? + else + return "" + end + else + _pending.push(notified) + end + + _poll(conn) + "" + + fun ref sentv(conn: TCPConnection ref, data: ByteSeqIter): ByteSeqIter => + let ret = recover val Array[ByteSeq] end + let data' = _notify.sentv(conn, data) + for bytes in data'.values() do + if _connected then + try + _ssl.write(bytes)? + else + return ret + end + else + _pending.push(bytes) + end + end + + _poll(conn) + ret + + fun ref received( + conn: TCPConnection ref, + data: Array[U8] iso, + times: USize) + : Bool + => + """ + Pass the data to the SSL session and check for both new application data + and new destination data. + """ + _ssl.receive(consume data) + _poll(conn) + + fun ref expect(conn: TCPConnection ref, qty: USize): USize => + """ + Keep track of the expect count for the wrapped protocol. Always tell the + TCPConnection to read all available data. + """ + _expect = _notify.expect(conn, qty) + 0 + + fun ref closed(conn: TCPConnection ref) => + """ + Forward to the wrapped protocol. + """ + _closed = true + + _poll(conn) + _ssl.dispose() + + _connected = false + _pending.clear() + _notify.closed(conn) + + fun ref throttled(conn: TCPConnection ref) => + """ + Forward to the wrapped protocol. + """ + _notify.throttled(conn) + + fun ref unthrottled(conn: TCPConnection ref) => + """ + Forward to the wrapped protocol. + """ + _notify.unthrottled(conn) + + fun ref _poll(conn: TCPConnection ref): Bool => + """ + Checks for both new application data and new destination data. Informs the + wrapped protocol that is has connected when the handshake is complete. + """ + match _ssl.state() + | SSLReady => + if not _connected then + _connected = true + if _accept_pending then + _notify.accepted(conn) + else + _notify.connected(conn) + end + + match _notify + | let alpn_notify: ALPNProtocolNotify => + alpn_notify.alpn_negotiated(conn, _ssl.alpn_selected()) + end + + try + while _pending.size() > 0 do + _ssl.write(_pending.shift()?)? + end + end + end + | SSLAuthFail => + _notify.auth_failed(conn) + + if not _closed then + conn.close() + end + + return true + | SSLError => + if not _closed then + conn.close() + end + + return true + end + + var continue_reading: Bool = true + + try + var received_called: USize = 0 + + while true do + let r = _ssl.read(_expect) + + if r isnt None then + received_called = received_called + 1 + if not _notify.received( + conn, + (consume r) as Array[U8] iso^, + received_called) + then + continue_reading = false + break + end + else + break + end + end + end + + try + while _ssl.can_send() do + conn.write_final(_ssl.send()?) + end + end + + continue_reading diff --git a/corral/_vendor/ssl/net/ssl_context.pony b/corral/_vendor/ssl/net/ssl_context.pony new file mode 100755 index 0000000..f95f7f9 --- /dev/null +++ b/corral/_vendor/ssl/net/ssl_context.pony @@ -0,0 +1,420 @@ +use "files" + +use "lib:crypt32" if windows +use "lib:cryptui" if windows +use "lib:bcrypt" if windows + +use @memcpy[Pointer[U8]](dst: Pointer[None], src: Pointer[None], n: USize) +use @SSL_CTX_ctrl[ILong]( + ctx: Pointer[_SSLContext] tag, + op: I32, + arg: ULong, + parg: Pointer[None]) +use @TLS_method[Pointer[None]]() if "openssl_1.1.x" or "openssl_3.0.x" or "libressl" +use @SSL_CTX_new[Pointer[_SSLContext]](method: Pointer[None]) +use @SSL_CTX_free[None](ctx: Pointer[_SSLContext] tag) +use @SSL_CTX_clear_options[ULong](ctx: Pointer[_SSLContext] tag, opts: ULong) if "openssl_1.1.x" or "openssl_3.0.x" +use @SSL_CTX_set_options[ULong](ctx: Pointer[_SSLContext] tag, opts: ULong) if "openssl_1.1.x" or "openssl_3.0.x" +use @SSL_CTX_use_certificate_chain_file[I32](ctx: Pointer[_SSLContext] tag, file: Pointer[U8] tag) +use @SSL_CTX_use_PrivateKey_file[I32](ctx: Pointer[_SSLContext] tag, file: Pointer[U8] tag, typ: I32) +use @SSL_CTX_check_private_key[I32](ctx: Pointer[_SSLContext] tag) +use @SSL_CTX_load_verify_locations[I32](ctx: Pointer[_SSLContext] tag, ca_file: Pointer[U8] tag, + ca_path: Pointer[U8] tag) +use @X509_STORE_new[Pointer[U8] tag]() +use @CertOpenSystemStoreA[Pointer[U8] tag](prov: Pointer[U8] tag, protcol: Pointer[U8] tag) + if windows +use @CertEnumCertificatesInStore[NullablePointer[_CertContext]](cert_store: Pointer[U8] tag, + prev_ctx: NullablePointer[_CertContext]) if windows +use @d2i_X509[Pointer[X509] tag](val_out: Pointer[U8] tag, der_in: Pointer[Pointer[U8]], + length: U32) +use @X509_STORE_add_cert[U32](store: Pointer[U8] tag, x509: Pointer[X509] tag) +use @X509_free[None](x509: Pointer[X509] tag) +use @SSL_CTX_set_cert_store[None](ctx: Pointer[_SSLContext] tag, store: Pointer[U8] tag) +use @X509_STORE_free[None](store: Pointer[U8] tag) +use @CertCloseStore[Bool](store: Pointer[U8] tag, flags: U32) if windows +use @SSL_CTX_set_cipher_list[I32](ctx: Pointer[_SSLContext] tag, control: Pointer[U8] tag) +use @SSL_CTX_set_verify_depth[None](ctx: Pointer[_SSLContext] tag, depth: U32) +use @SSL_CTX_set_alpn_select_cb[None](ctx: Pointer[_SSLContext] tag, cb: _ALPNSelectCallback, + resolver: ALPNProtocolResolver) if "openssl_1.1.x" or "openssl_3.0.x" or "libressl" +use @SSL_CTX_set_alpn_protos[I32](ctx: Pointer[_SSLContext] tag, protos: Pointer[U8] tag, + protos_len: USize) if "openssl_1.1.x" or "openssl_3.0.x" or "libressl" + +primitive _SSLContext + +primitive _SslCtrlSetOptions fun val apply(): I32 => 32 +primitive _SslCtrlClearOptions fun val apply(): I32 => 77 + +// These are the SSL_OP_NO_{SSL|TLS}vx{_x} in ssl.h. +// Since Pony doesn't allow underscore we use camel case +// and began them with underscore to keep them private. +// Also, in the version strings the "v" becomes "V" and +// the underscore "_" becomes "u". So SSL_OP_NO_TLSv1_2 +// _SslOpNo_TlsV1u2. +primitive _SslOpNoTlsV1 fun val apply(): ULong => 0x04000000 +primitive _SslOpNoTlsV1u2 fun val apply(): ULong => 0x08000000 +primitive _SslOpNoTlsV1u1 fun val apply(): ULong => 0x10000000 +primitive _SslOpNoTlsV1u3 fun val apply(): ULong => 0x20000000 + + +class val SSLContext + """ + An SSL context is used to create SSL sessions. + """ + var _ctx: Pointer[_SSLContext] tag + var _client_verify: Bool = true + var _server_verify: Bool = false + + new create() => + """ + Create an SSL context. + """ + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + _ctx = @SSL_CTX_new(@TLS_method()) + + // Allow only newer ciphers. + try + set_min_proto_version(Tls1u2Version())? + set_max_proto_version(SslAutoVersion())? + end + else + compile_error "You must select an SSL version to use." + end + + fun _set_options(opts: ULong) => + ifdef "openssl_1.1.x" or "openssl_3.0.x" then + @SSL_CTX_set_options(_ctx, opts) + elseif "libressl" then + @SSL_CTX_ctrl(_ctx, _SslCtrlSetOptions(), opts, Pointer[None]) + else + compile_error "You must select an SSL version to use." + end + + fun _clear_options(opts: ULong) => + ifdef "openssl_1.1.x" or "openssl_3.0.x" then + @SSL_CTX_clear_options(_ctx, opts) + elseif "libressl" then + @SSL_CTX_ctrl(_ctx, _SslCtrlClearOptions(), opts, Pointer[None]) + else + compile_error "You must select an SSL version to use." + end + + fun client(hostname: String = ""): SSL iso^ ? => + """ + Create a client-side SSL session. If a hostname is supplied, the server + side certificate must be valid for that hostname. + """ + let ctx = _ctx + let verify = _client_verify + recover SSL._create(ctx, false, verify, hostname)? end + + fun server(): SSL iso^ ? => + """ + Create a server-side SSL session. + """ + let ctx = _ctx + let verify = _server_verify + recover SSL._create(ctx, true, verify)? end + + fun ref set_cert(cert: FilePath, key: FilePath) ? => + """ + The cert file is a PEM certificate chain. The key file is a private key. + Servers must set this. For clients, it is optional. + """ + if + _ctx.is_null() + or (cert.path.size() == 0) + or (key.path.size() == 0) + or (0 == @SSL_CTX_use_certificate_chain_file( + _ctx, cert.path.cstring())) + or (0 == @SSL_CTX_use_PrivateKey_file( + _ctx, key.path.cstring(), I32(1))) + or (0 == @SSL_CTX_check_private_key(_ctx)) + then + error + end + + fun ref set_authority( + file: (FilePath | None), + path: (FilePath | None) = None) + ? + => + """ + Use a PEM file and/or a directory of PEM files to specify certificate + authorities. Clients must set this. For servers, it is optional. Use None + to indicate no file or no path. Raises an error if these verify locations + aren't valid. + + If both `file` and `path` are `None`, on Windows this method loads the + system root certificates. On Posix it raises an error. + """ + if (file is None) and (path is None) then + ifdef windows then + _load_windows_root_certs()? + else + error + end + else + let fs = try (file as FilePath).path else "" end + let ps = try (path as FilePath).path else "" end + + let f = if fs.size() > 0 then fs.cstring() else Pointer[U8] end + let p = if ps.size() > 0 then ps.cstring() else Pointer[U8] end + + if + _ctx.is_null() + or (f.is_null() and p.is_null()) + or (0 == @SSL_CTX_load_verify_locations(_ctx, f, p)) + then + error + end + end + + fun ref _load_windows_root_certs() ? => + ifdef windows then + let root_str = "ROOT" + let hStore = @CertOpenSystemStoreA(Pointer[U8], root_str.cstring()) + if hStore.is_null() then error end + + let x509_store = @X509_STORE_new() + if x509_store.is_null() then error end + + try + var pContext: NullablePointer[_CertContext] + pContext = + @CertEnumCertificatesInStore(hStore, NullablePointer[_CertContext].none()) + + while not pContext.is_none() do + let cert_context = pContext()? + let x509 = @d2i_X509(Pointer[U8], addressof cert_context.pbCertEncoded, + cert_context.cbCertEncoded) + if not x509.is_null() then + let result = @X509_STORE_add_cert(x509_store, x509) + @X509_free(x509) + if result != 1 then error end + end + + pContext = @CertEnumCertificatesInStore(hStore, pContext) + end + + @SSL_CTX_set_cert_store(_ctx, x509_store) + else + @X509_STORE_free(x509_store) + then + @CertCloseStore(hStore, U32(0)) + end + end + + fun ref set_ciphers(ciphers: String) ? => + """ + Set the accepted ciphers. This replaces the existing list. Raises an error + if the cipher list is invalid. + """ + if + _ctx.is_null() + or (0 == @SSL_CTX_set_cipher_list(_ctx, ciphers.cstring())) + then + error + end + + fun ref set_client_verify(state: Bool) => + """ + Set to true to require verification. Defaults to true. + """ + _client_verify = state + + fun ref set_server_verify(state: Bool) => + """ + Set to true to require verification. Defaults to false. + """ + _server_verify = state + + fun ref set_verify_depth(depth: U32) => + """ + Set the verify depth. Defaults to 6. + """ + if not _ctx.is_null() then + @SSL_CTX_set_verify_depth(_ctx, depth) + end + + fun ref set_min_proto_version(version: ULong) ? => + """ + Set minimum protocol version. Set to SslAutoVersion, 0, + to automatically manage lowest version. + + Supported versions: Ssl3Version, Tls1Version, Tls1u1Version, + Tls1u2Version, Tls1u3Version, Dtls1Version, + Dtls1u2Version + """ + let result = + @SSL_CTX_ctrl(_ctx, _SslCtrlSetMinProtoVersion(), version, Pointer[None]) + if result == 0 then + error + end + + fun ref get_min_proto_version(): ILong => + """ + Get minimum protocol version. Returns SslAutoVersion, 0, + when automatically managing lowest version. + + Supported versions: Ssl3Version, Tls1Version, Tls1u1Version, + Tls1u2Version, Tls1u3Version, Dtls1Version, + Dtls1u2Version + """ + @SSL_CTX_ctrl(_ctx, _SslCtrlGetMinProtoVersion(), 0, Pointer[None]) + + fun ref set_max_proto_version(version: ULong) ? => + """ + Set maximum protocol version. Set to SslAutoVersion, 0, + to automatically manage higest version. + + Supported versions: Ssl3Version, Tls1Version, Tls1u1Version, + Tls1u2Version, Tls1u3Version, Dtls1Version, + Dtls1u2Version + """ + let result = + @SSL_CTX_ctrl(_ctx, _SslCtrlSetMaxProtoVersion(), version, Pointer[None]) + if result == 0 then + error + end + + fun ref get_max_proto_version(): ILong => + """ + Get maximum protocol version. Returns SslAutoVersion, 0, + when automatically managing highest version. + + Supported versions: Ssl3Version, Tls1Version, Tls1u1Version, + Tls1u2Version, Tls1u3Version, Dtls1Version, + Dtls1u2Version + """ + @SSL_CTX_ctrl(_ctx, _SslCtrlGetMaxProtoVersion(), 0, Pointer[None]) + + fun ref alpn_set_resolver(resolver: ALPNProtocolResolver box): Bool => + """ + Use `resolver` to choose the protocol to be selected for incomming connections. + + Returns true on success. + Supported on OpenSSL 1.1.x, OpenSSL 3.0.x, and LibreSSL. + """ + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + @SSL_CTX_set_alpn_select_cb( + _ctx, addressof SSLContext._alpn_select_cb, resolver) + return true + else + compile_error "You must select an SSL version to use." + end + + fun ref alpn_set_client_protocols(protocols: Array[String] box): Bool => + """ + Configures the SSLContext to advertise the protocol names defined in `protocols` when connecting to a server + protocol names must have a size of 1 to 255 + + Returns true on success. + Supported on OpenSSL 1.1.x, OpenSSL 3.0.x, and LibreSSL. + """ + ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then + try + let proto_list = _ALPNProtocolList.from_array(protocols)? + let result = + @SSL_CTX_set_alpn_protos( + _ctx, proto_list.cpointer(), proto_list.size()) + return result == 0 + end + else + compile_error "You must select an SSL version to use." + end + + false + + fun @_alpn_select_cb( + ssl: Pointer[_SSL] tag, + out: Pointer[Pointer[U8] tag] tag, + outlen: Pointer[U8] tag, + inptr: Pointer[U8] box, + inlen: U32, + resolver: ALPNProtocolResolver box) + : I32 + => + let proto_arr_str = String.copy_cpointer(inptr, USize.from[U32](inlen)) + try + let proto_arr = _ALPNProtocolList.to_array(proto_arr_str)? + + match \exhaustive\ resolver.resolve(proto_arr) + | let matched: String => + var size = matched.size() + if (size > 0) and (size <= 255) then + var ptr = matched.cpointer() + @memcpy(out, addressof ptr, size.bitwidth() / 8) + @memcpy(outlen, addressof size, USize(1)) + _ALPNMatchResultCode.ok() + else + _ALPNMatchResultCode.fatal() + end + | ALPNNoAck => _ALPNMatchResultCode.no_ack() + | ALPNWarning => _ALPNMatchResultCode.warning() + | ALPNFatal => _ALPNMatchResultCode.fatal() + end + else + _ALPNMatchResultCode.fatal() + end + + fun ref allow_tls_v1(state: Bool) => + """ + Allow TLS v1. Defaults to false. + Deprecated: use set_min_proto_version and set_max_proto_version + """ + if not _ctx.is_null() then + if state then + _clear_options(_SslOpNoTlsV1()) + else + _set_options(_SslOpNoTlsV1()) + end + end + + fun ref allow_tls_v1_1(state: Bool) => + """ + Allow TLS v1.1. Defaults to false. + Deprecated: use set_min_proto_version and set_max_proto_version + """ + if not _ctx.is_null() then + if state then + _clear_options(_SslOpNoTlsV1u1()) + else + _set_options(_SslOpNoTlsV1u1()) + end + end + + fun ref allow_tls_v1_2(state: Bool) => + """ + Allow TLS v1.2. Defaults to true. + Deprecated: use set_min_proto_version and set_max_proto_version + """ + if not _ctx.is_null() then + if state then + _clear_options(_SslOpNoTlsV1u2()) + else + _set_options(_SslOpNoTlsV1u2()) + end + end + + fun ref dispose() => + """ + Free the SSL context. + """ + if not _ctx.is_null() then + @SSL_CTX_free(_ctx) + _ctx = Pointer[_SSLContext] + end + + fun _final() => + """ + Free the SSL context. + """ + if not _ctx.is_null() then + @SSL_CTX_free(_ctx) + end + + +struct _CertContext + var dwCertEncodingType: U32 = 0 + var pbCertEncoded: Pointer[U8] = Pointer[U8] + var cbCertEncoded: U32 = 0 diff --git a/corral/_vendor/ssl/net/ssl_versions.pony b/corral/_vendor/ssl/net/ssl_versions.pony new file mode 100755 index 0000000..e4a2e0b --- /dev/null +++ b/corral/_vendor/ssl/net/ssl_versions.pony @@ -0,0 +1,14 @@ +primitive SslAutoVersion fun val apply(): ULong => 0x0 + +primitive Ssl3Version fun val apply(): ULong => 0x300 +primitive Tls1Version fun val apply(): ULong => 0x301 +primitive Tls1u1Version fun val apply(): ULong => 0x302 +primitive Tls1u2Version fun val apply(): ULong => 0x303 +primitive Tls1u3Version fun val apply(): ULong => 0x304 +primitive Dtls1Version fun val apply(): ULong => 0xFEFF +primitive Dtls1u2Version fun val apply(): ULong => 0xFEFD + +primitive TlsMinVersion fun val apply(): ULong => Tls1Version() +primitive TlsMaxVersion fun val apply(): ULong => Tls1u3Version() +primitive DtlsMinVersion fun val apply(): ULong => Dtls1Version() +primitive DtlsMaxVersion fun val apply(): ULong => Dtls1u2Version() diff --git a/corral/_vendor/ssl/net/x509.pony b/corral/_vendor/ssl/net/x509.pony new file mode 100755 index 0000000..b5bc5ce --- /dev/null +++ b/corral/_vendor/ssl/net/x509.pony @@ -0,0 +1,195 @@ +use "collections" +use "net" + +use @pony_os_ip_string[Pointer[U8]](src: Pointer[U8], len: I32) +use @X509_get_subject_name[Pointer[_X509Name]](cert: Pointer[X509]) +use @X509_NAME_get_text_by_NID[I32](name: Pointer[_X509Name], nid: I32, + buf: Pointer[U8] tag, len: I32) +use @X509_get_ext_d2i[Pointer[_GeneralNameStack]](cert: Pointer[X509], + nid: I32, crit: Pointer[U8], idx: Pointer[U8]) +use @OPENSSL_sk_pop[Pointer[_GeneralName]](stack: Pointer[_GeneralNameStack]) + if "openssl_1.1.x" or "openssl_3.0.x" +use @sk_pop[Pointer[_GeneralName]](stack: Pointer[_GeneralNameStack]) + if "libressl" +use @GENERAL_NAME_get0_value[Pointer[U8] tag](name: Pointer[_GeneralName], + ptype: Pointer[I32]) +use @ASN1_STRING_type[I32](value: Pointer[U8] tag) +use @ASN1_STRING_get0_data[Pointer[U8]](value: Pointer[U8] tag) +use @ASN1_STRING_length[I32](value: Pointer[U8] tag) +use @GENERAL_NAME_free[None](name: Pointer[_GeneralName]) +use @OPENSSL_sk_free[None](stack: Pointer[_GeneralNameStack]) + if "openssl_1.1.x" or "openssl_3.0.x" +use @sk_free[None](stack: Pointer[_GeneralNameStack]) + if "libressl" + +primitive _X509Name +primitive _GeneralName +primitive _GeneralNameStack + +primitive X509 + fun valid_for_host(cert: Pointer[X509], host: String): Bool => + """ + Checks if an OpenSSL X509 certificate is valid for a given host. + """ + for name in all_names(cert).values() do + if _match_name(host, name) then + return true + end + end + false + + fun common_name(cert: Pointer[X509]): String ? => + """ + Get the common name for the certificate. Raises an error if the common name + contains any NULL bytes. + """ + if cert.is_null() then error end + + let subject = @X509_get_subject_name(cert) + let len = + @X509_NAME_get_text_by_NID(subject, I32(13), Pointer[U8], I32(0)) + + if len < 0 then error end + + let common = recover String(len.usize()) end + @X509_NAME_get_text_by_NID( + subject, I32(13), common.cstring(), len + 1) + common.recalc() + + if common.size() != len.usize() then error end + + common + + fun all_names(cert: Pointer[X509]): Array[String] val => + """ + Returns an array of all names for the certificate. Any names containing + NULL bytes are not included. This includes the common name and all subject + alternate names. + """ + let array = recover Array[String] end + + if cert.is_null() then + return array + end + + try + array.push(common_name(cert)?) + end + + let stack = + @X509_get_ext_d2i(cert, I32(85), Pointer[U8], Pointer[U8]) + + if stack.is_null() then + return array + end + + var name = + ifdef "openssl_1.1.x" or "openssl_3.0.x" then + @OPENSSL_sk_pop(stack) + elseif "libressl" then + @sk_pop(stack) + else + compile_error "You must select an SSL version to use." + end + + while not name.is_null() do + var ptype = I32(0) + let value = + @GENERAL_NAME_get0_value(name, addressof ptype) + + match ptype + | 2 => // GEN_DNS + // Check for V_ASN1_IA5STRING + if @ASN1_STRING_type(value) == 22 then + try + array.push( + recover + // Build a String from the ASN1 data. + let data = @ASN1_STRING_get0_data(value) + let len = @ASN1_STRING_length(value) + let s = String.copy_cstring(data) + + // If it contains NULL bytes, don't include it. + if s.size() != len.usize() then + error + end + + s + end) + end + end + | 7 => // GEN_IPADD + // Turn the IP address into a string. + array.push( + recover + // Build a String from the ASN1 data. + let data = @ASN1_STRING_get0_data(value) + let len = @ASN1_STRING_length(value) + String.from_cstring(@pony_os_ip_string(data, len)) + end) + end + + @GENERAL_NAME_free(name) + ifdef "openssl_1.1.x" or "openssl_3.0.x" then + name = @OPENSSL_sk_pop(stack) + elseif "libressl" then + name = @sk_pop(stack) + else + compile_error "You must select an SSL version to use." + end + end + + ifdef "openssl_1.1.x" or "openssl_3.0.x" then + @OPENSSL_sk_free(stack) + elseif "libressl" then + @sk_free(stack) + else + compile_error "You must select an SSL version to use." + end + array + + fun _match_name(host: String, name: String): Bool => + """ + Returns true if the name extracted from the certificate is valid for the + given host. + """ + if DNS.is_ip4(host) or DNS.is_ip6(host) then + // If the host is a literal IP address, it must match exactly. + return host == name + end + + if (name.size() > 0) and + (host.compare_sub(name, name.size(), 0, 0, true) is Equal) + then + // If the names are the same ignoring case, they match. + return true + end + + try + if name(0)? == '*' then + // The name has a wildcard. Must be followed by at least two + // non-empty domain levels. + if (name.size() < 3) or (name(1)? != '.') or (name(2)? == '.') then + return false + end + + try + // Find the second domain level and make sure it's followed by + // something other than a dot. + let offset = name.find(".", 3)? + + if name.at_offset(offset + 1)? == '.' then + return false + end + end + + // Get the host domain. + let domain = host.find(".")? + + // If the host domain is the wildcard domain ignoring case, they match. + return + host.compare_sub(name, name.size() - 1, domain, 1, true) is Equal + end + end + + false diff --git a/corral/_vendor/ssl/ssl.pony b/corral/_vendor/ssl/ssl.pony new file mode 100644 index 0000000..5849ed9 --- /dev/null +++ b/corral/_vendor/ssl/ssl.pony @@ -0,0 +1,5 @@ +""" +# SSL + +Pony bindings for OpenSSL and LibreSSL. +""" diff --git a/corral/git/_test.pony b/corral/git/_test.pony new file mode 100644 index 0000000..6cf4fcf --- /dev/null +++ b/corral/git/_test.pony @@ -0,0 +1,14 @@ +use "pony_test" +use sha1 = "sha1" +use inflate = "inflate" + +actor \nodoc\ Main is TestList + new create(env: Env) => + PonyTest(env, this) + + new make() => + None + + fun tag tests(test: PonyTest) => + sha1.Main.make().tests(test) + inflate.Main.make().tests(test) diff --git a/corral/git/git.pony b/corral/git/git.pony new file mode 100644 index 0000000..f40b172 --- /dev/null +++ b/corral/git/git.pony @@ -0,0 +1,10 @@ +""" +Pure Pony git internals for corral. This package tree provides the building +blocks for reading git repositories without shelling out to the git CLI. + +Sub-packages: +- `inflate` -- RFC 1951 DEFLATE decompression with RFC 1950 zlib framing +- `sha1` -- SHA-1 hashing (thin wrapper over ssl's Digest) + +Future phases will add `objects`, `pack`, `refs`, `checkout`, and `protocol`. +""" diff --git a/corral/git/inflate/_adler32.pony b/corral/git/inflate/_adler32.pony new file mode 100644 index 0000000..68a59d6 --- /dev/null +++ b/corral/git/inflate/_adler32.pony @@ -0,0 +1,42 @@ +primitive _Adler32 + """ + Computes the Adler-32 checksum per RFC 1950. Two running sums (s1, s2) + modulo 65521 (largest prime below 2^16). s1 = 1 + sum of bytes, + s2 = sum of s1 values. Result = (s2 << 16) | s1. + + Accumulates up to 5552 bytes before taking modulo -- this is zlib's NMAX, + the largest n where 255*n*(n+1)/2 + (n+1)*(BASE-1) fits in a U32. + """ + fun _from_ref(data: Array[U8] ref): U32 => + _compute(data) + + fun apply(data: Array[U8] val): U32 => + _compute(data) + + fun _compute(data: Array[U8] box): U32 => + let base: U32 = 65521 + let nmax: USize = 5552 + var s1: U32 = 1 + var s2: U32 = 0 + var offset: USize = 0 + let size = data.size() + + while offset < size do + let remaining = size - offset + let block_size = if remaining < nmax then remaining else nmax end + var i: USize = 0 + while i < block_size do + try + s1 = s1 + data(offset + i)?.u32() + else + _Unreachable() + end + s2 = s2 + s1 + i = i + 1 + end + s1 = s1 % base + s2 = s2 % base + offset = offset + block_size + end + + (s2 << 16) or s1 diff --git a/corral/git/inflate/_bit_reader.pony b/corral/git/inflate/_bit_reader.pony new file mode 100644 index 0000000..5806ffd --- /dev/null +++ b/corral/git/inflate/_bit_reader.pony @@ -0,0 +1,92 @@ +class ref _BitReader + """ + Reads bits from a byte array LSB-first (DEFLATE bit order). Tracks byte + position and bit offset within the current byte. + """ + let _data: Array[U8] val + var _byte_pos: USize + var _bit_pos: U8 // 0-7 within current byte + + new create(data: Array[U8] val, start_offset: USize = 0) => + _data = data + _byte_pos = start_offset + _bit_pos = 0 + + fun ref bits(n: U8): (U32 | InflateError) => + """ + Read n bits (0-25) and return as a U32, LSB-aligned. + """ + if n == 0 then return U32(0) end + var result: U32 = 0 + var bits_read: U8 = 0 + while bits_read < n do + if _byte_pos >= _data.size() then + return InflateIncompleteInput + end + let current_byte = try _data(_byte_pos)? else return InflateIncompleteInput end + let bits_available: U8 = 8 - _bit_pos + let bits_needed: U8 = n - bits_read + let take: U8 = if bits_available < bits_needed then bits_available else bits_needed end + + // Extract 'take' bits from current byte starting at _bit_pos. + // Use U16 for mask computation to avoid U8(1) << 8 clamping to 128. + let mask: U8 = (U16(1) << take.u16()).u8() - 1 + let extracted: U8 = (current_byte >> _bit_pos) and mask + result = result or (extracted.u32() << bits_read.u32()) + + bits_read = bits_read + take + _bit_pos = _bit_pos + take + if _bit_pos >= 8 then + _bit_pos = 0 + _byte_pos = _byte_pos + 1 + end + end + result + + fun ref _put_back(n: U8) => + """ + Put back n bits that were read but not consumed (used by Huffman decoder). + """ + let total_bits = (_byte_pos.u32() * 8) + _bit_pos.u32() + let new_total = total_bits - n.u32() + _byte_pos = (new_total / 8).usize() + _bit_pos = (new_total % 8).u8() + + fun ref align_to_byte() => + """ + Advance to the next byte boundary (for stored blocks). + """ + if _bit_pos > 0 then + _bit_pos = 0 + _byte_pos = _byte_pos + 1 + end + + fun bytes_remaining(): USize => + if _byte_pos >= _data.size() then + 0 + else + _data.size() - _byte_pos + end + + fun ref read_bytes(n: USize): (Array[U8] val | InflateError) => + """ + Read n bytes, only valid when byte-aligned. + """ + if (_byte_pos + n) > _data.size() then + return InflateIncompleteInput + end + let result = recover val + let arr = Array[U8](n) + var i: USize = 0 + while i < n do + try + arr.push(_data(_byte_pos + i)?) + else + _Unreachable() + end + i = i + 1 + end + arr + end + _byte_pos = _byte_pos + n + result diff --git a/corral/git/inflate/_deflate.pony b/corral/git/inflate/_deflate.pony new file mode 100644 index 0000000..68f5a17 --- /dev/null +++ b/corral/git/inflate/_deflate.pony @@ -0,0 +1,306 @@ +primitive _Deflate + """ + Core DEFLATE decompression engine (RFC 1951). Processes blocks sequentially + until BFINAL is set. + """ + fun apply(reader: _BitReader, output: Array[U8] ref): (None | InflateError) => + var bfinal: Bool = false + + while not bfinal do + // Read BFINAL (1 bit) + let bf = match reader.bits(1) + | let v: U32 => v + | let e: InflateError => return e + end + bfinal = bf == 1 + + // Read BTYPE (2 bits) + let btype = match reader.bits(2) + | let v: U32 => v + | let e: InflateError => return e + end + + match btype + | 0 => // Stored (no compression) + match _stored_block(reader, output) + | let e: InflateError => return e + end + | 1 => // Fixed Huffman + let lit_len_table = match _FixedHuffmanTables.lit_len() + | let t: _HuffmanTable => t + | let e: InflateError => return e + end + let dist_table = match _FixedHuffmanTables.dist() + | let t: _HuffmanTable => t + | let e: InflateError => return e + end + match _huffman_block(reader, output, lit_len_table, dist_table) + | let e: InflateError => return e + end + | 2 => // Dynamic Huffman + match _dynamic_block(reader, output) + | let e: InflateError => return e + end + else + return InflateInvalidBlockType + end + end + None + + fun _stored_block( + reader: _BitReader, + output: Array[U8] ref) + : (None | InflateError) + => + """ + Decompress a stored (uncompressed) block. + """ + reader.align_to_byte() + + // Read LEN (2 bytes, little-endian) + let len_lo = match reader.bits(8) + | let v: U32 => v + | let e: InflateError => return e + end + let len_hi = match reader.bits(8) + | let v: U32 => v + | let e: InflateError => return e + end + let len = (len_hi << 8) or len_lo + + // Read NLEN (2 bytes, little-endian) + let nlen_lo = match reader.bits(8) + | let v: U32 => v + | let e: InflateError => return e + end + let nlen_hi = match reader.bits(8) + | let v: U32 => v + | let e: InflateError => return e + end + let nlen = (nlen_hi << 8) or nlen_lo + + // Verify LEN == ~NLEN (lower 16 bits) + if len != (nlen xor 0xFFFF) then + return InflateInvalidStoredLength + end + + // Copy raw bytes + match reader.read_bytes(len.usize()) + | let bytes: Array[U8] val => + output.append(bytes) + None + | let e: InflateError => e + end + + fun _huffman_block( + reader: _BitReader, + output: Array[U8] ref, + lit_len_table: _HuffmanTable, + dist_table: _HuffmanTable) + : (None | InflateError) + => + """ + Decompress a Huffman-coded block (fixed or dynamic). + """ + let len_base = _Tables.length_base() + let len_extra = _Tables.length_extra() + let d_base = _Tables.dist_base() + let d_extra = _Tables.dist_extra() + + while true do + let symbol = match lit_len_table.decode(reader) + | let v: U16 => v + | let e: InflateError => return e + end + + if symbol < 256 then + // Literal byte + output.push(symbol.u8()) + elseif symbol == 256 then + // End of block + return None + elseif symbol <= 285 then + // Length code + let len_idx = (symbol - 257).usize() + let base_len = try len_base(len_idx)? else return InflateInvalidLitLen end + let extra = try len_extra(len_idx)? else return InflateInvalidLitLen end + let extra_val = if extra > 0 then + match reader.bits(extra) + | let v: U32 => v.u16() + | let e: InflateError => return e + end + else + U16(0) + end + let length = (base_len + extra_val).usize() + + // Decode distance + let dist_symbol = match dist_table.decode(reader) + | let v: U16 => v + | let e: InflateError => return e + end + + if dist_symbol.usize() >= d_base.size() then + return InflateInvalidDistance + end + + let base_dist = try d_base(dist_symbol.usize())? else return InflateInvalidDistance end + let d_extra_bits = try d_extra(dist_symbol.usize())? else return InflateInvalidDistance end + let d_extra_val = if d_extra_bits > 0 then + match reader.bits(d_extra_bits) + | let v: U32 => v.u16() + | let e: InflateError => return e + end + else + U16(0) + end + let distance = (base_dist + d_extra_val).usize() + + if distance > output.size() then + return InflateInvalidDistance + end + + // Copy bytes from back-reference. Must be byte-by-byte because + // distance can be less than length (overlapping copy). + let start = output.size() - distance + var i: USize = 0 + while i < length do + try + output.push(output(start + i)?) + else + _Unreachable() + end + i = i + 1 + end + else + return InflateInvalidLitLen + end + end + // Unreachable -- loop only exits via return + _Unreachable() + InflateInvalidLitLen + + fun _dynamic_block( + reader: _BitReader, + output: Array[U8] ref) + : (None | InflateError) + => + """ + Read dynamic Huffman code tables and decompress the block. + """ + // Read header fields + let hlit = match reader.bits(5) + | let v: U32 => v.usize() + 257 + | let e: InflateError => return e + end + let hdist = match reader.bits(5) + | let v: U32 => v.usize() + 1 + | let e: InflateError => return e + end + let hclen = match reader.bits(4) + | let v: U32 => v.usize() + 4 + | let e: InflateError => return e + end + + // Read code length code lengths in zigzag order + let cl_order = _Tables.code_length_order() + let cl_lengths = recover iso Array[U8].init(0, 19) end + var i: USize = 0 + while i < hclen do + let cl = match reader.bits(3) + | let v: U32 => v.u8() + | let e: InflateError => return e + end + let order_idx = try cl_order(i)? else return InflateInvalidCodeLengths end + try cl_lengths(order_idx.usize())? = cl end + i = i + 1 + end + + // Build code length Huffman table + let cl_table = match _BuildHuffmanTable(consume cl_lengths) + | let t: _HuffmanTable => t + | let e: InflateError => return e + end + + // Decode literal/length + distance code lengths + let total_codes = hlit + hdist + let code_lengths = recover iso Array[U8](total_codes) end + while code_lengths.size() < total_codes do + let sym = match cl_table.decode(reader) + | let v: U16 => v.usize() + | let e: InflateError => return e + end + + if sym < 16 then + code_lengths.push(sym.u8()) + elseif sym == 16 then + if code_lengths.size() == 0 then return InflateInvalidCodeLengths end + let repeat_count = match reader.bits(2) + | let v: U32 => v.usize() + 3 + | let e: InflateError => return e + end + let prev = try code_lengths(code_lengths.size() - 1)? else return InflateInvalidCodeLengths end + var j: USize = 0 + while j < repeat_count do + code_lengths.push(prev) + j = j + 1 + end + elseif sym == 17 then + let repeat_count = match reader.bits(3) + | let v: U32 => v.usize() + 3 + | let e: InflateError => return e + end + var j: USize = 0 + while j < repeat_count do + code_lengths.push(0) + j = j + 1 + end + elseif sym == 18 then + let repeat_count = match reader.bits(7) + | let v: U32 => v.usize() + 11 + | let e: InflateError => return e + end + var j: USize = 0 + while j < repeat_count do + code_lengths.push(0) + j = j + 1 + end + else + return InflateInvalidCodeLengths + end + end + + let all_lengths: Array[U8] val = consume code_lengths + + // Split into literal/length and distance code lengths + let lit_len_lengths = recover val + let arr = Array[U8](hlit) + var k: USize = 0 + while k < hlit do + try arr.push(all_lengths(k)?) else _Unreachable() end + k = k + 1 + end + arr + end + + let dist_lengths = recover val + let arr = Array[U8](hdist) + var k: USize = 0 + while k < hdist do + try arr.push(all_lengths(hlit + k)?) else _Unreachable() end + k = k + 1 + end + arr + end + + // Build tables + let lit_len_table = match _BuildHuffmanTable(lit_len_lengths) + | let t: _HuffmanTable => t + | let e: InflateError => return e + end + let dist_table = match _BuildHuffmanTable(dist_lengths) + | let t: _HuffmanTable => t + | let e: InflateError => return e + end + + _huffman_block(reader, output, lit_len_table, dist_table) diff --git a/corral/git/inflate/_huffman.pony b/corral/git/inflate/_huffman.pony new file mode 100644 index 0000000..333510b --- /dev/null +++ b/corral/git/inflate/_huffman.pony @@ -0,0 +1,147 @@ +class val _HuffmanTable + """ + Flat lookup table for Huffman decoding. Read max_bits from input, index into + the table, get symbol and actual code length, put back excess bits. + Each entry: (symbol: U16, code_length: U8). A zero code_length means the + entry is invalid. + """ + let _table: Array[(U16, U8)] val + let _max_bits: U8 + + new val _create(table': Array[(U16, U8)] val, max_bits': U8) => + _table = table' + _max_bits = max_bits' + + fun max_bits(): U8 => _max_bits + + fun decode(reader: _BitReader): (U16 | InflateError) => + """ + Decode one symbol from the bit stream. + """ + let peek_bits = match reader.bits(_max_bits) + | let v: U32 => v + | let e: InflateError => return e + end + + let index = peek_bits.usize() + if index >= _table.size() then + return InflateInvalidCodeLengths + end + + (let symbol, let code_len) = try _table(index)? else return InflateInvalidCodeLengths end + if code_len == 0 then + return InflateInvalidCodeLengths + end + + // Put back the excess bits we read + let excess = _max_bits - code_len + if excess > 0 then + reader._put_back(excess) + end + symbol + +primitive _BuildHuffmanTable + """ + Builds a flat lookup table from an array of code lengths. Returns a + _HuffmanTable or an error if the code lengths are invalid. + """ + fun apply(code_lengths: Array[U8] val): (_HuffmanTable | InflateError) => + // Find the maximum code length + var max_bits: U8 = 0 + for cl in code_lengths.values() do + if cl > max_bits then max_bits = cl end + end + + if max_bits == 0 then + // All zero-length codes -- empty table (valid for empty distance alphabet) + return _HuffmanTable._create(recover val Array[(U16, U8)] end, 0) + end + + if max_bits > 15 then + return InflateInvalidCodeLengths + end + + // Count the number of codes at each length + let bl_count = Array[U16].init(0, (max_bits.usize() + 1)) + for code_len in code_lengths.values() do + if code_len > 0 then + try bl_count(code_len.usize())? = bl_count(code_len.usize())? + 1 end + end + end + + // Compute the starting code for each length (RFC 1951 algorithm) + let next_code = Array[U32].init(0, (max_bits.usize() + 1)) + var code: U32 = 0 + var bits: U8 = 1 + while bits <= max_bits do + code = (code + (try bl_count((bits - 1).usize())? else 0 end).u32()) << 1 + try next_code(bits.usize())? = code end + bits = bits + 1 + end + + // Build the flat lookup table + let table_size = USize(1) << max_bits.usize() + let table = recover iso Array[(U16, U8)].init((0, 0), table_size) end + + var symbol: USize = 0 + while symbol < code_lengths.size() do + let sym_cl = try code_lengths(symbol)? else 0 end + if sym_cl > 0 then + let sym_code = try next_code(sym_cl.usize())? else 0 end + try next_code(sym_cl.usize())? = sym_code + 1 end + + // Reverse the bits for LSB-first lookup + let reversed = _reverse_bits(sym_code, sym_cl) + + // Fill all table entries that share this prefix + let fill_step = USize(1) << sym_cl.usize() + var idx = reversed.usize() + while idx < table_size do + try table(idx)? = (symbol.u16(), sym_cl) end + idx = idx + fill_step + end + end + symbol = symbol + 1 + end + + _HuffmanTable._create(consume table, max_bits) + + fun _reverse_bits(value: U32, num_bits: U8): U32 => + var result: U32 = 0 + var v = value + var i: U8 = 0 + while i < num_bits do + result = (result << 1) or (v and 1) + v = v >> 1 + i = i + 1 + end + result + +primitive _FixedHuffmanTables + """ + Pre-built fixed Huffman tables per RFC 1951 Section 3.2.6. + """ + fun lit_len(): (_HuffmanTable | InflateError) => + """ + Fixed literal/length table: + 0-143: 8-bit, 144-255: 9-bit, 256-279: 7-bit, 280-287: 8-bit. + """ + let lengths = recover val + let arr = Array[U8](288) + var i: U16 = 0 + while i <= 143 do arr.push(8); i = i + 1 end + while i <= 255 do arr.push(9); i = i + 1 end + while i <= 279 do arr.push(7); i = i + 1 end + while i <= 287 do arr.push(8); i = i + 1 end + arr + end + _BuildHuffmanTable(lengths) + + fun dist(): (_HuffmanTable | InflateError) => + """ + Fixed distance table: all 32 codes are 5-bit. + """ + let lengths = recover val + Array[U8].init(5, 32) + end + _BuildHuffmanTable(lengths) diff --git a/corral/git/inflate/_mort.pony b/corral/git/inflate/_mort.pony new file mode 100644 index 0000000..3d7f274 --- /dev/null +++ b/corral/git/inflate/_mort.pony @@ -0,0 +1,18 @@ +use @exit[None](status: U8) +use @fprintf[I32](stream: Pointer[U8] tag, fmt: Pointer[U8] tag, ...) +use @pony_os_stderr[Pointer[U8]]() + +primitive _Unreachable + """ + To be used in places that the compiler can't prove is unreachable but we are + certain is unreachable and if we reach it, we'd be silently hiding a bug. + """ + fun apply(loc: SourceLoc = __loc) => + @fprintf( + @pony_os_stderr(), + ("The unreachable was reached in %s at line %s\n" + + "Please open an issue at https://github.com/ponylang/corral/issues") + .cstring(), + loc.file().cstring(), + loc.line().string().cstring()) + @exit(1) diff --git a/corral/git/inflate/_tables.pony b/corral/git/inflate/_tables.pony new file mode 100644 index 0000000..8614ab2 --- /dev/null +++ b/corral/git/inflate/_tables.pony @@ -0,0 +1,50 @@ +primitive _Tables + """ + Static tables for DEFLATE decoding per RFC 1951. + """ + + fun length_base(): Array[U16] val => + """ + Base lengths for length codes 257-285. Index = code - 257. + """ + [as U16: + 3; 4; 5; 6; 7; 8; 9; 10; 11; 13 + 15; 17; 19; 23; 27; 31; 35; 43; 51; 59 + 67; 83; 99; 115; 131; 163; 195; 227; 258 + ] + + fun length_extra(): Array[U8] val => + """ + Extra bits for length codes 257-285. Index = code - 257. + """ + [as U8: + 0; 0; 0; 0; 0; 0; 0; 0; 1; 1 + 1; 1; 2; 2; 2; 2; 3; 3; 3; 3 + 4; 4; 4; 4; 5; 5; 5; 5; 0 + ] + + fun dist_base(): Array[U16] val => + """ + Base distances for distance codes 0-29. + """ + [as U16: + 1; 2; 3; 4; 5; 7; 9; 13; 17; 25 + 33; 49; 65; 97; 129; 193; 257; 385; 513; 769 + 1025; 1537; 2049; 3073; 4097; 6145; 8193; 12289; 16385; 24577 + ] + + fun dist_extra(): Array[U8] val => + """ + Extra bits for distance codes 0-29. + """ + [as U8: + 0; 0; 0; 0; 1; 1; 2; 2; 3; 3 + 4; 4; 5; 5; 6; 6; 7; 7; 8; 8 + 9; 9; 10; 10; 11; 11; 12; 12; 13; 13 + ] + + fun code_length_order(): Array[U8] val => + """ + Order of code length alphabet codes for dynamic Huffman (RFC 1951 3.2.7). + """ + [as U8: 16; 17; 18; 0; 8; 7; 9; 6; 10; 5; 11; 4; 12; 3; 13; 2; 14; 1; 15] diff --git a/corral/git/inflate/_test.pony b/corral/git/inflate/_test.pony new file mode 100644 index 0000000..82835a6 --- /dev/null +++ b/corral/git/inflate/_test.pony @@ -0,0 +1,40 @@ +use "pony_test" +use "pony_check" + +actor \nodoc\ Main is TestList + new create(env: Env) => + PonyTest(env, this) + + new make() => + None + + fun tag tests(test: PonyTest) => + // Adler-32 tests + test(_TestAdler32Empty) + test(_TestAdler32ZeroByte) + test(_TestAdler32Wikipedia) + test(Property1UnitTest[Array[U8] val](_PropertyAdler32Reference)) + + // Inflate tests + test(_TestInflateEmpty) + test(_TestInflateHello) + test(_TestInflateRepeated) + test(_TestInflateAllBytes) + test(_TestInflateStored) + test(_TestInflateDynamic) + test(_TestInflateMaxBackRef) + test(_TestInflateMultiBlock) + test(_TestInflateGitObject) + test(_TestInflateRawMinimal) + test(_TestInflateRawFixed) + test(_TestInflateRawStored) + + // Error path tests + test(_TestInflateChecksumCorruption) + test(_TestInflateTruncated) + test(_TestInflateInvalidBlockType) + test(_TestInflateInvalidHeader) + test(_TestInflateStoredLengthMismatch) + + // BitReader tests + test(_PropertyBitReaderSplit) diff --git a/corral/git/inflate/_test_adler32.pony b/corral/git/inflate/_test_adler32.pony new file mode 100644 index 0000000..7d0b6bc --- /dev/null +++ b/corral/git/inflate/_test_adler32.pony @@ -0,0 +1,52 @@ +use "pony_test" +use "pony_check" + +class \nodoc\ iso _TestAdler32Empty is UnitTest + """adler32(empty) == 1""" + fun name(): String => "inflate/adler32/empty" + + fun apply(h: TestHelper) => + h.assert_eq[U32](1, _Adler32(recover val Array[U8] end)) + +class \nodoc\ iso _TestAdler32ZeroByte is UnitTest + """adler32([0x00]) == 0x00010001""" + fun name(): String => "inflate/adler32/zero-byte" + + fun apply(h: TestHelper) => + h.assert_eq[U32](0x00010001, _Adler32([as U8: 0x00])) + +class \nodoc\ iso _TestAdler32Wikipedia is UnitTest + """adler32("Wikipedia") verified against Python's zlib.adler32().""" + fun name(): String => "inflate/adler32/wikipedia" + + fun apply(h: TestHelper) => + // Python: zlib.adler32(b"Wikipedia") => 300286872 => 0x11E60398 + let data: Array[U8] val = [as U8: + 0x57; 0x69; 0x6B; 0x69; 0x70; 0x65; 0x64; 0x69; 0x61] + h.assert_eq[U32](0x11E60398, _Adler32(data)) + +class \nodoc\ _PropertyAdler32Reference is Property1[Array[U8] val] + """ + Verify _Adler32 against a reference implementation computed directly + from the definition for random byte arrays. + """ + fun name(): String => "inflate/adler32/property/reference" + + fun gen(): Generator[Array[U8] val] => + Generators.map2[U8, USize, Array[U8] val]( + Generators.u8(), + Generators.usize(0, 6000), + {(fill, len) => recover val Array[U8].init(fill, len) end }) + + fun property(sample: Array[U8] val, h: PropertyHelper) => + // Reference implementation: direct computation without NMAX optimization + let base: U32 = 65521 + var s1: U32 = 1 + var s2: U32 = 0 + for b in sample.values() do + s1 = (s1 + b.u32()) % base + s2 = (s2 + s1) % base + end + let expected = (s2 << 16) or s1 + + h.assert_eq[U32](expected, _Adler32(sample)) diff --git a/corral/git/inflate/_test_inflate.pony b/corral/git/inflate/_test_inflate.pony new file mode 100644 index 0000000..96aede4 --- /dev/null +++ b/corral/git/inflate/_test_inflate.pony @@ -0,0 +1,432 @@ +use "pony_test" +use "pony_check" + +// ---- Helper ---- + +primitive _AssertInflateOk + """ + Decompress and verify the result matches expected output. + """ + fun apply( + h: TestHelper, + compressed: Array[U8] val, + expected: Array[U8] val, + raw: Bool = false) + => + let result = if raw then + InflateRaw(compressed) + else + Inflate(compressed) + end + match result + | let data: Array[U8] val => + h.assert_eq[USize](expected.size(), data.size(), + "output size mismatch: expected " + expected.size().string() + + " got " + data.size().string()) + var i: USize = 0 + while i < expected.size() do + try + h.assert_eq[U8](expected(i)?, data(i)?, + "byte mismatch at offset " + i.string()) + end + i = i + 1 + end + | let err: InflateError => + h.fail("expected success but got: " + err.string()) + end + +primitive _AssertInflateError + """ + Decompress and verify the result is the expected error type. + """ + fun apply( + h: TestHelper, + compressed: Array[U8] val, + expected: InflateError, + raw: Bool = false) + => + let result = if raw then + InflateRaw(compressed) + else + Inflate(compressed) + end + match result + | let data: Array[U8] val => + h.fail("expected error '" + expected.string() + + "' but got " + data.size().string() + " bytes") + | let err: InflateError => + h.assert_eq[String val](expected.string(), err.string()) + end + +// ---- Happy path tests ---- + +class \nodoc\ iso _TestInflateEmpty is UnitTest + """zlib.compress(b"") -- minimal valid zlib stream.""" + fun name(): String => "inflate/empty" + + fun apply(h: TestHelper) => + // Python: zlib.compress(b"") + let compressed: Array[U8] val = + [as U8: 0x78; 0x9C; 0x03; 0x00; 0x00; 0x00; 0x00; 0x01] + _AssertInflateOk(h, compressed, recover val Array[U8] end) + +class \nodoc\ iso _TestInflateHello is UnitTest + """zlib.compress(b"hello") -- single fixed Huffman block.""" + fun name(): String => "inflate/hello" + + fun apply(h: TestHelper) => + // Python: zlib.compress(b"hello") + let compressed: Array[U8] val = [as U8: + 0x78; 0x9C; 0xCB; 0x48; 0xCD; 0xC9; 0xC9; 0x07 + 0x00; 0x06; 0x2C; 0x02; 0x15] + let expected: Array[U8] val = [as U8: + 0x68; 0x65; 0x6C; 0x6C; 0x6F] // "hello" + _AssertInflateOk(h, compressed, expected) + +class \nodoc\ iso _TestInflateRepeated is UnitTest + """zlib.compress(b"a" * 1000) -- LZ77 back-references.""" + fun name(): String => "inflate/repeated" + + fun apply(h: TestHelper) => + // Python: zlib.compress(b"a" * 1000) + let compressed: Array[U8] val = [as U8: + 0x78; 0x9C; 0x4B; 0x4C; 0x1C; 0x05; 0xA3; 0x60 + 0x14; 0x0C; 0x77; 0x00; 0x00; 0xF9; 0xD8; 0x7A; 0xF8] + let expected = recover val Array[U8].init('a', 1000) end + _AssertInflateOk(h, compressed, expected) + +class \nodoc\ iso _TestInflateAllBytes is UnitTest + """zlib.compress(bytes(range(256))) -- all 256 literal values.""" + fun name(): String => "inflate/all-bytes" + + fun apply(h: TestHelper) => + // Python: zlib.compress(bytes(range(256))) + let compressed: Array[U8] val = [as U8: + 0x78; 0x9C; 0x01; 0x00; 0x01; 0xFF; 0xFE; 0x00; 0x01; 0x02 + 0x03; 0x04; 0x05; 0x06; 0x07; 0x08; 0x09; 0x0A; 0x0B; 0x0C + 0x0D; 0x0E; 0x0F; 0x10; 0x11; 0x12; 0x13; 0x14; 0x15; 0x16 + 0x17; 0x18; 0x19; 0x1A; 0x1B; 0x1C; 0x1D; 0x1E; 0x1F; 0x20 + 0x21; 0x22; 0x23; 0x24; 0x25; 0x26; 0x27; 0x28; 0x29; 0x2A + 0x2B; 0x2C; 0x2D; 0x2E; 0x2F; 0x30; 0x31; 0x32; 0x33; 0x34 + 0x35; 0x36; 0x37; 0x38; 0x39; 0x3A; 0x3B; 0x3C; 0x3D; 0x3E + 0x3F; 0x40; 0x41; 0x42; 0x43; 0x44; 0x45; 0x46; 0x47; 0x48 + 0x49; 0x4A; 0x4B; 0x4C; 0x4D; 0x4E; 0x4F; 0x50; 0x51; 0x52 + 0x53; 0x54; 0x55; 0x56; 0x57; 0x58; 0x59; 0x5A; 0x5B; 0x5C + 0x5D; 0x5E; 0x5F; 0x60; 0x61; 0x62; 0x63; 0x64; 0x65; 0x66 + 0x67; 0x68; 0x69; 0x6A; 0x6B; 0x6C; 0x6D; 0x6E; 0x6F; 0x70 + 0x71; 0x72; 0x73; 0x74; 0x75; 0x76; 0x77; 0x78; 0x79; 0x7A + 0x7B; 0x7C; 0x7D; 0x7E; 0x7F; 0x80; 0x81; 0x82; 0x83; 0x84 + 0x85; 0x86; 0x87; 0x88; 0x89; 0x8A; 0x8B; 0x8C; 0x8D; 0x8E + 0x8F; 0x90; 0x91; 0x92; 0x93; 0x94; 0x95; 0x96; 0x97; 0x98 + 0x99; 0x9A; 0x9B; 0x9C; 0x9D; 0x9E; 0x9F; 0xA0; 0xA1; 0xA2 + 0xA3; 0xA4; 0xA5; 0xA6; 0xA7; 0xA8; 0xA9; 0xAA; 0xAB; 0xAC + 0xAD; 0xAE; 0xAF; 0xB0; 0xB1; 0xB2; 0xB3; 0xB4; 0xB5; 0xB6 + 0xB7; 0xB8; 0xB9; 0xBA; 0xBB; 0xBC; 0xBD; 0xBE; 0xBF; 0xC0 + 0xC1; 0xC2; 0xC3; 0xC4; 0xC5; 0xC6; 0xC7; 0xC8; 0xC9; 0xCA + 0xCB; 0xCC; 0xCD; 0xCE; 0xCF; 0xD0; 0xD1; 0xD2; 0xD3; 0xD4 + 0xD5; 0xD6; 0xD7; 0xD8; 0xD9; 0xDA; 0xDB; 0xDC; 0xDD; 0xDE + 0xDF; 0xE0; 0xE1; 0xE2; 0xE3; 0xE4; 0xE5; 0xE6; 0xE7; 0xE8 + 0xE9; 0xEA; 0xEB; 0xEC; 0xED; 0xEE; 0xEF; 0xF0; 0xF1; 0xF2 + 0xF3; 0xF4; 0xF5; 0xF6; 0xF7; 0xF8; 0xF9; 0xFA; 0xFB; 0xFC + 0xFD; 0xFE; 0xFF; 0xAD; 0xF6; 0x7F; 0x81] + let expected = recover val + let arr = Array[U8](256) + var i: U16 = 0 + while i < 256 do + arr.push(i.u8()) + i = i + 1 + end + arr + end + _AssertInflateOk(h, compressed, expected) + +class \nodoc\ iso _TestInflateStored is UnitTest + """zlib.compress(b"short", level=0) -- stored (BTYPE=00) block.""" + fun name(): String => "inflate/stored" + + fun apply(h: TestHelper) => + // Python: zlib.compress(b"short", 0) + let compressed: Array[U8] val = [as U8: + 0x78; 0x01; 0x01; 0x05; 0x00; 0xFA; 0xFF; 0x73 + 0x68; 0x6F; 0x72; 0x74; 0x06; 0x89; 0x02; 0x31] + let expected: Array[U8] val = [as U8: + 0x73; 0x68; 0x6F; 0x72; 0x74] // "short" + _AssertInflateOk(h, compressed, expected) + +class \nodoc\ iso _TestInflateDynamic is UnitTest + """ + Compressed random-ish data that forces BTYPE=10 (dynamic Huffman). + Data is SHA-256 of 'test-vector-seed' repeated 128 times (4096 bytes). + """ + fun name(): String => "inflate/dynamic" + + fun apply(h: TestHelper) => + // Python: hashlib.sha256(b'test-vector-seed').digest() * 128, then zlib.compress() + let compressed: Array[U8] val = [as U8: + 0x78; 0x9C; 0xE3; 0xBA; 0x3B; 0xF1; 0xEB; 0xC2 + 0x97; 0x87; 0xB8; 0x9D; 0x98; 0xD2; 0x72; 0x96 + 0xCD; 0x17; 0x95; 0x3C; 0xE9; 0x1C; 0x35; 0xE5 + 0xF2; 0xE6; 0x87; 0x1A; 0x4A; 0x9E; 0x5C; 0xD1 + 0x42; 0xDE; 0xB3; 0x97; 0x71; 0x8D; 0xCA; 0x8F + 0xCA; 0x8F; 0xCA; 0x8F; 0xCA; 0x8F; 0xCA; 0x8F + 0xCA; 0x8F; 0xCA; 0x8F; 0xCA; 0x8F; 0xCA; 0x8F + 0xCA; 0x8F; 0xCA; 0x8F; 0xCA; 0x8F; 0xCA; 0x8F + 0xCA; 0x8F; 0xCA; 0x0F; 0x79; 0x79; 0x00; 0x7D + 0x51; 0x22; 0x6A] + let result = Inflate(compressed) + match result + | let data: Array[U8] val => + h.assert_eq[USize](4096, data.size()) + // Verify it's 128 repetitions of 32 bytes + try + let first_32 = recover val + let arr = Array[U8](32) + var i: USize = 0 + while i < 32 do + arr.push(data(i)?) + i = i + 1 + end + arr + end + var rep: USize = 1 + while rep < 128 do + var j: USize = 0 + while j < 32 do + h.assert_eq[U8](first_32(j)?, data((rep * 32) + j)?, + "mismatch at rep " + rep.string() + " byte " + j.string()) + j = j + 1 + end + rep = rep + 1 + end + end + | let err: InflateError => + h.fail("expected success but got: " + err.string()) + end + +class \nodoc\ iso _TestInflateMaxBackRef is UnitTest + """32KB+ input with repeated pattern testing full sliding window.""" + fun name(): String => "inflate/max-back-ref" + + fun apply(h: TestHelper) => + // Python: zlib.compress(b'A' * 100 + b'B' * 32668 + b'A' * 100) + let compressed: Array[U8] val = [as U8: + 0x78; 0x9C; 0xED; 0xC1; 0x41; 0x11; 0x00; 0x00 + 0x08; 0x03; 0xA0; 0x6C; 0xB3; 0x7F; 0x28; 0x53 + 0xCC; 0xF3; 0x01; 0x24; 0x7D; 0x03; 0x00; 0x00 + 0x00; 0x00; 0x00; 0x00; 0x00; 0x00; 0x00; 0x00 + 0x00; 0x00; 0x00; 0x00; 0x00; 0x00; 0x00; 0x00 + 0x00; 0x00; 0x00; 0x00; 0x00; 0x00; 0x00; 0x00 + 0x00; 0x00; 0x00; 0x00; 0x00; 0xCF; 0xE5; 0xC0 + 0x02; 0x75; 0xE0; 0x1A; 0xF0] + let result = Inflate(compressed) + match result + | let data: Array[U8] val => + h.assert_eq[USize](32868, data.size()) + // First 100 bytes should be 'A' + try h.assert_eq[U8]('A', data(0)?) end + try h.assert_eq[U8]('A', data(99)?) end + // Middle should be 'B' + try h.assert_eq[U8]('B', data(100)?) end + try h.assert_eq[U8]('B', data(32767)?) end + // Last 100 should be 'A' + try h.assert_eq[U8]('A', data(32768)?) end + try h.assert_eq[U8]('A', data(32867)?) end + | let err: InflateError => + h.fail("expected success but got: " + err.string()) + end + +class \nodoc\ iso _TestInflateMultiBlock is UnitTest + """Multiple DEFLATE blocks (BFINAL=0 followed by BFINAL=1).""" + fun name(): String => "inflate/multi-block" + + fun apply(h: TestHelper) => + // Python: zlib.compress((b'ABCDEFGHIJ' * 1000) + (b'0123456789' * 1000)) + let compressed: Array[U8] val = [as U8: + 0x78; 0x9C; 0xED; 0xC6; 0xC9; 0x0D; 0x80; 0x20 + 0x00; 0x00; 0xB0; 0x95; 0x10; 0x54; 0xE0; 0x89 + 0xE2; 0xB9; 0xFF; 0x40; 0xCC; 0x41; 0xD2; 0xBE + 0xDA; 0x8E; 0xB3; 0x5F; 0xF7; 0xF3; 0x7E; 0x7F + 0x33; 0x33; 0x33; 0x33; 0x33; 0x33; 0x33; 0x33 + 0x33; 0x33; 0x33; 0x33; 0x33; 0x33; 0x33; 0x33 + 0x33; 0x33; 0x33; 0x9B; 0x76; 0x61; 0x89; 0x69 + 0xDD; 0xF6; 0x5C; 0xAA; 0x99; 0x99; 0x99; 0x99 + 0x99; 0x99; 0x99; 0x99; 0x99; 0x99; 0x99; 0x99 + 0x99; 0x99; 0x99; 0x99; 0x99; 0x99; 0xD9; 0xBC + 0x1B; 0xE1; 0x8D; 0x9E; 0xAF] + let result = Inflate(compressed) + match result + | let data: Array[U8] val => + h.assert_eq[USize](20000, data.size()) + // Check first part is ABCDEFGHIJ repeated + try h.assert_eq[U8]('A', data(0)?) end + try h.assert_eq[U8]('J', data(9)?) end + try h.assert_eq[U8]('A', data(10)?) end + // Check second part is 0123456789 repeated + try h.assert_eq[U8]('0', data(10000)?) end + try h.assert_eq[U8]('9', data(10009)?) end + | let err: InflateError => + h.fail("expected success but got: " + err.string()) + end + +class \nodoc\ iso _TestInflateGitObject is UnitTest + """ + zlib.compress(b"blob 5\0hello") -- bridges inflate testing to git use case. + """ + fun name(): String => "inflate/git-object" + + fun apply(h: TestHelper) => + // Python: zlib.compress(b"blob 5\0hello") + let compressed: Array[U8] val = [as U8: + 0x78; 0x9C; 0x4B; 0xCA; 0xC9; 0x4F; 0x52; 0x30 + 0x65; 0xC8; 0x48; 0xCD; 0xC9; 0xC9; 0x07; 0x00 + 0x19; 0xAA; 0x04; 0x09] + let expected: Array[U8] val = [as U8: + 0x62; 0x6C; 0x6F; 0x62; 0x20; 0x35; 0x00; 0x68 + 0x65; 0x6C; 0x6C; 0x6F] // "blob 5\0hello" + _AssertInflateOk(h, compressed, expected) + +class \nodoc\ iso _TestInflateRawMinimal is UnitTest + """ + Minimal raw DEFLATE stream: 0x03 0x00 (empty fixed block with BFINAL=1). + From Mark Adler's puff reference. + """ + fun name(): String => "inflate/raw/minimal" + + fun apply(h: TestHelper) => + let compressed: Array[U8] val = [as U8: 0x03; 0x00] + _AssertInflateOk(h, compressed, recover val Array[U8] end where raw = true) + +class \nodoc\ iso _TestInflateRawFixed is UnitTest + """Raw DEFLATE fixed Huffman block for 'hello' (no zlib framing).""" + fun name(): String => "inflate/raw/fixed" + + fun apply(h: TestHelper) => + // Stripped from zlib.compress(b"hello"): remove 2-byte header and 4-byte trailer + let compressed: Array[U8] val = [as U8: + 0xCB; 0x48; 0xCD; 0xC9; 0xC9; 0x07; 0x00] + let expected: Array[U8] val = [as U8: + 0x68; 0x65; 0x6C; 0x6C; 0x6F] // "hello" + _AssertInflateOk(h, compressed, expected where raw = true) + +class \nodoc\ iso _TestInflateRawStored is UnitTest + """Raw DEFLATE stored block containing 'hello'.""" + fun name(): String => "inflate/raw/stored" + + fun apply(h: TestHelper) => + // Hand-crafted: BFINAL=1, BTYPE=00, LEN=5, NLEN=~5, "hello" + let compressed: Array[U8] val = [as U8: + 0x01; 0x05; 0x00; 0xFA; 0xFF; 0x68; 0x65; 0x6C; 0x6C; 0x6F] + let expected: Array[U8] val = [as U8: + 0x68; 0x65; 0x6C; 0x6C; 0x6F] // "hello" + _AssertInflateOk(h, compressed, expected where raw = true) + +// ---- Error path tests ---- + +class \nodoc\ iso _TestInflateChecksumCorruption is UnitTest + """Flip a byte in compressed payload to trigger checksum mismatch.""" + fun name(): String => "inflate/error/checksum" + + fun apply(h: TestHelper) => + // Take valid "hello" compressed data, flip a byte in the payload + let corrupted: Array[U8] val = [as U8: + 0x78; 0x9C; 0xCB; 0x48; 0xCD; 0xC9; 0xC9; 0x07 + 0x00; 0x06; 0x2C; 0x02; 0xFF] // last byte changed from 0x15 to 0xFF + _AssertInflateError(h, corrupted, InflateChecksumMismatch) + +class \nodoc\ iso _TestInflateTruncated is UnitTest + """Truncated input at various points.""" + fun name(): String => "inflate/error/truncated" + + fun apply(h: TestHelper) => + // Just the zlib header, no DEFLATE data + _AssertInflateError(h, + [as U8: 0x78; 0x9C], InflateIncompleteInput) + + // zlib header + partial fixed block + _AssertInflateError(h, + [as U8: 0x78; 0x9C; 0xCB], InflateIncompleteInput) + +class \nodoc\ iso _TestInflateInvalidBlockType is UnitTest + """Craft a byte stream with BTYPE=11 (invalid).""" + fun name(): String => "inflate/error/invalid-block-type" + + fun apply(h: TestHelper) => + // Zlib header + BFINAL=1, BTYPE=11 (binary: 111 = 0x07 in first 3 bits) + // Extra bytes so the reader doesn't run out of input before detecting block type + let data: Array[U8] val = [as U8: 0x78; 0x9C; 0x07; 0x00; 0x00; 0x00; 0x00; 0x00] + _AssertInflateError(h, data, InflateInvalidBlockType) + +class \nodoc\ iso _TestInflateInvalidHeader is UnitTest + """Various invalid zlib headers.""" + fun name(): String => "inflate/error/invalid-header" + + fun apply(h: TestHelper) => + // Wrong CM value (not 8) + _AssertInflateError(h, + [as U8: 0x79; 0x9C; 0x03; 0x00; 0x00; 0x00; 0x00; 0x01], + InflateInvalidHeader) + + // FCHECK failure -- (0x78 * 256 + 0x00) % 31 != 0 + _AssertInflateError(h, + [as U8: 0x78; 0x00; 0x03; 0x00; 0x00; 0x00; 0x00; 0x01], + InflateInvalidHeader) + + // FDICT set (bit 5 of FLG) + // 0x78 0xBC: CM=8, CINFO=7, FDICT=1, FCHECK adjusted + // (0x78*256 + 0xBB) = 30907, 30907 % 31 = 0 => FCHECK works, FDICT set + _AssertInflateError(h, + [as U8: 0x78; 0xBB; 0x03; 0x00; 0x00; 0x00; 0x00; 0x01], + InflateInvalidHeader) + +class \nodoc\ iso _TestInflateStoredLengthMismatch is UnitTest + """Stored block where LEN != ~NLEN.""" + fun name(): String => "inflate/error/stored-length-mismatch" + + fun apply(h: TestHelper) => + // Raw DEFLATE: BFINAL=1, BTYPE=00, LEN=5 (0x0005), NLEN=0x0000 (wrong) + _AssertInflateError(h, + [as U8: 0x01; 0x05; 0x00; 0x00; 0x00; 0x68; 0x65; 0x6C; 0x6C; 0x6F], + InflateInvalidStoredLength where raw = true) + +// ---- BitReader tests ---- + +class \nodoc\ iso _PropertyBitReaderSplit is UnitTest + """ + Reading n bits then m bits produces the same result as reading n+m bits + and splitting. + + TODO: Convert back to Property1[Array[U8] val] once ponyc#4838 is fixed. + Was a property test using Generators.map2 but segfaults due to vtable bug. + """ + fun name(): String => "inflate/bit-reader-split" + + fun apply(h: TestHelper) => + _check_split(h, [as U8: 0xAB; 0xCD; 0xEF; 0x01]) + _check_split(h, [as U8: 0x00; 0x00; 0x00; 0x00]) + _check_split(h, [as U8: 0xFF; 0xFF; 0xFF; 0xFF]) + _check_split(h, [as U8: 0x55; 0xAA; 0x55; 0xAA]) + + fun _check_split(h: TestHelper, data: Array[U8] val) => + // Read 5 bits then 7 bits from one reader + let r1 = _BitReader(data) + let a = match r1.bits(5) + | let v: U32 => v + | let e: InflateError => h.fail(e.string()); return + end + let b = match r1.bits(7) + | let v: U32 => v + | let e: InflateError => h.fail(e.string()); return + end + + // Read 12 bits from another reader and split + let r2 = _BitReader(data) + let combined = match r2.bits(12) + | let v: U32 => v + | let e: InflateError => h.fail(e.string()); return + end + + let a2 = combined and 0x1F // lower 5 bits + let b2 = (combined >> 5) and 0x7F // next 7 bits + + h.assert_eq[U32](a, a2, "lower bits mismatch") + h.assert_eq[U32](b, b2, "upper bits mismatch") diff --git a/corral/git/inflate/inflate.pony b/corral/git/inflate/inflate.pony new file mode 100644 index 0000000..e8ef195 --- /dev/null +++ b/corral/git/inflate/inflate.pony @@ -0,0 +1,95 @@ +""" +RFC 1951 DEFLATE decompression with RFC 1950 zlib framing. Provides one-shot +decompression suitable for git object reading -- objects are read whole from +disk or pack entries, so streaming decompression is not needed. + +Use `Inflate` for zlib-framed data (loose git objects). Use `InflateRaw` for +raw DEFLATE streams without zlib framing (packfile delta data). +""" + +primitive Inflate + """ + Decompresses zlib-framed data (RFC 1950) containing a DEFLATE stream + (RFC 1951). Returns the decompressed bytes or an error describing what + went wrong. + + This is a one-shot API: provide the complete compressed input and receive + the complete decompressed output. Streaming decompression is not needed + for git object reading -- objects are read whole from disk or pack entries. + """ + fun apply(data: Array[U8] val): (Array[U8] val | InflateError) => + // Validate zlib header (RFC 1950) + if data.size() < 6 then return InflateIncompleteInput end + + let cmf = try data(0)? else return InflateIncompleteInput end + let flg = try data(1)? else return InflateIncompleteInput end + + // CM must be 8 (deflate), CINFO must be <= 7 + if (cmf and 0x0F) != 8 then return InflateInvalidHeader end + if (cmf >> 4) > 7 then return InflateInvalidHeader end + + // FCHECK: (CMF * 256 + FLG) must be a multiple of 31 + if (((cmf.u16() * 256) + flg.u16()) % 31) != 0 then + return InflateInvalidHeader + end + + // FDICT must not be set (git never uses preset dictionaries) + if (flg and 0x20) != 0 then return InflateInvalidHeader end + + // All decompression + checksum inside recover val. + // data is val (sendable), inflate_err holds only val primitives (sendable). + var inflate_err: (InflateError | None) = None + let decompressed: Array[U8] val = recover val + let reader = _BitReader(data, 2) + let output = Array[U8] + + match _Deflate(reader, output) + | let e: InflateError => + inflate_err = e + else + // Read 4-byte Adler-32 checksum (big-endian) after DEFLATE stream + reader.align_to_byte() + var ok = true + let b0 = match reader.bits(8) | let v: U32 => v | let e: InflateError => inflate_err = e; ok = false; U32(0) end + let b1 = if ok then match reader.bits(8) | let v: U32 => v | let e: InflateError => inflate_err = e; ok = false; U32(0) end else U32(0) end + let b2 = if ok then match reader.bits(8) | let v: U32 => v | let e: InflateError => inflate_err = e; ok = false; U32(0) end else U32(0) end + let b3 = if ok then match reader.bits(8) | let v: U32 => v | let e: InflateError => inflate_err = e; ok = false; U32(0) end else U32(0) end + + if ok then + let stored = (b0 << 24) or (b1 << 16) or (b2 << 8) or b3 + let computed = _Adler32._from_ref(output) + if stored != computed then + inflate_err = InflateChecksumMismatch + end + end + end + output + end + + match inflate_err + | let e: InflateError => e + else + decompressed + end + +primitive InflateRaw + """ + Decompresses a raw DEFLATE stream (RFC 1951) without zlib framing. + Needed for git packfile delta data, which is stored without zlib headers. + """ + fun apply(data: Array[U8] val): (Array[U8] val | InflateError) => + var inflate_err: (InflateError | None) = None + let decompressed: Array[U8] val = recover val + let reader = _BitReader(data) + let output = Array[U8] + match _Deflate(reader, output) + | let e: InflateError => inflate_err = e + end + output + end + + match inflate_err + | let e: InflateError => e + else + decompressed + end diff --git a/corral/git/inflate/inflate_error.pony b/corral/git/inflate/inflate_error.pony new file mode 100644 index 0000000..395824a --- /dev/null +++ b/corral/git/inflate/inflate_error.pony @@ -0,0 +1,43 @@ +type InflateError is + ( InflateInvalidHeader + | InflateInvalidBlockType + | InflateIncompleteInput + | InflateInvalidCodeLengths + | InflateInvalidDistance + | InflateInvalidLitLen + | InflateChecksumMismatch + | InflateInvalidStoredLength + ) + """Errors that can occur during DEFLATE decompression or zlib framing.""" + +primitive InflateInvalidHeader + """Zlib header validation failed (wrong CM, bad FCHECK, or FDICT set).""" + fun string(): String val => "invalid zlib header" + +primitive InflateInvalidBlockType + """DEFLATE block type 11 (reserved/invalid) was encountered.""" + fun string(): String val => "invalid DEFLATE block type" + +primitive InflateIncompleteInput + """Compressed data ended before the stream was complete.""" + fun string(): String val => "unexpected end of compressed data" + +primitive InflateInvalidCodeLengths + """Dynamic Huffman code lengths failed validation.""" + fun string(): String val => "invalid Huffman code lengths" + +primitive InflateInvalidDistance + """Back-reference distance exceeds current output size.""" + fun string(): String val => "invalid back-reference distance" + +primitive InflateInvalidLitLen + """Literal/length code is outside the valid range.""" + fun string(): String val => "invalid literal/length code" + +primitive InflateChecksumMismatch + """Computed Adler-32 does not match the stored checksum.""" + fun string(): String val => "Adler-32 checksum mismatch" + +primitive InflateInvalidStoredLength + """Stored block LEN does not match the complement NLEN.""" + fun string(): String val => "stored block length mismatch" diff --git a/corral/git/sha1/_mort.pony b/corral/git/sha1/_mort.pony new file mode 100644 index 0000000..3d7f274 --- /dev/null +++ b/corral/git/sha1/_mort.pony @@ -0,0 +1,18 @@ +use @exit[None](status: U8) +use @fprintf[I32](stream: Pointer[U8] tag, fmt: Pointer[U8] tag, ...) +use @pony_os_stderr[Pointer[U8]]() + +primitive _Unreachable + """ + To be used in places that the compiler can't prove is unreachable but we are + certain is unreachable and if we reach it, we'd be silently hiding a bug. + """ + fun apply(loc: SourceLoc = __loc) => + @fprintf( + @pony_os_stderr(), + ("The unreachable was reached in %s at line %s\n" + + "Please open an issue at https://github.com/ponylang/corral/issues") + .cstring(), + loc.file().cstring(), + loc.line().string().cstring()) + @exit(1) diff --git a/corral/git/sha1/_test.pony b/corral/git/sha1/_test.pony new file mode 100644 index 0000000..fa27ef5 --- /dev/null +++ b/corral/git/sha1/_test.pony @@ -0,0 +1,118 @@ +use "pony_test" +use "pony_check" +use crypto = "ssl/crypto" + +actor \nodoc\ Main is TestList + new create(env: Env) => + PonyTest(env, this) + + new make() => + None + + fun tag tests(test: PonyTest) => + test(Property1UnitTest[Array[U8] val](_PropertySha1DigestSize)) + test(Property1UnitTest[Array[U8] val](_PropertySha1HexSize)) + test(_TestSha1Empty) + test(_TestSha1Abc) + test(_TestSha1Long) + test(_TestSha1GitObject) + test(_TestSha1FromChunksEquivalence) + test(_TestSha1HexFormat) + +class \nodoc\ _PropertySha1DigestSize is Property1[Array[U8] val] + """SHA-1 digest is always 20 bytes for arbitrary input.""" + fun name(): String => "sha1/property/digest-size" + + fun gen(): Generator[Array[U8] val] => + Generators.map2[U8, USize, Array[U8] val]( + Generators.u8(), + Generators.usize(0, 200), + {(fill, len) => recover val Array[U8].init(fill, len) end }) + + fun property(sample: Array[U8] val, h: PropertyHelper) => + h.assert_eq[USize](20, GitSha1(sample).size()) + +class \nodoc\ _PropertySha1HexSize is Property1[Array[U8] val] + """SHA-1 hex string is always 40 characters for arbitrary input.""" + fun name(): String => "sha1/property/hex-size" + + fun gen(): Generator[Array[U8] val] => + Generators.map2[U8, USize, Array[U8] val]( + Generators.u8(), + Generators.usize(0, 200), + {(fill, len) => recover val Array[U8].init(fill, len) end }) + + fun property(sample: Array[U8] val, h: PropertyHelper) => + h.assert_eq[USize](40, GitSha1.hex(sample).size()) + +class \nodoc\ iso _TestSha1Empty is UnitTest + """NIST FIPS 180-4: SHA-1 of empty string.""" + fun name(): String => "sha1/nist/empty" + + fun apply(h: TestHelper) => + let expected = "da39a3ee5e6b4b0d3255bfef95601890afd80709" + h.assert_eq[String val](expected, GitSha1.hex("")) + +class \nodoc\ iso _TestSha1Abc is UnitTest + """NIST FIPS 180-4: SHA-1 of "abc".""" + fun name(): String => "sha1/nist/abc" + + fun apply(h: TestHelper) => + let expected = "a9993e364706816aba3e25717850c26c9cd0d89d" + h.assert_eq[String val](expected, GitSha1.hex("abc")) + +class \nodoc\ iso _TestSha1Long is UnitTest + """NIST FIPS 180-4: SHA-1 of the 448-bit test string.""" + fun name(): String => "sha1/nist/long" + + fun apply(h: TestHelper) => + let input = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" + let expected = "84983e441c3bd26ebaae4aa1f95129e5e54670f1" + h.assert_eq[String val](expected, GitSha1.hex(input)) + +class \nodoc\ iso _TestSha1GitObject is UnitTest + """ + SHA-1 of a git blob object. Verified with: + echo -n hello | git hash-object --stdin + printf 'blob 5\0hello' | sha1sum + """ + fun name(): String => "sha1/git-object" + + fun apply(h: TestHelper) => + let expected = "b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0" + h.assert_eq[String val](expected, GitSha1.hex("blob 5\0hello")) + + // Same result via from_chunks + let chunks: Array[ByteSeq] val = ["blob 5\0"; "hello"] + h.assert_eq[String val](expected, GitSha1.hex_from_chunks(chunks)) + +class \nodoc\ iso _TestSha1FromChunksEquivalence is UnitTest + """from_chunks produces the same digest as apply on concatenated input.""" + fun name(): String => "sha1/from-chunks-equivalence" + + fun apply(h: TestHelper) => + let whole = GitSha1("blob 5\0hello") + let chunks: Array[ByteSeq] val = ["blob 5\0"; "hello"] + let chunked = GitSha1.from_chunks(chunks) + + h.assert_eq[USize](whole.size(), chunked.size()) + var i: USize = 0 + while i < whole.size() do + try + h.assert_eq[U8](whole(i)?, chunked(i)?) + end + i = i + 1 + end + +class \nodoc\ iso _TestSha1HexFormat is UnitTest + """Hex output is exactly 40 characters, all lowercase hex.""" + fun name(): String => "sha1/hex-format" + + fun apply(h: TestHelper) => + let hex_str = GitSha1.hex("test") + h.assert_eq[USize](40, hex_str.size()) + for c in hex_str.values() do + let valid = + ((c >= '0') and (c <= '9')) or ((c >= 'a') and (c <= 'f')) + h.assert_true(valid, "character '" + String.from_array([c]) + "' is not lowercase hex") + end diff --git a/corral/git/sha1/sha1.pony b/corral/git/sha1/sha1.pony new file mode 100644 index 0000000..53cf508 --- /dev/null +++ b/corral/git/sha1/sha1.pony @@ -0,0 +1,52 @@ +""" +SHA-1 hashing for git object verification. Wraps ssl's Digest to isolate the +ssl dependency to this single package. + +Git uses SHA-1 for object IDs: the SHA-1 hash of the object's header and +content produces the 40-character hex object ID. Use `GitSha1` for single-buffer +hashing and `GitSha1.from_chunks` when header and content are separate (avoiding +concatenation). +""" + +use crypto = "ssl/crypto" + +primitive GitSha1 + """ + Computes SHA-1 digests for git object verification. Wraps ssl's Digest + to isolate the ssl dependency to this single package. + + Git uses SHA-1 for object IDs: the SHA-1 hash of the object's header + and content produces the 40-character hex object ID. This primitive + provides both raw bytes and hex string forms. + """ + + fun apply(data: ByteSeq): Array[U8] val => + """ + Returns the 20-byte SHA-1 digest of the input. + """ + crypto.SHA1(data) + + fun hex(data: ByteSeq): String val => + """ + Returns the 40-character lowercase hex SHA-1 digest of the input. + """ + crypto.ToHexString(crypto.SHA1(data)) + + fun from_chunks(chunks: ReadSeq[ByteSeq] val): Array[U8] val => + """ + Returns the 20-byte SHA-1 digest of the concatenated chunks. + Useful for hashing git objects where header and content are separate + (e.g., ["blob 5\0", content]). + """ + let d = crypto.Digest.sha1() + for chunk in chunks.values() do + try d.append(chunk)? else _Unreachable() end + end + d.final() + + fun hex_from_chunks(chunks: ReadSeq[ByteSeq] val): String val => + """ + Returns the 40-character lowercase hex SHA-1 digest of the + concatenated chunks. + """ + crypto.ToHexString(from_chunks(chunks)) diff --git a/corral/test/_test.pony b/corral/test/_test.pony index 42c6e3e..41f6104 100644 --- a/corral/test/_test.pony +++ b/corral/test/_test.pony @@ -2,6 +2,7 @@ use "pony_test" use "files" use integration = "integration" use cmd = "../cmd" +use git = "../git" actor \nodoc\ Main is TestList new create(env: Env) => @@ -36,6 +37,7 @@ actor \nodoc\ Main is TestList test(integration.TestUpdateScripts) cmd.Main.make().tests(test) + git.Main.make().tests(test) fun @runtime_override_defaults(rto: RuntimeOptions) => rto.ponynoblock = true diff --git a/make.ps1 b/make.ps1 index f222252..5ec2262 100644 --- a/make.ps1 +++ b/make.ps1 @@ -36,6 +36,8 @@ $ErrorActionPreference = "Stop" $rootDir = Split-Path $script:MyInvocation.MyCommand.Path $srcDir = Join-Path -Path $rootDir -ChildPath "corral" +$ponyArgs = @("--define", "libressl", "--path", "corral\_vendor", "--path", ".") + if ($Config -ieq "Release") { $configFlag = "" @@ -95,7 +97,7 @@ function BuildCorral { if ($binaryTimestamp -lt $file.LastWriteTimeUtc) { - ponyc "$configFlag" --cpu "$CPU" --output "$buildDir" "$srcDir" + ponyc "$configFlag" $ponyArgs --cpu "$CPU" --output "$buildDir" "$srcDir" break buildFiles } } @@ -115,8 +117,8 @@ function BuildTest if ($testTimestamp -lt $file.LastWriteTimeUtc) { $testDir = Join-Path -Path $srcDir -ChildPath "test" - Write-Output "ponyc `"$configFlag`" --cpu `"$CPU`" --output `"$buildDir`" --bin-name `"test`" `"$testDir`"" - ponyc "$configFlag" --cpu "$CPU" --output "$buildDir" --bin-name test "$testDir" + Write-Output "ponyc `"$configFlag`" $ponyArgs --cpu `"$CPU`" --output `"$buildDir`" --bin-name `"test`" `"$testDir`"" + ponyc "$configFlag" $ponyArgs --cpu "$CPU" --output "$buildDir" --bin-name test "$testDir" break testFiles } }