├── .cargo └── config.toml ├── .github ├── dependabot.yml ├── update-ada.sh └── workflows │ ├── ci.yml │ ├── release.yml │ └── scorecard.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bench └── parse.rs ├── build.rs ├── deps ├── ada.cpp ├── ada.h ├── ada_c.h └── wasi_to_unknown.cpp ├── examples └── simple.rs ├── justfile ├── rust-toolchain.toml ├── scripts └── wasmtime-wrapper.sh └── src ├── ffi.rs ├── idna.rs ├── lib.rs └── url_search_params.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-wasip1] 2 | runner = ["./scripts/wasmtime-wrapper.sh"] 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: monthly 12 | -------------------------------------------------------------------------------- /.github/update-ada.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | BASE_DIR=$(pwd) 5 | DEPENDENCIES_DIR="$BASE_DIR/deps" 6 | 7 | WORKSPACE=$(mktemp -d 2> /dev/null || mktemp -d -t 'tmp') 8 | 9 | cleanup () { 10 | EXIT_CODE=$? 11 | [ -d "$WORKSPACE" ] && rm -rf "$WORKSPACE" 12 | exit $EXIT_CODE 13 | } 14 | 15 | trap cleanup INT TERM EXIT 16 | 17 | cd "$WORKSPACE" 18 | curl -sL -o "ada" "https://github.com/ada-url/ada/releases/latest/download/singleheader.zip" 19 | unzip ada 20 | rm ada 21 | cp * "$DEPENDENCIES_DIR" 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | paths-ignore: 7 | - '**/*.md' 8 | push: 9 | branches: 10 | - main 11 | paths-ignore: 12 | - '**/*.md' 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: ${{ github.ref_name != 'main' }} 17 | 18 | jobs: 19 | test: 20 | name: Check & Test 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | include: 25 | - os: windows-latest 26 | - os: ubuntu-latest 27 | - os: macos-latest 28 | - os: ubuntu-latest 29 | env: 30 | CARGO_BUILD_TARGET: wasm32-wasip1 31 | CARGO_TARGET_WASM32_WASI_RUNNER: /home/runner/.wasmtime/bin/wasmtime --dir=. 32 | runs-on: ${{ matrix.os }} 33 | env: ${{ matrix.env || fromJSON('{}') }} 34 | steps: 35 | - uses: actions/checkout@v3 36 | 37 | - name: Install Wasm deps 38 | if: matrix.env.CARGO_BUILD_TARGET == 'wasm32-wasip1' 39 | run: | 40 | rustup target add wasm32-wasip1 41 | curl -LO https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.deb 42 | sudo dpkg --install wasi-sdk-25.0-x86_64-linux.deb 43 | curl -LO https://github.com/bytecodealliance/wasmtime/releases/download/v13.0.0/wasmtime-v13.0.0-x86_64-linux.tar.xz 44 | tar xvf wasmtime-v13.0.0-x86_64-linux.tar.xz 45 | echo `pwd`/wasmtime-v13.0.0-x86_64-linux >> $GITHUB_PATH 46 | 47 | - uses: Swatinem/rust-cache@v2 48 | with: 49 | shared-key: ci 50 | save-if: ${{ github.ref_name == 'main' }} 51 | 52 | - run: rustup show 53 | 54 | - name: Install cargo-hack 55 | uses: taiki-e/install-action@cargo-hack 56 | 57 | - name: Clippy 58 | run: cargo hack clippy --feature-powerset -- -D warnings 59 | 60 | - name: Test 61 | run: cargo hack test --feature-powerset 62 | 63 | - name: Check Documentation 64 | env: 65 | RUSTDOCFLAGS: '-D warnings' 66 | run: cargo hack doc --feature-powerset 67 | 68 | format: 69 | name: Format 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v3 73 | 74 | - run: rustup show 75 | 76 | - run: cargo fmt --all -- --check 77 | 78 | lint: 79 | name: Clippy 80 | runs-on: ubuntu-latest 81 | steps: 82 | - uses: actions/checkout@v3 83 | 84 | - uses: Swatinem/rust-cache@v2 85 | with: 86 | shared-key: ci 87 | save-if: false 88 | 89 | - run: rustup show 90 | 91 | - run: cargo clippy -- -D warnings 92 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | publish-tag: 7 | description: 'The tag of the version to publish' 8 | required: true 9 | type: string 10 | 11 | concurrency: 12 | group: release 13 | 14 | env: 15 | RUST_BACKTRACE: 1 16 | CARGO_TERM_COLOR: always 17 | 18 | jobs: 19 | test-release: 20 | name: Check & Test release 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | include: 25 | - os: windows-latest 26 | - os: macos-latest 27 | - os: ubuntu-latest 28 | env: 29 | CARGO_BUILD_TARGET: wasm32-wasip1 30 | CARGO_TARGET_WASM32_WASI_RUNNER: /home/runner/.wasmtime/bin/wasmtime --dir=. 31 | runs-on: ${{ matrix.os }} 32 | if: github.ref == 'refs/heads/main' 33 | env: ${{ matrix.env || fromJSON('{}') }} 34 | steps: 35 | - uses: actions/checkout@v4 36 | with: 37 | persist-credentials: false 38 | 39 | - name: Install Wasm deps 40 | if: matrix.env.CARGO_BUILD_TARGET == 'wasm32-wasip1' 41 | run: | 42 | rustup target add wasm32-wasip1 43 | curl -LO https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.deb 44 | sudo dpkg --install wasi-sdk-25.0-x86_64-linux.deb 45 | curl -LO https://github.com/bytecodealliance/wasmtime/releases/download/v13.0.0/wasmtime-v13.0.0-x86_64-linux.tar.xz 46 | tar xvf wasmtime-v13.0.0-x86_64-linux.tar.xz 47 | echo `pwd`/wasmtime-v13.0.0-x86_64-linux >> $GITHUB_PATH 48 | 49 | - uses: Swatinem/rust-cache@v2 50 | with: 51 | shared-key: release 52 | save-if: ${{ github.ref_name == 'main' }} 53 | 54 | - run: rustup show 55 | 56 | - name: Install cargo-hack 57 | uses: taiki-e/install-action@cargo-hack 58 | 59 | - name: Clippy 60 | run: cargo hack clippy --feature-powerset -- -D warnings 61 | 62 | - name: Test 63 | run: cargo hack test --feature-powerset 64 | 65 | - name: Check Documentation 66 | env: 67 | RUSTDOCFLAGS: '-D warnings' 68 | run: cargo hack doc --feature-powerset 69 | 70 | - name: Check semver 71 | if: matrix.os == 'ubuntu-latest' 72 | uses: obi1kenobi/cargo-semver-checks-action@v2 73 | 74 | publish-release: 75 | name: Publish release 76 | needs: test-release 77 | runs-on: ubuntu-latest 78 | if: github.ref == 'refs/heads/main' 79 | steps: 80 | - name: Checkout 81 | uses: actions/checkout@v4 82 | with: 83 | persist-credentials: true 84 | 85 | - uses: taiki-e/install-action@v2 86 | with: 87 | tool: cargo-edit 88 | 89 | - name: Update Cargo.toml version 90 | env: 91 | NEW_VERSION: ${{ inputs.publish-tag }} 92 | run: | 93 | VERSION=${NEW_VERSION#v} 94 | cargo set-version "${VERSION}" 95 | 96 | git add Cargo.toml 97 | git commit -m "chore: bump version to ${NEW_VERSION}" 98 | git push 99 | 100 | - name: Tag the version 101 | env: 102 | GIT_TAG: ${{ inputs.publish-tag }} 103 | run: |+ 104 | git tag "${GIT_TAG}" 105 | git push origin "${GIT_TAG}" 106 | 107 | - name: Publish to crates.io 108 | run: cargo publish 109 | env: 110 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 111 | 112 | - name: Create github release 113 | uses: taiki-e/create-gh-release-action@v1 114 | with: 115 | branch: main 116 | ref: refs/tags/"${GIT_TAG}" 117 | env: 118 | GIT_TAG: ${{ inputs.publish-tag }} 119 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 120 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '0 0 * * 1' 14 | 15 | # Declare default permissions as read only. 16 | permissions: read-all 17 | 18 | jobs: 19 | analysis: 20 | name: Scorecard analysis 21 | runs-on: ubuntu-latest 22 | permissions: 23 | # Needed to upload the results to code-scanning dashboard. 24 | security-events: write 25 | # Needed to publish results and get a badge (see publish_results below). 26 | id-token: write 27 | # Uncomment the permissions below if installing in a private repository. 28 | # contents: read 29 | # actions: read 30 | 31 | steps: 32 | - name: "Checkout code" 33 | uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 34 | with: 35 | persist-credentials: false 36 | 37 | - name: "Run analysis" 38 | uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # v2.1.3 39 | with: 40 | results_file: results.sarif 41 | results_format: sarif 42 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 43 | # - you want to enable the Branch-Protection check on a *public* repository, or 44 | # - you are installing Scorecard on a *private* repository 45 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 46 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 47 | 48 | # Public repositories: 49 | # - Publish results to OpenSSF REST API for easy access by consumers 50 | # - Allows the repository to include the Scorecard badge. 51 | # - See https://github.com/ossf/scorecard-action#publishing-results. 52 | # For private repositories: 53 | # - `publish_results` will always be set to `false`, regardless 54 | # of the value entered here. 55 | publish_results: true 56 | 57 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 58 | # format to the repository Actions tab. 59 | - name: "Upload artifact" 60 | uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 61 | with: 62 | name: SARIF file 63 | path: results.sarif 64 | retention-days: 5 65 | 66 | # Upload the results to GitHub's code scanning dashboard. 67 | - name: "Upload to code-scanning" 68 | uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 69 | with: 70 | sarif_file: results.sarif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ada-url" 7 | version = "3.2.4" 8 | dependencies = [ 9 | "cc", 10 | "criterion", 11 | "derive_more", 12 | "link_args", 13 | "regex", 14 | "serde", 15 | "serde_json", 16 | "url", 17 | ] 18 | 19 | [[package]] 20 | name = "aho-corasick" 21 | version = "1.1.3" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 24 | dependencies = [ 25 | "memchr", 26 | ] 27 | 28 | [[package]] 29 | name = "anes" 30 | version = "0.1.6" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 33 | 34 | [[package]] 35 | name = "anstyle" 36 | version = "1.0.10" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 39 | 40 | [[package]] 41 | name = "autocfg" 42 | version = "1.4.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 45 | 46 | [[package]] 47 | name = "bitflags" 48 | version = "2.9.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 51 | 52 | [[package]] 53 | name = "cast" 54 | version = "0.3.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 57 | 58 | [[package]] 59 | name = "cc" 60 | version = "1.2.20" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" 63 | dependencies = [ 64 | "jobserver", 65 | "libc", 66 | "shlex", 67 | ] 68 | 69 | [[package]] 70 | name = "cfg-if" 71 | version = "1.0.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 74 | 75 | [[package]] 76 | name = "ciborium" 77 | version = "0.2.2" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 80 | dependencies = [ 81 | "ciborium-io", 82 | "ciborium-ll", 83 | "serde", 84 | ] 85 | 86 | [[package]] 87 | name = "ciborium-io" 88 | version = "0.2.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 91 | 92 | [[package]] 93 | name = "ciborium-ll" 94 | version = "0.2.2" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 97 | dependencies = [ 98 | "ciborium-io", 99 | "half", 100 | ] 101 | 102 | [[package]] 103 | name = "clap" 104 | version = "4.5.37" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 107 | dependencies = [ 108 | "clap_builder", 109 | ] 110 | 111 | [[package]] 112 | name = "clap_builder" 113 | version = "4.5.37" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 116 | dependencies = [ 117 | "anstyle", 118 | "clap_lex", 119 | ] 120 | 121 | [[package]] 122 | name = "clap_lex" 123 | version = "0.7.4" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 126 | 127 | [[package]] 128 | name = "convert_case" 129 | version = "0.6.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" 132 | dependencies = [ 133 | "unicode-segmentation", 134 | ] 135 | 136 | [[package]] 137 | name = "criterion" 138 | version = "0.5.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" 141 | dependencies = [ 142 | "anes", 143 | "cast", 144 | "ciborium", 145 | "clap", 146 | "criterion-plot", 147 | "is-terminal", 148 | "itertools", 149 | "num-traits", 150 | "once_cell", 151 | "oorandom", 152 | "regex", 153 | "serde", 154 | "serde_derive", 155 | "serde_json", 156 | "tinytemplate", 157 | "walkdir", 158 | ] 159 | 160 | [[package]] 161 | name = "criterion-plot" 162 | version = "0.5.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 165 | dependencies = [ 166 | "cast", 167 | "itertools", 168 | ] 169 | 170 | [[package]] 171 | name = "crunchy" 172 | version = "0.2.3" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" 175 | 176 | [[package]] 177 | name = "derive_more" 178 | version = "1.0.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" 181 | dependencies = [ 182 | "derive_more-impl", 183 | ] 184 | 185 | [[package]] 186 | name = "derive_more-impl" 187 | version = "1.0.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" 190 | dependencies = [ 191 | "convert_case", 192 | "proc-macro2", 193 | "quote", 194 | "syn", 195 | "unicode-xid", 196 | ] 197 | 198 | [[package]] 199 | name = "displaydoc" 200 | version = "0.2.5" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 203 | dependencies = [ 204 | "proc-macro2", 205 | "quote", 206 | "syn", 207 | ] 208 | 209 | [[package]] 210 | name = "either" 211 | version = "1.15.0" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 214 | 215 | [[package]] 216 | name = "form_urlencoded" 217 | version = "1.2.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 220 | dependencies = [ 221 | "percent-encoding", 222 | ] 223 | 224 | [[package]] 225 | name = "getrandom" 226 | version = "0.3.2" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 229 | dependencies = [ 230 | "cfg-if", 231 | "libc", 232 | "r-efi", 233 | "wasi", 234 | ] 235 | 236 | [[package]] 237 | name = "half" 238 | version = "2.6.0" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 241 | dependencies = [ 242 | "cfg-if", 243 | "crunchy", 244 | ] 245 | 246 | [[package]] 247 | name = "hermit-abi" 248 | version = "0.5.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" 251 | 252 | [[package]] 253 | name = "icu_collections" 254 | version = "1.5.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 257 | dependencies = [ 258 | "displaydoc", 259 | "yoke", 260 | "zerofrom", 261 | "zerovec", 262 | ] 263 | 264 | [[package]] 265 | name = "icu_locid" 266 | version = "1.5.0" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 269 | dependencies = [ 270 | "displaydoc", 271 | "litemap", 272 | "tinystr", 273 | "writeable", 274 | "zerovec", 275 | ] 276 | 277 | [[package]] 278 | name = "icu_locid_transform" 279 | version = "1.5.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 282 | dependencies = [ 283 | "displaydoc", 284 | "icu_locid", 285 | "icu_locid_transform_data", 286 | "icu_provider", 287 | "tinystr", 288 | "zerovec", 289 | ] 290 | 291 | [[package]] 292 | name = "icu_locid_transform_data" 293 | version = "1.5.1" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" 296 | 297 | [[package]] 298 | name = "icu_normalizer" 299 | version = "1.5.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 302 | dependencies = [ 303 | "displaydoc", 304 | "icu_collections", 305 | "icu_normalizer_data", 306 | "icu_properties", 307 | "icu_provider", 308 | "smallvec", 309 | "utf16_iter", 310 | "utf8_iter", 311 | "write16", 312 | "zerovec", 313 | ] 314 | 315 | [[package]] 316 | name = "icu_normalizer_data" 317 | version = "1.5.1" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" 320 | 321 | [[package]] 322 | name = "icu_properties" 323 | version = "1.5.1" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 326 | dependencies = [ 327 | "displaydoc", 328 | "icu_collections", 329 | "icu_locid_transform", 330 | "icu_properties_data", 331 | "icu_provider", 332 | "tinystr", 333 | "zerovec", 334 | ] 335 | 336 | [[package]] 337 | name = "icu_properties_data" 338 | version = "1.5.1" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" 341 | 342 | [[package]] 343 | name = "icu_provider" 344 | version = "1.5.0" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 347 | dependencies = [ 348 | "displaydoc", 349 | "icu_locid", 350 | "icu_provider_macros", 351 | "stable_deref_trait", 352 | "tinystr", 353 | "writeable", 354 | "yoke", 355 | "zerofrom", 356 | "zerovec", 357 | ] 358 | 359 | [[package]] 360 | name = "icu_provider_macros" 361 | version = "1.5.0" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 364 | dependencies = [ 365 | "proc-macro2", 366 | "quote", 367 | "syn", 368 | ] 369 | 370 | [[package]] 371 | name = "idna" 372 | version = "1.0.3" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 375 | dependencies = [ 376 | "idna_adapter", 377 | "smallvec", 378 | "utf8_iter", 379 | ] 380 | 381 | [[package]] 382 | name = "idna_adapter" 383 | version = "1.2.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 386 | dependencies = [ 387 | "icu_normalizer", 388 | "icu_properties", 389 | ] 390 | 391 | [[package]] 392 | name = "is-terminal" 393 | version = "0.4.16" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" 396 | dependencies = [ 397 | "hermit-abi", 398 | "libc", 399 | "windows-sys", 400 | ] 401 | 402 | [[package]] 403 | name = "itertools" 404 | version = "0.10.5" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 407 | dependencies = [ 408 | "either", 409 | ] 410 | 411 | [[package]] 412 | name = "itoa" 413 | version = "1.0.15" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 416 | 417 | [[package]] 418 | name = "jobserver" 419 | version = "0.1.33" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 422 | dependencies = [ 423 | "getrandom", 424 | "libc", 425 | ] 426 | 427 | [[package]] 428 | name = "libc" 429 | version = "0.2.172" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 432 | 433 | [[package]] 434 | name = "link_args" 435 | version = "0.6.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "2c7721e472624c9aaad27a5eb6b7c9c6045c7a396f2efb6dabaec1b640d5e89b" 438 | 439 | [[package]] 440 | name = "litemap" 441 | version = "0.7.5" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 444 | 445 | [[package]] 446 | name = "memchr" 447 | version = "2.7.4" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 450 | 451 | [[package]] 452 | name = "num-traits" 453 | version = "0.2.19" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 456 | dependencies = [ 457 | "autocfg", 458 | ] 459 | 460 | [[package]] 461 | name = "once_cell" 462 | version = "1.21.3" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 465 | 466 | [[package]] 467 | name = "oorandom" 468 | version = "11.1.5" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" 471 | 472 | [[package]] 473 | name = "percent-encoding" 474 | version = "2.3.1" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 477 | 478 | [[package]] 479 | name = "proc-macro2" 480 | version = "1.0.95" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 483 | dependencies = [ 484 | "unicode-ident", 485 | ] 486 | 487 | [[package]] 488 | name = "quote" 489 | version = "1.0.40" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 492 | dependencies = [ 493 | "proc-macro2", 494 | ] 495 | 496 | [[package]] 497 | name = "r-efi" 498 | version = "5.2.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 501 | 502 | [[package]] 503 | name = "regex" 504 | version = "1.11.1" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 507 | dependencies = [ 508 | "aho-corasick", 509 | "memchr", 510 | "regex-automata", 511 | "regex-syntax", 512 | ] 513 | 514 | [[package]] 515 | name = "regex-automata" 516 | version = "0.4.9" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 519 | dependencies = [ 520 | "aho-corasick", 521 | "memchr", 522 | "regex-syntax", 523 | ] 524 | 525 | [[package]] 526 | name = "regex-syntax" 527 | version = "0.8.5" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 530 | 531 | [[package]] 532 | name = "ryu" 533 | version = "1.0.20" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 536 | 537 | [[package]] 538 | name = "same-file" 539 | version = "1.0.6" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 542 | dependencies = [ 543 | "winapi-util", 544 | ] 545 | 546 | [[package]] 547 | name = "serde" 548 | version = "1.0.219" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 551 | dependencies = [ 552 | "serde_derive", 553 | ] 554 | 555 | [[package]] 556 | name = "serde_derive" 557 | version = "1.0.219" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 560 | dependencies = [ 561 | "proc-macro2", 562 | "quote", 563 | "syn", 564 | ] 565 | 566 | [[package]] 567 | name = "serde_json" 568 | version = "1.0.140" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 571 | dependencies = [ 572 | "itoa", 573 | "memchr", 574 | "ryu", 575 | "serde", 576 | ] 577 | 578 | [[package]] 579 | name = "shlex" 580 | version = "1.3.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 583 | 584 | [[package]] 585 | name = "smallvec" 586 | version = "1.15.0" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 589 | 590 | [[package]] 591 | name = "stable_deref_trait" 592 | version = "1.2.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 595 | 596 | [[package]] 597 | name = "syn" 598 | version = "2.0.101" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 601 | dependencies = [ 602 | "proc-macro2", 603 | "quote", 604 | "unicode-ident", 605 | ] 606 | 607 | [[package]] 608 | name = "synstructure" 609 | version = "0.13.1" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 612 | dependencies = [ 613 | "proc-macro2", 614 | "quote", 615 | "syn", 616 | ] 617 | 618 | [[package]] 619 | name = "tinystr" 620 | version = "0.7.6" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 623 | dependencies = [ 624 | "displaydoc", 625 | "zerovec", 626 | ] 627 | 628 | [[package]] 629 | name = "tinytemplate" 630 | version = "1.2.1" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 633 | dependencies = [ 634 | "serde", 635 | "serde_json", 636 | ] 637 | 638 | [[package]] 639 | name = "unicode-ident" 640 | version = "1.0.18" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 643 | 644 | [[package]] 645 | name = "unicode-segmentation" 646 | version = "1.12.0" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 649 | 650 | [[package]] 651 | name = "unicode-xid" 652 | version = "0.2.6" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 655 | 656 | [[package]] 657 | name = "url" 658 | version = "2.5.4" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 661 | dependencies = [ 662 | "form_urlencoded", 663 | "idna", 664 | "percent-encoding", 665 | ] 666 | 667 | [[package]] 668 | name = "utf16_iter" 669 | version = "1.0.5" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 672 | 673 | [[package]] 674 | name = "utf8_iter" 675 | version = "1.0.4" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 678 | 679 | [[package]] 680 | name = "walkdir" 681 | version = "2.5.0" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 684 | dependencies = [ 685 | "same-file", 686 | "winapi-util", 687 | ] 688 | 689 | [[package]] 690 | name = "wasi" 691 | version = "0.14.2+wasi-0.2.4" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 694 | dependencies = [ 695 | "wit-bindgen-rt", 696 | ] 697 | 698 | [[package]] 699 | name = "winapi-util" 700 | version = "0.1.9" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 703 | dependencies = [ 704 | "windows-sys", 705 | ] 706 | 707 | [[package]] 708 | name = "windows-sys" 709 | version = "0.59.0" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 712 | dependencies = [ 713 | "windows-targets", 714 | ] 715 | 716 | [[package]] 717 | name = "windows-targets" 718 | version = "0.52.6" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 721 | dependencies = [ 722 | "windows_aarch64_gnullvm", 723 | "windows_aarch64_msvc", 724 | "windows_i686_gnu", 725 | "windows_i686_gnullvm", 726 | "windows_i686_msvc", 727 | "windows_x86_64_gnu", 728 | "windows_x86_64_gnullvm", 729 | "windows_x86_64_msvc", 730 | ] 731 | 732 | [[package]] 733 | name = "windows_aarch64_gnullvm" 734 | version = "0.52.6" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 737 | 738 | [[package]] 739 | name = "windows_aarch64_msvc" 740 | version = "0.52.6" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 743 | 744 | [[package]] 745 | name = "windows_i686_gnu" 746 | version = "0.52.6" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 749 | 750 | [[package]] 751 | name = "windows_i686_gnullvm" 752 | version = "0.52.6" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 755 | 756 | [[package]] 757 | name = "windows_i686_msvc" 758 | version = "0.52.6" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 761 | 762 | [[package]] 763 | name = "windows_x86_64_gnu" 764 | version = "0.52.6" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 767 | 768 | [[package]] 769 | name = "windows_x86_64_gnullvm" 770 | version = "0.52.6" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 773 | 774 | [[package]] 775 | name = "windows_x86_64_msvc" 776 | version = "0.52.6" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 779 | 780 | [[package]] 781 | name = "wit-bindgen-rt" 782 | version = "0.39.0" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 785 | dependencies = [ 786 | "bitflags", 787 | ] 788 | 789 | [[package]] 790 | name = "write16" 791 | version = "1.0.0" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 794 | 795 | [[package]] 796 | name = "writeable" 797 | version = "0.5.5" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 800 | 801 | [[package]] 802 | name = "yoke" 803 | version = "0.7.5" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 806 | dependencies = [ 807 | "serde", 808 | "stable_deref_trait", 809 | "yoke-derive", 810 | "zerofrom", 811 | ] 812 | 813 | [[package]] 814 | name = "yoke-derive" 815 | version = "0.7.5" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 818 | dependencies = [ 819 | "proc-macro2", 820 | "quote", 821 | "syn", 822 | "synstructure", 823 | ] 824 | 825 | [[package]] 826 | name = "zerofrom" 827 | version = "0.1.6" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 830 | dependencies = [ 831 | "zerofrom-derive", 832 | ] 833 | 834 | [[package]] 835 | name = "zerofrom-derive" 836 | version = "0.1.6" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 839 | dependencies = [ 840 | "proc-macro2", 841 | "quote", 842 | "syn", 843 | "synstructure", 844 | ] 845 | 846 | [[package]] 847 | name = "zerovec" 848 | version = "0.10.4" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 851 | dependencies = [ 852 | "yoke", 853 | "zerofrom", 854 | "zerovec-derive", 855 | ] 856 | 857 | [[package]] 858 | name = "zerovec-derive" 859 | version = "0.10.3" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 862 | dependencies = [ 863 | "proc-macro2", 864 | "quote", 865 | "syn", 866 | ] 867 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ada-url" 3 | authors = [ 4 | "Yagiz Nizipli ", 5 | "Daniel Lemire ", 6 | "LongYinan ", 7 | "Boshen " 8 | ] 9 | version = "3.2.4" 10 | edition = "2024" 11 | description = "Fast WHATWG Compliant URL parser" 12 | documentation = "https://docs.rs/ada-url" 13 | readme = "README.md" 14 | keywords = ["url", "parser", "whatwg", "performance"] 15 | categories = ["parser-implementations", "web-programming", "encoding", "parsing", "no-std"] 16 | repository = "https://github.com/ada-url/rust" 17 | homepage = "https://ada-url.com" 18 | license = "MIT OR Apache-2.0" 19 | 20 | [[bench]] 21 | name = "parse" 22 | path = "bench/parse.rs" 23 | harness = false 24 | 25 | [features] 26 | default = ["std"] 27 | # pass `cpp_set_stdlib("c++")` to `cc` 28 | libcpp = [] 29 | # enables serde serialization/deserialization support 30 | serde = ["dep:serde", "std"] 31 | # enable allocations 32 | std = [] 33 | 34 | [dependencies] 35 | derive_more = { version = "1", features = ["full"] } 36 | serde = { version = "1", optional = true, features = ["derive"] } 37 | 38 | [dev-dependencies] 39 | criterion = { version = "0.5", default-features = false, features = ["cargo_bench_support"] } 40 | url = "2" # Used by benchmarks 41 | serde = { version = "1.0", features = ["derive"] } 42 | serde_json = "1.0" 43 | 44 | [build-dependencies] 45 | cc = { version = "1.1", features = ["parallel"] } 46 | link_args = "0.6" 47 | regex = { version = "1.11", features = [] } 48 | 49 | [package.metadata.docs.rs] 50 | features = ["serde"] 51 | 52 | [package.metadata.playground] 53 | features = ["serde"] 54 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Yagiz Nizipli and Daniel Lemire 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2023 Yagiz Nizipli and Daniel Lemire 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WHATWG URL parser for Rust 2 | 3 | Fast [WHATWG URL Specification](https://url.spec.whatwg.org) compliant URL parser for Rust. 4 | Well-tested and widely used by Node.js since [Node 18](https://nodejs.org/en/blog/release/v18.17.0). 5 | 6 | The Ada library passes the full range of tests from the specification, across a wide range of platforms (e.g., Windows, Linux, macOS). 7 | It fully supports the relevant [Unicode Technical Standard](https://www.unicode.org/reports/tr46/#ToUnicode). 8 | 9 | ## Usage 10 | 11 | See [here](examples/simple.rs) for a usage example. 12 | You can run it locally with `cargo run --example simple`. 13 | Feel free to adjust it for exploring this crate further. 14 | 15 | ### Features 16 | 17 | **std:** Functionalities that require `std`. 18 | This feature is enabled by default, set `no-default-features` to `true` if you want `no-std`. 19 | 20 | **serde:** Allow `Url` to work with `serde`. This feature is disabled by default. Enabling this feature without `std` would provide you only `Serialize`. 21 | Enabling this feature and `std` would provide you both `Serialize` and `Deserialize`. 22 | 23 | **libcpp:** Build `ada-url` with `libc++`. This feature is disabled by default. 24 | Enabling this feature without `libc++` installed would cause compile error. 25 | 26 | ### Performance 27 | 28 | Ada is fast. The benchmark below shows **3.49 times** faster URL parsing compared to `url` 29 | 30 | ```text 31 | can_parse/ada_url time: [1.2109 µs 1.2121 µs 1.2133 µs] 32 | thrpt: [635.09 MiB/s 635.75 MiB/s 636.38 MiB/s] 33 | 34 | parse/ada_url time: [2.0124 µs 2.0157 µs 2.0190 µs] 35 | thrpt: [381.67 MiB/s 382.28 MiB/s 382.91 MiB/s] 36 | 37 | parse/url time: [7.0530 µs 7.0597 µs 7.0666 µs] 38 | thrpt: [109.04 MiB/s 109.15 MiB/s 109.25 MiB/s] 39 | ``` 40 | 41 | ### Implemented traits 42 | 43 | `Url` implements the following traits. 44 | 45 | | Trait(s) | Description | 46 | |-------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 47 | | **[`Display`](https://doc.rust-lang.org/std/fmt/trait.Display.html)** | Provides `to_string` and allows for the value to be used in [format!](https://doc.rust-lang.org/std/fmt/fn.format.html) macros (e.g. `println!`). | 48 | | **[`Debug`](https://doc.rust-lang.org/std/fmt/trait.Debug.html)** | Allows debugger output in format macros, (`{:?}` syntax) | 49 | | **[`PartialEq`](https://doc.rust-lang.org/std/cmp/trait.PartialEq.html), [`Eq`](https://doc.rust-lang.org/std/cmp/trait.Eq.html)** | Allows for comparison, `url1 == url2`, `url1.eq(url2)` | 50 | | **[`PartialOrd`](https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html), [`Ord`](https://doc.rust-lang.org/std/cmp/trait.Ord.html)** | Allows for ordering `url1 < url2`, done so alphabetically. This is also allows `Url` to be used as a key in a [`BTreeMap`](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html) | 51 | | **[`Hash`](https://doc.rust-lang.org/std/hash/trait.Hash.html)** | Makes it so that `Url` can be hashed based on the string representation. This is important so that `Url` can be used as a key in a [`HashMap`](https://doc.rust-lang.org/std/collections/struct.HashMap.html) | 52 | | **[`FromStr`](https://doc.rust-lang.org/std/str/trait.FromStr.html)** | Allows for use with [`str`'s `parse` method](https://doc.rust-lang.org/std/primitive.str.html#method.parse) | 53 | | **[`TryFrom`, `TryFrom<&str>`](https://doc.rust-lang.org/std/convert/trait.TryFrom.html)** | Provides `try_into` methods for `String` and `&str` | 54 | | **[`Borrow`](https://doc.rust-lang.org/std/borrow/trait.Borrow.html), [`Borrow<[u8]>`](https://doc.rust-lang.org/std/borrow/trait.Borrow.html)** | Used in some crates so that the `Url` can be used as a key. | 55 | | **[`Deref`](https://doc.rust-lang.org/std/ops/trait.Deref.html)** | Allows for `&Url` to dereference as a `&str`. Also provides a [number of string methods](https://doc.rust-lang.org/std/string/struct.String.html#deref-methods-str) | 56 | | **[`AsRef<[u8]>`](https://doc.rust-lang.org/std/convert/trait.AsRef.html), [`AsRef`](https://doc.rust-lang.org/std/convert/trait.AsRef.html)** | Used to do a cheap reference-to-reference conversion. | 57 | | **[`Send`](https://doc.rust-lang.org/std/marker/trait.Send.html)** | Used to declare that the type can be transferred across thread boundaries. | 58 | | **[`Sync`](https://doc.rust-lang.org/stable/std/marker/trait.Sync.html)** | Used to declare that the type is thread-safe. | 59 | 60 | ## Development 61 | 62 | ### `justfile` 63 | 64 | The [`justfile`](./justfile) contains commands (called "recipes") that can be executed by [just](https://github.com/casey/just) for convenience. 65 | 66 | **Run all lints and tests:** 67 | 68 | ```sh 69 | just all 70 | ``` 71 | 72 | **Skipping features:** 73 | 74 | ```sh 75 | just all --skip=libcpp,serde 76 | ``` 77 | 78 | ## License 79 | 80 | This code is made available under the Apache License 2.0 as well as the MIT license. 81 | 82 | Our tests include third-party code and data. The benchmarking code includes third-party code: it is provided for research purposes only and not part of the library. 83 | -------------------------------------------------------------------------------- /bench/parse.rs: -------------------------------------------------------------------------------- 1 | use criterion::{Criterion, Throughput, black_box, criterion_group, criterion_main}; 2 | 3 | const URLS: &[&str] = &[ 4 | "https://www.google.com/webhp?hl=en&ictx=2&sa=X&ved=0ahUKEwil_oSxzJj8AhVtEFkFHTHnCGQQPQgI", 5 | "https://support.google.com/websearch/?p=ws_results_help&hl=en-CA&fg=1", 6 | "https://en.wikipedia.org/wiki/Dog#Roles_with_humans", 7 | "https://www.tiktok.com/@aguyandagolden/video/7133277734310038830", 8 | "https://business.twitter.com/en/help/troubleshooting/how-twitter-ads-work.html?ref=web-twc-ao-gbl-adsinfo&utm_source=twc&utm_medium=web&utm_campaign=ao&utm_content=adsinfo", 9 | "https://images-na.ssl-images-amazon.com/images/I/41Gc3C8UysL.css?AUIClients/AmazonGatewayAuiAssets", 10 | "https://www.reddit.com/?after=t3_zvz1ze", 11 | "https://www.reddit.com/login/?dest=https%3A%2F%2Fwww.reddit.com%2F", 12 | "postgresql://other:9818274x1!!@localhost:5432/otherdb?connect_timeout=10&application_name=myapp", 13 | "http://192.168.1.1", // ipv4 14 | "http://[2606:4700:4700::1111]", // ipv6 15 | ]; 16 | 17 | pub fn parse_benchmark(c: &mut Criterion) { 18 | let mut group = c.benchmark_group("parse"); 19 | group.throughput(Throughput::Bytes(URLS.iter().map(|u| u.len() as u64).sum())); 20 | group.bench_function("ada_url", |b| { 21 | b.iter(|| { 22 | URLS.iter().for_each(|url| { 23 | ada_url::Url::try_from(*black_box(url)).unwrap(); 24 | }) 25 | }) 26 | }); 27 | group.bench_function("url", |b| { 28 | b.iter(|| { 29 | URLS.iter().for_each(|url| { 30 | black_box(url).parse::().unwrap(); 31 | }) 32 | }) 33 | }); 34 | group.finish(); 35 | } 36 | 37 | pub fn can_parse_benchmark(c: &mut Criterion) { 38 | let mut group = c.benchmark_group("can_parse"); 39 | group.throughput(Throughput::Bytes(URLS.iter().map(|u| u.len() as u64).sum())); 40 | group.bench_function("ada_url", |b| { 41 | b.iter(|| { 42 | URLS.iter().for_each(|url| { 43 | let _ = ada_url::Url::can_parse(*black_box(url), None); 44 | }) 45 | }) 46 | }); 47 | // TODO: Add `url` crate when it supports can_parse function. 48 | group.finish(); 49 | } 50 | 51 | criterion_group!(benches, parse_benchmark, can_parse_benchmark); 52 | criterion_main!(benches); 53 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use std::fmt::{Display, Formatter}; 3 | use std::fs::File; 4 | use std::io::Read; 5 | use std::path::Path; 6 | use std::{env, fmt}; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct Target { 10 | pub architecture: String, 11 | pub vendor: String, 12 | pub system: Option, 13 | pub abi: Option, 14 | } 15 | 16 | impl Target { 17 | pub fn as_strs(&self) -> (&str, &str, Option<&str>, Option<&str>) { 18 | ( 19 | self.architecture.as_str(), 20 | self.vendor.as_str(), 21 | self.system.as_deref(), 22 | self.abi.as_deref(), 23 | ) 24 | } 25 | } 26 | 27 | impl Display for Target { 28 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 29 | write!(f, "{}-{}", &self.architecture, &self.vendor)?; 30 | 31 | if let Some(ref system) = self.system { 32 | write!(f, "-{}", system) 33 | } else { 34 | Ok(()) 35 | }?; 36 | 37 | if let Some(ref abi) = self.abi { 38 | write!(f, "-{}", abi) 39 | } else { 40 | Ok(()) 41 | } 42 | } 43 | } 44 | 45 | pub fn ndk() -> String { 46 | env::var("ANDROID_NDK").expect("ANDROID_NDK variable not set") 47 | } 48 | 49 | pub fn target_arch(arch: &str) -> &str { 50 | match arch { 51 | "armv7" => "arm", 52 | "aarch64" => "arm64", 53 | "i686" => "x86", 54 | arch => arch, 55 | } 56 | } 57 | 58 | fn host_tag() -> &'static str { 59 | // Because this is part of build.rs, the target_os is actually the host system 60 | if cfg!(target_os = "windows") { 61 | "windows-x86_64" 62 | } else if cfg!(target_os = "linux") { 63 | "linux-x86_64" 64 | } else if cfg!(target_os = "macos") { 65 | "darwin-x86_64" 66 | } else { 67 | panic!("host os is not supported") 68 | } 69 | } 70 | 71 | /// Get NDK major version from source.properties 72 | fn ndk_major_version(ndk_dir: &Path) -> u32 { 73 | // Capture version from the line with Pkg.Revision 74 | let re = Regex::new(r"Pkg.Revision = (\d+)\.(\d+)\.(\d+)").unwrap(); 75 | // There's a source.properties file in the ndk directory, which contains 76 | let mut source_properties = 77 | File::open(ndk_dir.join("source.properties")).expect("Couldn't open source.properties"); 78 | let mut buf = String::new(); 79 | source_properties 80 | .read_to_string(&mut buf) 81 | .expect("Could not read source.properties"); 82 | // Capture version info 83 | let captures = re 84 | .captures(&buf) 85 | .expect("source.properties did not match the regex"); 86 | // Capture 0 is the whole line of text 87 | captures[1].parse().expect("could not parse major version") 88 | } 89 | 90 | fn main() { 91 | let target_str = env::var("TARGET").unwrap(); 92 | let target: Vec = target_str.split('-').map(|s| s.into()).collect(); 93 | assert!(target.len() >= 2, "Failed to parse TARGET {}", target_str); 94 | 95 | let abi = if target.len() > 3 { 96 | Some(target[3].clone()) 97 | } else { 98 | None 99 | }; 100 | 101 | let system = if target.len() > 2 { 102 | Some(target[2].clone()) 103 | } else { 104 | None 105 | }; 106 | 107 | let target = Target { 108 | architecture: target[0].clone(), 109 | vendor: target[1].clone(), 110 | system, 111 | abi, 112 | }; 113 | 114 | let mut build = cc::Build::new(); 115 | build 116 | .file("./deps/ada.cpp") 117 | .include("./deps") 118 | .cpp(true) 119 | .std("c++20") 120 | .define("ADA_INCLUDE_URL_PATTERN", "0"); 121 | 122 | let compile_target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH"); 123 | let compile_target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS"); 124 | let compile_target_feature = env::var("CARGO_CFG_TARGET_FEATURE"); 125 | // Except for Emscripten target (which emulates POSIX environment), compile to Wasm via WASI SDK 126 | // which is currently the only standalone provider of stdlib for compilation of C/C++ libraries. 127 | 128 | match target.system.as_deref() { 129 | Some("android" | "androideabi") => { 130 | let ndk = ndk(); 131 | let major = ndk_major_version(Path::new(&ndk)); 132 | if major < 22 { 133 | build 134 | .flag(format!("--sysroot={}/sysroot", ndk)) 135 | .flag(format!( 136 | "-isystem{}/sources/cxx-stl/llvm-libc++/include", 137 | ndk 138 | )); 139 | } else { 140 | // NDK versions >= 22 have the sysroot in the llvm prebuilt by 141 | let host_toolchain = format!("{}/toolchains/llvm/prebuilt/{}", ndk, host_tag()); 142 | // sysroot is stored in the prebuilt llvm, under the host 143 | build.flag(format!("--sysroot={}/sysroot", host_toolchain)); 144 | } 145 | } 146 | _ => { 147 | if compile_target_arch.starts_with("wasm") && compile_target_os != "emscripten" { 148 | let wasi_sdk = env::var("WASI_SDK").unwrap_or_else(|_| "/opt/wasi-sdk".to_owned()); 149 | assert!( 150 | Path::new(&wasi_sdk).exists(), 151 | "WASI SDK not found at {wasi_sdk}" 152 | ); 153 | build.compiler(format!("{wasi_sdk}/bin/clang++")); 154 | let wasi_sysroot_lib = match compile_target_feature { 155 | Ok(compile_target_feature) if compile_target_feature.contains("atomics") => { 156 | "wasm32-wasip1-threads" 157 | } 158 | _ => "wasm32-wasip1", 159 | }; 160 | println!( 161 | "cargo:rustc-link-search={wasi_sdk}/share/wasi-sysroot/lib/{wasi_sysroot_lib}" 162 | ); 163 | // Wasm exceptions are new and not yet supported by WASI SDK. 164 | build.flag("-fno-exceptions"); 165 | // WASI SDK only has libc++ available. 166 | build.cpp_set_stdlib("c++"); 167 | // Explicitly link C++ ABI to avoid linking errors (it takes care of C++ -> C "lowering"). 168 | println!("cargo:rustc-link-lib=c++abi"); 169 | // Because Ada is a pure parsing library that doesn't need any OS syscalls, 170 | // it's also possible to compile it to wasm32-unknown-unknown. 171 | // This still requires WASI SDK for libc & libc++, but then we need a few hacks / overrides to get a pure Wasm w/o imports instead. 172 | if compile_target_os == "unknown" { 173 | build.target("wasm32-wasip1"); 174 | println!("cargo:rustc-link-lib=c"); 175 | build.file("./deps/wasi_to_unknown.cpp"); 176 | } 177 | } 178 | 179 | let compiler = build.get_compiler(); 180 | // Note: it's possible to use Clang++ explicitly on Windows as well, so this check 181 | // should be specifically for "is target compiler MSVC" and not "is target OS Windows". 182 | if compiler.is_like_msvc() { 183 | build.static_crt(true); 184 | link_args::windows! { 185 | unsafe { 186 | no_default_lib( 187 | "libcmt.lib", 188 | ); 189 | } 190 | } 191 | } else if compiler.is_like_clang() && cfg!(feature = "libcpp") { 192 | build.cpp_set_stdlib("c++"); 193 | } 194 | } 195 | } 196 | 197 | build.compile("ada"); 198 | } 199 | -------------------------------------------------------------------------------- /deps/ada_c.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ada_c.h 3 | * @brief Includes the C definitions for Ada. This is a C file, not C++. 4 | */ 5 | #ifndef ADA_C_H 6 | #define ADA_C_H 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | // This is a reference to ada::url_components::omitted 13 | // It represents "uint32_t(-1)" 14 | #define ada_url_omitted 0xffffffff 15 | 16 | // string that is owned by the ada_url instance 17 | typedef struct { 18 | const char* data; 19 | size_t length; 20 | } ada_string; 21 | 22 | // string that must be freed by the caller 23 | typedef struct { 24 | const char* data; 25 | size_t length; 26 | } ada_owned_string; 27 | 28 | typedef struct { 29 | uint32_t protocol_end; 30 | uint32_t username_end; 31 | uint32_t host_start; 32 | uint32_t host_end; 33 | uint32_t port; 34 | uint32_t pathname_start; 35 | uint32_t search_start; 36 | uint32_t hash_start; 37 | } ada_url_components; 38 | 39 | typedef void* ada_url; 40 | 41 | // input should be a null terminated C string (ASCII or UTF-8) 42 | // you must call ada_free on the returned pointer 43 | ada_url ada_parse(const char* input, size_t length); 44 | ada_url ada_parse_with_base(const char* input, size_t input_length, 45 | const char* base, size_t base_length); 46 | 47 | // input and base should be a null terminated C strings 48 | bool ada_can_parse(const char* input, size_t length); 49 | bool ada_can_parse_with_base(const char* input, size_t input_length, 50 | const char* base, size_t base_length); 51 | 52 | void ada_free(ada_url result); 53 | void ada_free_owned_string(ada_owned_string owned); 54 | ada_url ada_copy(ada_url input); 55 | 56 | bool ada_is_valid(ada_url result); 57 | 58 | // url_aggregator getters 59 | // if ada_is_valid(result)) is false, an empty string is returned 60 | ada_owned_string ada_get_origin(ada_url result); 61 | ada_string ada_get_href(ada_url result); 62 | ada_string ada_get_username(ada_url result); 63 | ada_string ada_get_password(ada_url result); 64 | ada_string ada_get_port(ada_url result); 65 | ada_string ada_get_hash(ada_url result); 66 | ada_string ada_get_host(ada_url result); 67 | ada_string ada_get_hostname(ada_url result); 68 | ada_string ada_get_pathname(ada_url result); 69 | ada_string ada_get_search(ada_url result); 70 | ada_string ada_get_protocol(ada_url result); 71 | uint8_t ada_get_host_type(ada_url result); 72 | uint8_t ada_get_scheme_type(ada_url result); 73 | 74 | // url_aggregator setters 75 | // if ada_is_valid(result)) is false, the setters have no effect 76 | // input should be a null terminated C string 77 | bool ada_set_href(ada_url result, const char* input, size_t length); 78 | bool ada_set_host(ada_url result, const char* input, size_t length); 79 | bool ada_set_hostname(ada_url result, const char* input, size_t length); 80 | bool ada_set_protocol(ada_url result, const char* input, size_t length); 81 | bool ada_set_username(ada_url result, const char* input, size_t length); 82 | bool ada_set_password(ada_url result, const char* input, size_t length); 83 | bool ada_set_port(ada_url result, const char* input, size_t length); 84 | bool ada_set_pathname(ada_url result, const char* input, size_t length); 85 | void ada_set_search(ada_url result, const char* input, size_t length); 86 | void ada_set_hash(ada_url result, const char* input, size_t length); 87 | 88 | // url_aggregator clear methods 89 | void ada_clear_port(ada_url result); 90 | void ada_clear_hash(ada_url result); 91 | void ada_clear_search(ada_url result); 92 | 93 | // url_aggregator functions 94 | // if ada_is_valid(result) is false, functions below will return false 95 | bool ada_has_credentials(ada_url result); 96 | bool ada_has_empty_hostname(ada_url result); 97 | bool ada_has_hostname(ada_url result); 98 | bool ada_has_non_empty_username(ada_url result); 99 | bool ada_has_non_empty_password(ada_url result); 100 | bool ada_has_port(ada_url result); 101 | bool ada_has_password(ada_url result); 102 | bool ada_has_hash(ada_url result); 103 | bool ada_has_search(ada_url result); 104 | 105 | // returns a pointer to the internal url_aggregator::url_components 106 | const ada_url_components* ada_get_components(ada_url result); 107 | 108 | // idna methods 109 | ada_owned_string ada_idna_to_unicode(const char* input, size_t length); 110 | ada_owned_string ada_idna_to_ascii(const char* input, size_t length); 111 | 112 | // url search params 113 | typedef void* ada_url_search_params; 114 | 115 | // Represents an std::vector 116 | typedef void* ada_strings; 117 | typedef void* ada_url_search_params_keys_iter; 118 | typedef void* ada_url_search_params_values_iter; 119 | 120 | typedef struct { 121 | ada_string key; 122 | ada_string value; 123 | } ada_string_pair; 124 | 125 | typedef void* ada_url_search_params_entries_iter; 126 | 127 | ada_url_search_params ada_parse_search_params(const char* input, size_t length); 128 | void ada_free_search_params(ada_url_search_params result); 129 | 130 | size_t ada_search_params_size(ada_url_search_params result); 131 | void ada_search_params_sort(ada_url_search_params result); 132 | ada_owned_string ada_search_params_to_string(ada_url_search_params result); 133 | 134 | void ada_search_params_append(ada_url_search_params result, const char* key, 135 | size_t key_length, const char* value, 136 | size_t value_length); 137 | void ada_search_params_set(ada_url_search_params result, const char* key, 138 | size_t key_length, const char* value, 139 | size_t value_length); 140 | void ada_search_params_remove(ada_url_search_params result, const char* key, 141 | size_t key_length); 142 | void ada_search_params_remove_value(ada_url_search_params result, 143 | const char* key, size_t key_length, 144 | const char* value, size_t value_length); 145 | bool ada_search_params_has(ada_url_search_params result, const char* key, 146 | size_t key_length); 147 | bool ada_search_params_has_value(ada_url_search_params result, const char* key, 148 | size_t key_length, const char* value, 149 | size_t value_length); 150 | ada_string ada_search_params_get(ada_url_search_params result, const char* key, 151 | size_t key_length); 152 | ada_strings ada_search_params_get_all(ada_url_search_params result, 153 | const char* key, size_t key_length); 154 | void ada_search_params_reset(ada_url_search_params result, const char* input, 155 | size_t length); 156 | ada_url_search_params_keys_iter ada_search_params_get_keys( 157 | ada_url_search_params result); 158 | ada_url_search_params_values_iter ada_search_params_get_values( 159 | ada_url_search_params result); 160 | ada_url_search_params_entries_iter ada_search_params_get_entries( 161 | ada_url_search_params result); 162 | 163 | void ada_free_strings(ada_strings result); 164 | size_t ada_strings_size(ada_strings result); 165 | ada_string ada_strings_get(ada_strings result, size_t index); 166 | 167 | void ada_free_search_params_keys_iter(ada_url_search_params_keys_iter result); 168 | ada_string ada_search_params_keys_iter_next( 169 | ada_url_search_params_keys_iter result); 170 | bool ada_search_params_keys_iter_has_next( 171 | ada_url_search_params_keys_iter result); 172 | 173 | void ada_free_search_params_values_iter( 174 | ada_url_search_params_values_iter result); 175 | ada_string ada_search_params_values_iter_next( 176 | ada_url_search_params_values_iter result); 177 | bool ada_search_params_values_iter_has_next( 178 | ada_url_search_params_values_iter result); 179 | 180 | void ada_free_search_params_entries_iter( 181 | ada_url_search_params_entries_iter result); 182 | ada_string_pair ada_search_params_entries_iter_next( 183 | ada_url_search_params_entries_iter result); 184 | bool ada_search_params_entries_iter_has_next( 185 | ada_url_search_params_entries_iter result); 186 | 187 | #endif // ADA_C_H 188 | -------------------------------------------------------------------------------- /deps/wasi_to_unknown.cpp: -------------------------------------------------------------------------------- 1 | // Some shims for WASI symbols used by the WASI libc environment initializer, 2 | // but not actually required by Ada. This allows to compile Ada Rust to 3 | // wasm32-unknown-unknown with WASI SDK. 4 | 5 | #include 6 | 7 | extern "C" { 8 | 9 | int32_t __imported_wasi_snapshot_preview1_environ_get(int32_t, int32_t) { 10 | __builtin_unreachable(); 11 | } 12 | 13 | int32_t __imported_wasi_snapshot_preview1_environ_sizes_get(int32_t, int32_t) { 14 | __builtin_unreachable(); 15 | } 16 | 17 | int32_t __imported_wasi_snapshot_preview1_fd_close(int32_t) { 18 | __builtin_unreachable(); 19 | } 20 | 21 | int32_t __imported_wasi_snapshot_preview1_fd_fdstat_get(int32_t, int32_t) { 22 | __builtin_unreachable(); 23 | } 24 | 25 | int32_t __imported_wasi_snapshot_preview1_fd_read(int32_t, 26 | int32_t, 27 | int32_t, 28 | int32_t) { 29 | __builtin_unreachable(); 30 | } 31 | 32 | int32_t __imported_wasi_snapshot_preview1_fd_seek(int32_t, 33 | int64_t, 34 | int32_t, 35 | int32_t) { 36 | __builtin_unreachable(); 37 | } 38 | 39 | int32_t __imported_wasi_snapshot_preview1_fd_write(int32_t, 40 | int32_t, 41 | int32_t, 42 | int32_t) { 43 | __builtin_unreachable(); 44 | } 45 | 46 | int32_t __imported_wasi_snapshot_preview1_sched_yield() { 47 | return 0; 48 | } 49 | 50 | _Noreturn void __imported_wasi_snapshot_preview1_proc_exit(int32_t) { 51 | __builtin_unreachable(); 52 | } 53 | 54 | } // extern "C" 55 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use ada_url::Url; 2 | 3 | fn main() { 4 | let url = Url::parse("http://www.google:8080/love#drug", None).expect("bad url"); 5 | 6 | println!("port: {:?}", url.port()); 7 | println!("hash: {:?}", url.hash()); 8 | println!("pathname: {:?}", url.pathname()); 9 | println!("href: {:?}", url.href()); 10 | 11 | let mut url = url; 12 | url.set_port(Some("9999")).expect("bad port"); 13 | println!("href: {:?}", url.href()); 14 | } 15 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | _default: 2 | @just --list --unsorted 3 | 4 | # run doc, clippy, and test recipies 5 | all *args: 6 | just fmt {{args}} 7 | just doc {{args}} 8 | just clippy {{args}} 9 | just test {{args}} 10 | 11 | # Format all code 12 | fmt *args: 13 | cargo fmt --all {{args}} 14 | 15 | # run tests on all feature combinations 16 | test *args: 17 | cargo hack test --feature-powerset {{args}} 18 | 19 | # type check and lint code on all feature combinations 20 | clippy *args: 21 | cargo hack clippy --feature-powerset {{args}} -- -D warnings 22 | 23 | # lint documentation on all feature combinations 24 | doc *args: 25 | RUSTDOCFLAGS='-D warnings' cargo hack doc --feature-powerset {{args}} 26 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.86.0" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /scripts/wasmtime-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | cd $SCRIPT_DIR/.. 4 | wasmtime run --max-wasm-stack=4194304 --env INSTA_WORKSPACE_ROOT=/ --mapdir "/::$(pwd)" -- "$@" 5 | -------------------------------------------------------------------------------- /src/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | use core::ffi::{c_char, c_uint}; 3 | 4 | #[cfg(feature = "std")] 5 | extern crate std; 6 | 7 | #[cfg(feature = "std")] 8 | use std::fmt::Display; 9 | 10 | #[repr(C)] 11 | pub struct ada_url { 12 | _unused: [u8; 0], 13 | _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, 14 | } 15 | 16 | #[repr(C)] 17 | pub struct ada_url_search_params { 18 | _unused: [u8; 0], 19 | _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, 20 | } 21 | 22 | #[repr(C)] 23 | pub struct ada_string { 24 | pub data: *const c_char, 25 | pub length: usize, 26 | } 27 | 28 | impl ada_string { 29 | #[must_use] 30 | pub const fn as_str(&self) -> &'static str { 31 | // We need to handle length 0 since data will be `nullptr` 32 | // Not handling will result in a panic due to core::slice::from_raw_parts 33 | // implementation 34 | if self.length == 0 { 35 | return ""; 36 | } 37 | unsafe { 38 | let slice = core::slice::from_raw_parts(self.data.cast(), self.length); 39 | core::str::from_utf8_unchecked(slice) 40 | } 41 | } 42 | } 43 | 44 | #[repr(C)] 45 | pub struct ada_owned_string { 46 | pub data: *const c_char, 47 | pub length: usize, 48 | } 49 | 50 | impl AsRef for ada_owned_string { 51 | fn as_ref(&self) -> &str { 52 | // We need to handle length 0 since data will be `nullptr` 53 | // Not handling will result in a panic due to core::slice::from_raw_parts 54 | // implementation 55 | if self.length == 0 { 56 | return ""; 57 | } 58 | unsafe { 59 | let slice = core::slice::from_raw_parts(self.data.cast(), self.length); 60 | core::str::from_utf8_unchecked(slice) 61 | } 62 | } 63 | } 64 | 65 | #[cfg(feature = "std")] 66 | impl Display for ada_owned_string { 67 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 68 | write!(f, "{}", self.as_ref().to_owned()) 69 | } 70 | } 71 | 72 | impl Drop for ada_owned_string { 73 | fn drop(&mut self) { 74 | // @note This is needed because ada_free_owned_string accepts by value 75 | let copy = ada_owned_string { 76 | data: self.data, 77 | length: self.length, 78 | }; 79 | unsafe { 80 | ada_free_owned_string(copy); 81 | }; 82 | } 83 | } 84 | 85 | /// Represents an std::vector 86 | #[repr(C)] 87 | pub struct ada_strings { 88 | _unused: [u8; 0], 89 | _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, 90 | } 91 | 92 | #[repr(C)] 93 | pub struct ada_url_search_params_keys_iter { 94 | _unused: [u8; 0], 95 | _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, 96 | } 97 | 98 | #[repr(C)] 99 | pub struct ada_url_search_params_values_iter { 100 | _unused: [u8; 0], 101 | _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, 102 | } 103 | 104 | #[repr(C)] 105 | pub struct ada_url_search_params_entries_iter { 106 | _unused: [u8; 0], 107 | _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, 108 | } 109 | 110 | /// Represents a key/value pair of strings 111 | #[repr(C)] 112 | pub struct ada_string_pair { 113 | pub key: ada_string, 114 | pub value: ada_string, 115 | } 116 | 117 | #[repr(C)] 118 | pub struct ada_url_components { 119 | pub protocol_end: u32, 120 | pub username_end: u32, 121 | pub host_start: u32, 122 | pub host_end: u32, 123 | pub port: u32, 124 | pub pathname_start: u32, 125 | pub search_start: u32, 126 | pub hash_start: u32, 127 | } 128 | 129 | unsafe extern "C" { 130 | pub fn ada_parse(input: *const c_char, length: usize) -> *mut ada_url; 131 | pub fn ada_parse_with_base( 132 | input: *const c_char, 133 | input_length: usize, 134 | base: *const c_char, 135 | base_length: usize, 136 | ) -> *mut ada_url; 137 | pub fn ada_free(url: *mut ada_url); 138 | pub fn ada_free_owned_string(url: ada_owned_string); 139 | pub fn ada_copy(url: *mut ada_url) -> *mut ada_url; 140 | pub fn ada_is_valid(url: *mut ada_url) -> bool; 141 | pub fn ada_can_parse(url: *const c_char, length: usize) -> bool; 142 | pub fn ada_can_parse_with_base( 143 | input: *const c_char, 144 | input_length: usize, 145 | base: *const c_char, 146 | base_length: usize, 147 | ) -> bool; 148 | pub fn ada_get_components(url: *mut ada_url) -> *mut ada_url_components; 149 | 150 | // Getters 151 | pub fn ada_get_origin(url: *mut ada_url) -> ada_owned_string; 152 | pub fn ada_get_href(url: *mut ada_url) -> ada_string; 153 | pub fn ada_get_username(url: *mut ada_url) -> ada_string; 154 | pub fn ada_get_password(url: *mut ada_url) -> ada_string; 155 | pub fn ada_get_port(url: *mut ada_url) -> ada_string; 156 | pub fn ada_get_hash(url: *mut ada_url) -> ada_string; 157 | pub fn ada_get_host(url: *mut ada_url) -> ada_string; 158 | pub fn ada_get_hostname(url: *mut ada_url) -> ada_string; 159 | pub fn ada_get_pathname(url: *mut ada_url) -> ada_string; 160 | pub fn ada_get_search(url: *mut ada_url) -> ada_string; 161 | pub fn ada_get_protocol(url: *mut ada_url) -> ada_string; 162 | pub fn ada_get_host_type(url: *mut ada_url) -> c_uint; 163 | pub fn ada_get_scheme_type(url: *mut ada_url) -> c_uint; 164 | 165 | // Setters 166 | pub fn ada_set_href(url: *mut ada_url, input: *const c_char, length: usize) -> bool; 167 | pub fn ada_set_username(url: *mut ada_url, input: *const c_char, length: usize) -> bool; 168 | pub fn ada_set_password(url: *mut ada_url, input: *const c_char, length: usize) -> bool; 169 | pub fn ada_set_port(url: *mut ada_url, input: *const c_char, length: usize) -> bool; 170 | pub fn ada_set_hash(url: *mut ada_url, input: *const c_char, length: usize); 171 | pub fn ada_set_host(url: *mut ada_url, input: *const c_char, length: usize) -> bool; 172 | pub fn ada_set_hostname(url: *mut ada_url, input: *const c_char, length: usize) -> bool; 173 | pub fn ada_set_pathname(url: *mut ada_url, input: *const c_char, length: usize) -> bool; 174 | pub fn ada_set_search(url: *mut ada_url, input: *const c_char, length: usize); 175 | pub fn ada_set_protocol(url: *mut ada_url, input: *const c_char, length: usize) -> bool; 176 | 177 | // Clear methods 178 | pub fn ada_clear_search(url: *mut ada_url); 179 | pub fn ada_clear_hash(url: *mut ada_url); 180 | pub fn ada_clear_port(url: *mut ada_url); 181 | 182 | // Validators 183 | pub fn ada_has_credentials(url: *mut ada_url) -> bool; 184 | pub fn ada_has_empty_hostname(url: *mut ada_url) -> bool; 185 | pub fn ada_has_hostname(url: *mut ada_url) -> bool; 186 | pub fn ada_has_non_empty_username(url: *mut ada_url) -> bool; 187 | pub fn ada_has_non_empty_password(url: *mut ada_url) -> bool; 188 | pub fn ada_has_port(url: *mut ada_url) -> bool; 189 | pub fn ada_has_password(url: *mut ada_url) -> bool; 190 | pub fn ada_has_hash(url: *mut ada_url) -> bool; 191 | pub fn ada_has_search(url: *mut ada_url) -> bool; 192 | 193 | // IDNA methods 194 | pub fn ada_idna_to_unicode(input: *const c_char, length: usize) -> ada_owned_string; 195 | pub fn ada_idna_to_ascii(input: *const c_char, length: usize) -> ada_owned_string; 196 | 197 | // URLSearchParams 198 | pub fn ada_parse_search_params( 199 | input: *const c_char, 200 | length: usize, 201 | ) -> *mut ada_url_search_params; 202 | pub fn ada_free_search_params(search_params: *mut ada_url_search_params); 203 | pub fn ada_search_params_size(search_params: *mut ada_url_search_params) -> usize; 204 | pub fn ada_search_params_sort(search_params: *mut ada_url_search_params); 205 | pub fn ada_search_params_to_string( 206 | search_params: *mut ada_url_search_params, 207 | ) -> ada_owned_string; 208 | pub fn ada_search_params_append( 209 | search_params: *mut ada_url_search_params, 210 | name: *const c_char, 211 | name_length: usize, 212 | value: *const c_char, 213 | value_length: usize, 214 | ); 215 | pub fn ada_search_params_set( 216 | search_params: *mut ada_url_search_params, 217 | name: *const c_char, 218 | name_length: usize, 219 | value: *const c_char, 220 | value_length: usize, 221 | ); 222 | pub fn ada_search_params_remove( 223 | search_params: *mut ada_url_search_params, 224 | name: *const c_char, 225 | name_length: usize, 226 | ); 227 | pub fn ada_search_params_remove_value( 228 | search_params: *mut ada_url_search_params, 229 | name: *const c_char, 230 | name_length: usize, 231 | value: *const c_char, 232 | value_length: usize, 233 | ); 234 | pub fn ada_search_params_has( 235 | search_params: *mut ada_url_search_params, 236 | name: *const c_char, 237 | name_length: usize, 238 | ) -> bool; 239 | pub fn ada_search_params_has_value( 240 | search_params: *mut ada_url_search_params, 241 | name: *const c_char, 242 | name_length: usize, 243 | value: *const c_char, 244 | value_length: usize, 245 | ) -> bool; 246 | pub fn ada_search_params_get( 247 | search_params: *mut ada_url_search_params, 248 | key: *const c_char, 249 | key_length: usize, 250 | ) -> ada_string; 251 | pub fn ada_search_params_get_all( 252 | // not implemented 253 | search_params: *mut ada_url_search_params, 254 | key: *const c_char, 255 | key_length: usize, 256 | ) -> *mut ada_strings; 257 | pub fn ada_search_params_get_keys( 258 | search_params: *mut ada_url_search_params, 259 | ) -> *mut ada_url_search_params_keys_iter; 260 | pub fn ada_search_params_get_values( 261 | search_params: *mut ada_url_search_params, 262 | ) -> *mut ada_url_search_params_values_iter; 263 | pub fn ada_search_params_get_entries( 264 | search_params: *mut ada_url_search_params, 265 | ) -> *mut ada_url_search_params_entries_iter; 266 | 267 | pub fn ada_free_strings(strings: *mut ada_strings); 268 | pub fn ada_strings_size(strings: *mut ada_strings) -> usize; 269 | pub fn ada_strings_get(strings: *mut ada_strings, index: usize) -> ada_string; 270 | pub fn ada_free_search_params_keys_iter(iter: *mut ada_url_search_params_keys_iter); 271 | pub fn ada_search_params_keys_iter_next( 272 | iter: *mut ada_url_search_params_keys_iter, 273 | ) -> ada_string; 274 | pub fn ada_search_params_keys_iter_has_next(iter: *mut ada_url_search_params_keys_iter) 275 | -> bool; 276 | 277 | pub fn ada_free_search_params_values_iter(iter: *mut ada_url_search_params_values_iter); 278 | pub fn ada_search_params_values_iter_next( 279 | iter: *mut ada_url_search_params_values_iter, 280 | ) -> ada_string; 281 | pub fn ada_search_params_values_iter_has_next( 282 | iter: *mut ada_url_search_params_values_iter, 283 | ) -> bool; 284 | 285 | pub fn ada_free_search_params_entries_iter(iter: *mut ada_url_search_params_entries_iter); 286 | pub fn ada_search_params_entries_iter_next( 287 | iter: *mut ada_url_search_params_entries_iter, 288 | ) -> ada_string_pair; 289 | pub fn ada_search_params_entries_iter_has_next( 290 | iter: *mut ada_url_search_params_entries_iter, 291 | ) -> bool; 292 | } 293 | 294 | #[cfg(test)] 295 | mod tests { 296 | use crate::ffi; 297 | 298 | #[test] 299 | fn ada_free_owned_string_works() { 300 | let str = "meßagefactory.ca"; 301 | let result = unsafe { ffi::ada_idna_to_ascii(str.as_ptr().cast(), str.len()) }; 302 | assert_eq!(result.as_ref(), "xn--meagefactory-m9a.ca"); 303 | unsafe { ffi::ada_free_owned_string(result) }; 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/idna.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "std")] 2 | extern crate std; 3 | 4 | #[cfg_attr(not(feature = "std"), allow(unused_imports))] 5 | use crate::ffi; 6 | 7 | #[cfg(feature = "std")] 8 | use std::string::String; 9 | 10 | /// IDNA struct implements the `to_ascii` and `to_unicode` functions from the Unicode Technical 11 | /// Standard supporting a wide range of systems. It is suitable for URL parsing. 12 | /// For more information, [read the specification](https://www.unicode.org/reports/tr46/#ToUnicode) 13 | pub struct Idna {} 14 | 15 | impl Idna { 16 | /// Process international domains according to the UTS #46 standard. 17 | /// Returns empty string if the input is invalid. 18 | /// 19 | /// For more information, [read the specification](https://www.unicode.org/reports/tr46/#ToUnicode) 20 | /// 21 | /// ``` 22 | /// use ada_url::Idna; 23 | /// assert_eq!(Idna::unicode("xn--meagefactory-m9a.ca"), "meßagefactory.ca"); 24 | /// ``` 25 | #[must_use] 26 | #[cfg(feature = "std")] 27 | pub fn unicode(input: &str) -> String { 28 | unsafe { ffi::ada_idna_to_unicode(input.as_ptr().cast(), input.len()) }.to_string() 29 | } 30 | 31 | /// Process international domains according to the UTS #46 standard. 32 | /// Returns empty string if the input is invalid. 33 | /// 34 | /// For more information, [read the specification](https://www.unicode.org/reports/tr46/#ToASCII) 35 | /// 36 | /// ``` 37 | /// use ada_url::Idna; 38 | /// assert_eq!(Idna::ascii("meßagefactory.ca"), "xn--meagefactory-m9a.ca"); 39 | /// ``` 40 | #[must_use] 41 | #[cfg(feature = "std")] 42 | pub fn ascii(input: &str) -> String { 43 | unsafe { ffi::ada_idna_to_ascii(input.as_ptr().cast(), input.len()) }.to_string() 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | #[cfg_attr(not(feature = "std"), allow(unused_imports))] 50 | use crate::idna::*; 51 | 52 | #[test] 53 | fn unicode_should_work() { 54 | #[cfg(feature = "std")] 55 | assert_eq!(Idna::unicode("xn--meagefactory-m9a.ca"), "meßagefactory.ca"); 56 | } 57 | 58 | #[test] 59 | fn ascii_should_work() { 60 | #[cfg(feature = "std")] 61 | assert_eq!(Idna::ascii("meßagefactory.ca"), "xn--meagefactory-m9a.ca"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Ada URL 2 | //! 3 | //! Ada is a fast and spec-compliant URL parser written in C++. 4 | //! - It's widely tested by both Web Platform Tests and Google OSS Fuzzer. 5 | //! - It is extremely fast. 6 | //! - It's the default URL parser of Node.js since Node 18.16.0. 7 | //! - It supports Unicode Technical Standard. 8 | //! 9 | //! The Ada library passes the full range of tests from the specification, across a wide range 10 | //! of platforms (e.g., Windows, Linux, macOS). 11 | //! 12 | //! ## Performance 13 | //! 14 | //! Ada is extremely fast. 15 | //! For more information read our [benchmark page](https://ada-url.com/docs/performance). 16 | //! 17 | //! ```text 18 | //! ada ▏ 188 ns/URL ███▏ 19 | //! servo url ▏ 664 ns/URL ███████████▎ 20 | //! CURL ▏ 1471 ns/URL █████████████████████████ 21 | //! ``` 22 | //! 23 | //! ## serde 24 | //! 25 | //! If you enable the `serde` feature, [`Url`](struct.Url.html) will implement 26 | //! [`serde::Serialize`](https://docs.rs/serde/1/serde/trait.Serialize.html) and 27 | //! [`serde::Deserialize`](https://docs.rs/serde/1/serde/trait.Deserialize.html). 28 | //! See [serde documentation](https://serde.rs) for more information. 29 | //! 30 | //! ```toml 31 | //! ada-url = { version = "1", features = ["serde"] } 32 | //! ``` 33 | //! 34 | //! ## no-std 35 | //! 36 | //! Whilst `ada-url` has `std` feature enabled by default, you can set `no-default-features` 37 | //! get a subset of features that work in no-std environment. 38 | //! 39 | //! ```toml 40 | //! ada-url = { version = "1", no-default-features = true } 41 | //! ``` 42 | 43 | #![cfg_attr(not(feature = "std"), no_std)] 44 | 45 | pub mod ffi; 46 | mod idna; 47 | mod url_search_params; 48 | pub use idna::Idna; 49 | pub use url_search_params::{ 50 | UrlSearchParams, UrlSearchParamsEntry, UrlSearchParamsEntryIterator, 51 | UrlSearchParamsKeyIterator, UrlSearchParamsValueIterator, 52 | }; 53 | 54 | #[cfg(feature = "std")] 55 | extern crate std; 56 | 57 | #[cfg(feature = "std")] 58 | use std::string::String; 59 | 60 | use core::{borrow, ffi::c_uint, fmt, hash, ops}; 61 | use derive_more::Display; 62 | 63 | /// Error type of [`Url::parse`]. 64 | #[derive(Debug, Display, PartialEq, Eq)] 65 | #[cfg_attr(feature = "std", derive(derive_more::Error))] // error still requires std: https://github.com/rust-lang/rust/issues/103765 66 | #[display(bound(Input: core::fmt::Debug))] 67 | #[display("Invalid url: {input:?}")] 68 | pub struct ParseUrlError { 69 | /// The invalid input that caused the error. 70 | pub input: Input, 71 | } 72 | 73 | /// Defines the type of the host. 74 | #[derive(Debug, Clone, PartialEq, Eq)] 75 | pub enum HostType { 76 | Domain = 0, 77 | IPV4 = 1, 78 | IPV6 = 2, 79 | } 80 | 81 | impl From for HostType { 82 | fn from(value: c_uint) -> Self { 83 | match value { 84 | 0 => Self::Domain, 85 | 1 => Self::IPV4, 86 | 2 => Self::IPV6, 87 | _ => Self::Domain, 88 | } 89 | } 90 | } 91 | 92 | /// Defines the scheme type of the url. 93 | #[derive(Debug, Clone, PartialEq, Eq)] 94 | pub enum SchemeType { 95 | Http = 0, 96 | NotSpecial = 1, 97 | Https = 2, 98 | Ws = 3, 99 | Ftp = 4, 100 | Wss = 5, 101 | File = 6, 102 | } 103 | 104 | impl From for SchemeType { 105 | fn from(value: c_uint) -> Self { 106 | match value { 107 | 0 => Self::Http, 108 | 1 => Self::NotSpecial, 109 | 2 => Self::Https, 110 | 3 => Self::Ws, 111 | 4 => Self::Ftp, 112 | 5 => Self::Wss, 113 | 6 => Self::File, 114 | _ => Self::NotSpecial, 115 | } 116 | } 117 | } 118 | 119 | /// Components are a serialization-free representation of a URL. 120 | /// For usages where string serialization has a high cost, you can 121 | /// use url components with `href` attribute. 122 | /// 123 | /// By using 32-bit integers, we implicitly assume that the URL string 124 | /// cannot exceed 4 GB. 125 | /// 126 | /// ```text 127 | /// https://user:pass@example.com:1234/foo/bar?baz#quux 128 | /// | | | | ^^^^| | | 129 | /// | | | | | | | `----- hash_start 130 | /// | | | | | | `--------- search_start 131 | /// | | | | | `----------------- pathname_start 132 | /// | | | | `--------------------- port 133 | /// | | | `----------------------- host_end 134 | /// | | `---------------------------------- host_start 135 | /// | `--------------------------------------- username_end 136 | /// `--------------------------------------------- protocol_end 137 | /// ``` 138 | #[derive(Debug)] 139 | pub struct UrlComponents { 140 | pub protocol_end: u32, 141 | pub username_end: u32, 142 | pub host_start: u32, 143 | pub host_end: u32, 144 | pub port: Option, 145 | pub pathname_start: Option, 146 | pub search_start: Option, 147 | pub hash_start: Option, 148 | } 149 | 150 | impl From<&ffi::ada_url_components> for UrlComponents { 151 | fn from(value: &ffi::ada_url_components) -> Self { 152 | let port = (value.port != u32::MAX).then_some(value.port); 153 | let pathname_start = (value.pathname_start != u32::MAX).then_some(value.pathname_start); 154 | let search_start = (value.search_start != u32::MAX).then_some(value.search_start); 155 | let hash_start = (value.hash_start != u32::MAX).then_some(value.hash_start); 156 | Self { 157 | protocol_end: value.protocol_end, 158 | username_end: value.username_end, 159 | host_start: value.host_start, 160 | host_end: value.host_end, 161 | port, 162 | pathname_start, 163 | search_start, 164 | hash_start, 165 | } 166 | } 167 | } 168 | 169 | /// A parsed URL struct according to WHATWG URL specification. 170 | #[derive(Eq)] 171 | pub struct Url(*mut ffi::ada_url); 172 | 173 | /// Clone trait by default uses bit-wise copy. 174 | /// In Rust, FFI requires deep copy, which requires an additional/inexpensive FFI call. 175 | impl Clone for Url { 176 | fn clone(&self) -> Self { 177 | unsafe { ffi::ada_copy(self.0).into() } 178 | } 179 | } 180 | 181 | impl Drop for Url { 182 | fn drop(&mut self) { 183 | unsafe { ffi::ada_free(self.0) } 184 | } 185 | } 186 | 187 | impl From<*mut ffi::ada_url> for Url { 188 | fn from(value: *mut ffi::ada_url) -> Self { 189 | Self(value) 190 | } 191 | } 192 | 193 | type SetterResult = Result<(), ()>; 194 | 195 | #[inline] 196 | const fn setter_result(successful: bool) -> SetterResult { 197 | if successful { Ok(()) } else { Err(()) } 198 | } 199 | 200 | impl Url { 201 | /// Parses the input with an optional base 202 | /// 203 | /// ``` 204 | /// use ada_url::Url; 205 | /// let out = Url::parse("https://ada-url.github.io/ada", None) 206 | /// .expect("This is a valid URL. Should have parsed it."); 207 | /// assert_eq!(out.protocol(), "https:"); 208 | /// ``` 209 | pub fn parse(input: Input, base: Option<&str>) -> Result> 210 | where 211 | Input: AsRef, 212 | { 213 | let url_aggregator = match base { 214 | Some(base) => unsafe { 215 | ffi::ada_parse_with_base( 216 | input.as_ref().as_ptr().cast(), 217 | input.as_ref().len(), 218 | base.as_ptr().cast(), 219 | base.len(), 220 | ) 221 | }, 222 | None => unsafe { ffi::ada_parse(input.as_ref().as_ptr().cast(), input.as_ref().len()) }, 223 | }; 224 | 225 | if unsafe { ffi::ada_is_valid(url_aggregator) } { 226 | Ok(url_aggregator.into()) 227 | } else { 228 | Err(ParseUrlError { input }) 229 | } 230 | } 231 | 232 | /// Returns whether or not the URL can be parsed or not. 233 | /// 234 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-canparse) 235 | /// 236 | /// ``` 237 | /// use ada_url::Url; 238 | /// assert!(Url::can_parse("https://ada-url.github.io/ada", None)); 239 | /// assert!(Url::can_parse("/pathname", Some("https://ada-url.github.io/ada"))); 240 | /// ``` 241 | #[must_use] 242 | pub fn can_parse(input: &str, base: Option<&str>) -> bool { 243 | unsafe { 244 | if let Some(base) = base { 245 | ffi::ada_can_parse_with_base( 246 | input.as_ptr().cast(), 247 | input.len(), 248 | base.as_ptr().cast(), 249 | base.len(), 250 | ) 251 | } else { 252 | ffi::ada_can_parse(input.as_ptr().cast(), input.len()) 253 | } 254 | } 255 | } 256 | 257 | /// Returns the type of the host such as default, ipv4 or ipv6. 258 | #[must_use] 259 | pub fn host_type(&self) -> HostType { 260 | HostType::from(unsafe { ffi::ada_get_host_type(self.0) }) 261 | } 262 | 263 | /// Returns the type of the scheme such as http, https, etc. 264 | #[must_use] 265 | pub fn scheme_type(&self) -> SchemeType { 266 | SchemeType::from(unsafe { ffi::ada_get_scheme_type(self.0) }) 267 | } 268 | 269 | /// Return the origin of this URL 270 | /// 271 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-origin) 272 | /// 273 | /// ``` 274 | /// use ada_url::Url; 275 | /// 276 | /// let url = Url::parse("blob:https://example.com/foo", None).expect("Invalid URL"); 277 | /// assert_eq!(url.origin(), "https://example.com"); 278 | /// ``` 279 | #[must_use] 280 | #[cfg(feature = "std")] 281 | pub fn origin(&self) -> String { 282 | unsafe { ffi::ada_get_origin(self.0) }.to_string() 283 | } 284 | 285 | /// Return the parsed version of the URL with all components. 286 | /// 287 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-href) 288 | #[must_use] 289 | pub fn href(&self) -> &str { 290 | unsafe { ffi::ada_get_href(self.0) }.as_str() 291 | } 292 | 293 | /// Updates the href of the URL, and triggers the URL parser. 294 | /// 295 | /// ``` 296 | /// use ada_url::Url; 297 | /// 298 | /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL"); 299 | /// url.set_href("https://lemire.me").unwrap(); 300 | /// assert_eq!(url.href(), "https://lemire.me/"); 301 | /// ``` 302 | #[allow(clippy::result_unit_err)] 303 | pub fn set_href(&mut self, input: &str) -> SetterResult { 304 | setter_result(unsafe { ffi::ada_set_href(self.0, input.as_ptr().cast(), input.len()) }) 305 | } 306 | 307 | /// Return the username for this URL as a percent-encoded ASCII string. 308 | /// 309 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-username) 310 | /// 311 | /// ``` 312 | /// use ada_url::Url; 313 | /// 314 | /// let url = Url::parse("ftp://rms:secret123@example.com", None).expect("Invalid URL"); 315 | /// assert_eq!(url.username(), "rms"); 316 | /// ``` 317 | #[must_use] 318 | pub fn username(&self) -> &str { 319 | unsafe { ffi::ada_get_username(self.0) }.as_str() 320 | } 321 | 322 | /// Updates the `username` of the URL. 323 | /// 324 | /// ``` 325 | /// use ada_url::Url; 326 | /// 327 | /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL"); 328 | /// url.set_username(Some("username")).unwrap(); 329 | /// assert_eq!(url.href(), "https://username@yagiz.co/"); 330 | /// ``` 331 | #[allow(clippy::result_unit_err)] 332 | pub fn set_username(&mut self, input: Option<&str>) -> SetterResult { 333 | setter_result(unsafe { 334 | ffi::ada_set_username( 335 | self.0, 336 | input.unwrap_or("").as_ptr().cast(), 337 | input.map_or(0, str::len), 338 | ) 339 | }) 340 | } 341 | 342 | /// Return the password for this URL, if any, as a percent-encoded ASCII string. 343 | /// 344 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-password) 345 | /// 346 | /// ``` 347 | /// use ada_url::Url; 348 | /// 349 | /// let url = Url::parse("ftp://rms:secret123@example.com", None).expect("Invalid URL"); 350 | /// assert_eq!(url.password(), "secret123"); 351 | /// ``` 352 | #[must_use] 353 | pub fn password(&self) -> &str { 354 | unsafe { ffi::ada_get_password(self.0) }.as_str() 355 | } 356 | 357 | /// Updates the `password` of the URL. 358 | /// 359 | /// ``` 360 | /// use ada_url::Url; 361 | /// 362 | /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL"); 363 | /// url.set_password(Some("password")).unwrap(); 364 | /// assert_eq!(url.href(), "https://:password@yagiz.co/"); 365 | /// ``` 366 | #[allow(clippy::result_unit_err)] 367 | pub fn set_password(&mut self, input: Option<&str>) -> SetterResult { 368 | setter_result(unsafe { 369 | ffi::ada_set_password( 370 | self.0, 371 | input.unwrap_or("").as_ptr().cast(), 372 | input.map_or(0, str::len), 373 | ) 374 | }) 375 | } 376 | 377 | /// Return the port number for this URL, or an empty string. 378 | /// 379 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-port) 380 | /// 381 | /// ``` 382 | /// use ada_url::Url; 383 | /// 384 | /// let url = Url::parse("https://example.com", None).expect("Invalid URL"); 385 | /// assert_eq!(url.port(), ""); 386 | /// 387 | /// let url = Url::parse("https://example.com:8080", None).expect("Invalid URL"); 388 | /// assert_eq!(url.port(), "8080"); 389 | /// ``` 390 | #[must_use] 391 | pub fn port(&self) -> &str { 392 | unsafe { ffi::ada_get_port(self.0) }.as_str() 393 | } 394 | 395 | /// Updates the `port` of the URL. 396 | /// 397 | /// ``` 398 | /// use ada_url::Url; 399 | /// 400 | /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL"); 401 | /// url.set_port(Some("8080")).unwrap(); 402 | /// assert_eq!(url.href(), "https://yagiz.co:8080/"); 403 | /// ``` 404 | #[allow(clippy::result_unit_err)] 405 | pub fn set_port(&mut self, input: Option<&str>) -> SetterResult { 406 | if let Some(value) = input { 407 | setter_result(unsafe { ffi::ada_set_port(self.0, value.as_ptr().cast(), value.len()) }) 408 | } else { 409 | unsafe { ffi::ada_clear_port(self.0) } 410 | Ok(()) 411 | } 412 | } 413 | 414 | /// Return this URL’s fragment identifier, or an empty string. 415 | /// A fragment is the part of the URL with the # symbol. 416 | /// The fragment is optional and, if present, contains a fragment identifier that identifies 417 | /// a secondary resource, such as a section heading of a document. 418 | /// In HTML, the fragment identifier is usually the id attribute of a an element that is 419 | /// scrolled to on load. Browsers typically will not send the fragment portion of a URL to the 420 | /// server. 421 | /// 422 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-hash) 423 | /// 424 | /// ``` 425 | /// use ada_url::Url; 426 | /// 427 | /// let url = Url::parse("https://example.com/data.csv#row=4", None).expect("Invalid URL"); 428 | /// assert_eq!(url.hash(), "#row=4"); 429 | /// assert!(url.has_hash()); 430 | /// ``` 431 | #[must_use] 432 | pub fn hash(&self) -> &str { 433 | unsafe { ffi::ada_get_hash(self.0) }.as_str() 434 | } 435 | 436 | /// Updates the `hash` of the URL. 437 | /// 438 | /// ``` 439 | /// use ada_url::Url; 440 | /// 441 | /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL"); 442 | /// url.set_hash(Some("this-is-my-hash")); 443 | /// assert_eq!(url.href(), "https://yagiz.co/#this-is-my-hash"); 444 | /// ``` 445 | pub fn set_hash(&mut self, input: Option<&str>) { 446 | match input { 447 | Some(value) => unsafe { ffi::ada_set_hash(self.0, value.as_ptr().cast(), value.len()) }, 448 | None => unsafe { ffi::ada_clear_hash(self.0) }, 449 | } 450 | } 451 | 452 | /// Return the parsed representation of the host for this URL with an optional port number. 453 | /// 454 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-host) 455 | /// 456 | /// ``` 457 | /// use ada_url::Url; 458 | /// 459 | /// let url = Url::parse("https://127.0.0.1:8080/index.html", None).expect("Invalid URL"); 460 | /// assert_eq!(url.host(), "127.0.0.1:8080"); 461 | /// ``` 462 | #[must_use] 463 | pub fn host(&self) -> &str { 464 | unsafe { ffi::ada_get_host(self.0) }.as_str() 465 | } 466 | 467 | /// Updates the `host` of the URL. 468 | /// 469 | /// ``` 470 | /// use ada_url::Url; 471 | /// 472 | /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL"); 473 | /// url.set_host(Some("localhost:3000")).unwrap(); 474 | /// assert_eq!(url.href(), "https://localhost:3000/"); 475 | /// ``` 476 | #[allow(clippy::result_unit_err)] 477 | pub fn set_host(&mut self, input: Option<&str>) -> SetterResult { 478 | setter_result(unsafe { 479 | ffi::ada_set_host( 480 | self.0, 481 | input.unwrap_or("").as_ptr().cast(), 482 | input.map_or(0, str::len), 483 | ) 484 | }) 485 | } 486 | 487 | /// Return the parsed representation of the host for this URL. Non-ASCII domain labels are 488 | /// punycode-encoded per IDNA if this is the host of a special URL, or percent encoded for 489 | /// non-special URLs. 490 | /// 491 | /// Hostname does not contain port number. 492 | /// 493 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-hostname) 494 | /// 495 | /// ``` 496 | /// use ada_url::Url; 497 | /// 498 | /// let url = Url::parse("https://127.0.0.1:8080/index.html", None).expect("Invalid URL"); 499 | /// assert_eq!(url.hostname(), "127.0.0.1"); 500 | /// ``` 501 | #[must_use] 502 | pub fn hostname(&self) -> &str { 503 | unsafe { ffi::ada_get_hostname(self.0) }.as_str() 504 | } 505 | 506 | /// Updates the `hostname` of the URL. 507 | /// 508 | /// ``` 509 | /// use ada_url::Url; 510 | /// 511 | /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL"); 512 | /// url.set_hostname(Some("localhost")).unwrap(); 513 | /// assert_eq!(url.href(), "https://localhost/"); 514 | /// ``` 515 | #[allow(clippy::result_unit_err)] 516 | pub fn set_hostname(&mut self, input: Option<&str>) -> SetterResult { 517 | setter_result(unsafe { 518 | ffi::ada_set_hostname( 519 | self.0, 520 | input.unwrap_or("").as_ptr().cast(), 521 | input.map_or(0, str::len), 522 | ) 523 | }) 524 | } 525 | 526 | /// Return the path for this URL, as a percent-encoded ASCII string. 527 | /// 528 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-pathname) 529 | /// 530 | /// ``` 531 | /// use ada_url::Url; 532 | /// 533 | /// let url = Url::parse("https://example.com/api/versions?page=2", None).expect("Invalid URL"); 534 | /// assert_eq!(url.pathname(), "/api/versions"); 535 | /// ``` 536 | #[must_use] 537 | pub fn pathname(&self) -> &str { 538 | unsafe { ffi::ada_get_pathname(self.0) }.as_str() 539 | } 540 | 541 | /// Updates the `pathname` of the URL. 542 | /// 543 | /// ``` 544 | /// use ada_url::Url; 545 | /// 546 | /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL"); 547 | /// url.set_pathname(Some("/contact")).unwrap(); 548 | /// assert_eq!(url.href(), "https://yagiz.co/contact"); 549 | /// ``` 550 | #[allow(clippy::result_unit_err)] 551 | pub fn set_pathname(&mut self, input: Option<&str>) -> SetterResult { 552 | setter_result(unsafe { 553 | ffi::ada_set_pathname( 554 | self.0, 555 | input.unwrap_or("").as_ptr().cast(), 556 | input.map_or(0, str::len), 557 | ) 558 | }) 559 | } 560 | 561 | /// Return this URL’s query string, if any, as a percent-encoded ASCII string. 562 | /// 563 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-search) 564 | /// 565 | /// ``` 566 | /// use ada_url::Url; 567 | /// 568 | /// let url = Url::parse("https://example.com/products?page=2", None).expect("Invalid URL"); 569 | /// assert_eq!(url.search(), "?page=2"); 570 | /// 571 | /// let url = Url::parse("https://example.com/products", None).expect("Invalid URL"); 572 | /// assert_eq!(url.search(), ""); 573 | /// ``` 574 | #[must_use] 575 | pub fn search(&self) -> &str { 576 | unsafe { ffi::ada_get_search(self.0) }.as_str() 577 | } 578 | 579 | /// Updates the `search` of the URL. 580 | /// 581 | /// ``` 582 | /// use ada_url::Url; 583 | /// 584 | /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL"); 585 | /// url.set_search(Some("?page=1")); 586 | /// assert_eq!(url.href(), "https://yagiz.co/?page=1"); 587 | /// ``` 588 | pub fn set_search(&mut self, input: Option<&str>) { 589 | match input { 590 | Some(value) => unsafe { 591 | ffi::ada_set_search(self.0, value.as_ptr().cast(), value.len()); 592 | }, 593 | None => unsafe { ffi::ada_clear_search(self.0) }, 594 | } 595 | } 596 | 597 | /// Return the scheme of this URL, lower-cased, as an ASCII string with the ‘:’ delimiter. 598 | /// 599 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-protocol) 600 | /// 601 | /// ``` 602 | /// use ada_url::Url; 603 | /// 604 | /// let url = Url::parse("file:///tmp/foo", None).expect("Invalid URL"); 605 | /// assert_eq!(url.protocol(), "file:"); 606 | /// ``` 607 | #[must_use] 608 | pub fn protocol(&self) -> &str { 609 | unsafe { ffi::ada_get_protocol(self.0) }.as_str() 610 | } 611 | 612 | /// Updates the `protocol` of the URL. 613 | /// 614 | /// ``` 615 | /// use ada_url::Url; 616 | /// 617 | /// let mut url = Url::parse("http://yagiz.co", None).expect("Invalid URL"); 618 | /// url.set_protocol("http").unwrap(); 619 | /// assert_eq!(url.href(), "http://yagiz.co/"); 620 | /// ``` 621 | #[allow(clippy::result_unit_err)] 622 | pub fn set_protocol(&mut self, input: &str) -> SetterResult { 623 | setter_result(unsafe { ffi::ada_set_protocol(self.0, input.as_ptr().cast(), input.len()) }) 624 | } 625 | 626 | /// A URL includes credentials if its username or password is not the empty string. 627 | #[must_use] 628 | pub fn has_credentials(&self) -> bool { 629 | unsafe { ffi::ada_has_credentials(self.0) } 630 | } 631 | 632 | /// Returns true if it has an host but it is the empty string. 633 | #[must_use] 634 | pub fn has_empty_hostname(&self) -> bool { 635 | unsafe { ffi::ada_has_empty_hostname(self.0) } 636 | } 637 | 638 | /// Returns true if it has a host (included an empty host) 639 | #[must_use] 640 | pub fn has_hostname(&self) -> bool { 641 | unsafe { ffi::ada_has_hostname(self.0) } 642 | } 643 | 644 | /// Returns true if URL has a non-empty username. 645 | #[must_use] 646 | pub fn has_non_empty_username(&self) -> bool { 647 | unsafe { ffi::ada_has_non_empty_username(self.0) } 648 | } 649 | 650 | /// Returns true if URL has a non-empty password. 651 | #[must_use] 652 | pub fn has_non_empty_password(&self) -> bool { 653 | unsafe { ffi::ada_has_non_empty_password(self.0) } 654 | } 655 | 656 | /// Returns true if URL has a port. 657 | #[must_use] 658 | pub fn has_port(&self) -> bool { 659 | unsafe { ffi::ada_has_port(self.0) } 660 | } 661 | 662 | /// Returns true if URL has password. 663 | #[must_use] 664 | pub fn has_password(&self) -> bool { 665 | unsafe { ffi::ada_has_password(self.0) } 666 | } 667 | 668 | /// Returns true if URL has a hash/fragment. 669 | #[must_use] 670 | pub fn has_hash(&self) -> bool { 671 | unsafe { ffi::ada_has_hash(self.0) } 672 | } 673 | 674 | /// Returns true if URL has search/query. 675 | #[must_use] 676 | pub fn has_search(&self) -> bool { 677 | unsafe { ffi::ada_has_search(self.0) } 678 | } 679 | 680 | /// Returns the parsed version of the URL with all components. 681 | /// 682 | /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-href) 683 | #[must_use] 684 | pub fn as_str(&self) -> &str { 685 | self.href() 686 | } 687 | 688 | /// Returns the URL components of the instance. 689 | #[must_use] 690 | pub fn components(&self) -> UrlComponents { 691 | unsafe { ffi::ada_get_components(self.0).as_ref().unwrap() }.into() 692 | } 693 | } 694 | 695 | /// Serializes this URL into a `serde` stream. 696 | /// 697 | /// This implementation is only available if the `serde` Cargo feature is enabled. 698 | #[cfg(feature = "serde")] 699 | impl serde::Serialize for Url { 700 | fn serialize(&self, serializer: S) -> Result 701 | where 702 | S: serde::Serializer, 703 | { 704 | serializer.serialize_str(self.as_str()) 705 | } 706 | } 707 | 708 | /// Deserializes this URL from a `serde` stream. 709 | /// 710 | /// This implementation is only available if the `serde` Cargo feature is enabled. 711 | #[cfg(feature = "serde")] 712 | #[cfg(feature = "std")] 713 | impl<'de> serde::Deserialize<'de> for Url { 714 | fn deserialize(deserializer: D) -> Result 715 | where 716 | D: serde::Deserializer<'de>, 717 | { 718 | use serde::de::{Error, Unexpected, Visitor}; 719 | 720 | struct UrlVisitor; 721 | 722 | impl Visitor<'_> for UrlVisitor { 723 | type Value = Url; 724 | 725 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 726 | formatter.write_str("a string representing an URL") 727 | } 728 | 729 | fn visit_str(self, s: &str) -> Result 730 | where 731 | E: Error, 732 | { 733 | Url::parse(s, None).map_err(|err| { 734 | let err_s = std::format!("{}", err); 735 | Error::invalid_value(Unexpected::Str(s), &err_s.as_str()) 736 | }) 737 | } 738 | } 739 | 740 | deserializer.deserialize_str(UrlVisitor) 741 | } 742 | } 743 | 744 | /// Send is required for sharing Url between threads safely 745 | unsafe impl Send for Url {} 746 | 747 | /// Sync is required for sharing Url between threads safely 748 | unsafe impl Sync for Url {} 749 | 750 | /// URLs compare like their stringification. 751 | impl PartialEq for Url { 752 | fn eq(&self, other: &Self) -> bool { 753 | self.href() == other.href() 754 | } 755 | } 756 | 757 | impl PartialOrd for Url { 758 | fn partial_cmp(&self, other: &Self) -> Option { 759 | Some(self.cmp(other)) 760 | } 761 | } 762 | 763 | impl Ord for Url { 764 | fn cmp(&self, other: &Self) -> core::cmp::Ordering { 765 | self.href().cmp(other.href()) 766 | } 767 | } 768 | 769 | impl hash::Hash for Url { 770 | fn hash(&self, state: &mut H) { 771 | self.href().hash(state); 772 | } 773 | } 774 | 775 | impl borrow::Borrow for Url { 776 | fn borrow(&self) -> &str { 777 | self.href() 778 | } 779 | } 780 | 781 | impl AsRef<[u8]> for Url { 782 | fn as_ref(&self) -> &[u8] { 783 | self.href().as_bytes() 784 | } 785 | } 786 | 787 | #[cfg(feature = "std")] 788 | impl From for String { 789 | fn from(val: Url) -> Self { 790 | val.href().to_owned() 791 | } 792 | } 793 | 794 | impl fmt::Debug for Url { 795 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 796 | f.debug_struct("Url") 797 | .field("href", &self.href()) 798 | .field("components", &self.components()) 799 | .finish() 800 | } 801 | } 802 | 803 | impl<'input> TryFrom<&'input str> for Url { 804 | type Error = ParseUrlError<&'input str>; 805 | 806 | fn try_from(value: &'input str) -> Result { 807 | Self::parse(value, None) 808 | } 809 | } 810 | 811 | #[cfg(feature = "std")] 812 | impl TryFrom for Url { 813 | type Error = ParseUrlError; 814 | 815 | fn try_from(value: String) -> Result { 816 | Self::parse(value, None) 817 | } 818 | } 819 | 820 | #[cfg(feature = "std")] 821 | impl<'input> TryFrom<&'input String> for Url { 822 | type Error = ParseUrlError<&'input String>; 823 | 824 | fn try_from(value: &'input String) -> Result { 825 | Self::parse(value, None) 826 | } 827 | } 828 | 829 | impl ops::Deref for Url { 830 | type Target = str; 831 | fn deref(&self) -> &Self::Target { 832 | self.href() 833 | } 834 | } 835 | 836 | impl AsRef for Url { 837 | fn as_ref(&self) -> &str { 838 | self.href() 839 | } 840 | } 841 | 842 | impl fmt::Display for Url { 843 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 844 | f.write_str(self.href()) 845 | } 846 | } 847 | 848 | #[cfg(feature = "std")] 849 | impl core::str::FromStr for Url { 850 | type Err = ParseUrlError>; 851 | 852 | fn from_str(s: &str) -> Result { 853 | Self::parse(s, None).map_err(|ParseUrlError { input }| ParseUrlError { 854 | input: input.into(), 855 | }) 856 | } 857 | } 858 | 859 | #[cfg(test)] 860 | mod test { 861 | use super::*; 862 | 863 | #[test] 864 | fn should_display_serialization() { 865 | let tests = [ 866 | ("http://example.com/", "http://example.com/"), 867 | ("HTTP://EXAMPLE.COM", "http://example.com/"), 868 | ("http://user:pwd@domain.com", "http://user:pwd@domain.com/"), 869 | ( 870 | "HTTP://EXAMPLE.COM/FOO/BAR?K1=V1&K2=V2", 871 | "http://example.com/FOO/BAR?K1=V1&K2=V2", 872 | ), 873 | ( 874 | "http://example.com/🦀/❤️/", 875 | "http://example.com/%F0%9F%A6%80/%E2%9D%A4%EF%B8%8F/", 876 | ), 877 | ( 878 | "https://example.org/hello world.html", 879 | "https://example.org/hello%20world.html", 880 | ), 881 | ( 882 | "https://三十六計.org/走為上策/", 883 | "https://xn--ehq95fdxbx86i.org/%E8%B5%B0%E7%82%BA%E4%B8%8A%E7%AD%96/", 884 | ), 885 | ]; 886 | for (value, expected) in tests { 887 | let url = Url::parse(value, None).expect("Should have parsed url"); 888 | assert_eq!(url.as_str(), expected); 889 | } 890 | } 891 | 892 | #[test] 893 | fn try_from_ok() { 894 | let url = Url::try_from("http://example.com/foo/bar?k1=v1&k2=v2"); 895 | #[cfg(feature = "std")] 896 | std::dbg!(&url); 897 | let url = url.unwrap(); 898 | assert_eq!(url.href(), "http://example.com/foo/bar?k1=v1&k2=v2"); 899 | assert_eq!( 900 | url, 901 | Url::parse("http://example.com/foo/bar?k1=v1&k2=v2", None).unwrap(), 902 | ); 903 | } 904 | 905 | #[test] 906 | fn try_from_err() { 907 | let url = Url::try_from("this is not a url"); 908 | #[cfg(feature = "std")] 909 | std::dbg!(&url); 910 | let error = url.unwrap_err(); 911 | #[cfg(feature = "std")] 912 | assert_eq!(error.to_string(), r#"Invalid url: "this is not a url""#); 913 | assert_eq!(error.input, "this is not a url"); 914 | } 915 | 916 | #[test] 917 | fn should_compare_urls() { 918 | let tests = [ 919 | ("http://example.com/", "http://example.com/", true), 920 | ("http://example.com/", "https://example.com/", false), 921 | ("http://example.com#", "https://example.com/#", false), 922 | ("http://example.com", "https://example.com#", false), 923 | ( 924 | "https://user:pwd@example.com", 925 | "https://user:pwd@example.com", 926 | true, 927 | ), 928 | ]; 929 | for (left, right, expected) in tests { 930 | let left_url = Url::parse(left, None).expect("Should have parsed url"); 931 | let right_url = Url::parse(right, None).expect("Should have parsed url"); 932 | assert_eq!( 933 | left_url == right_url, 934 | expected, 935 | "left: {left}, right: {right}, expected: {expected}", 936 | ); 937 | } 938 | } 939 | #[test] 940 | fn should_order_alphabetically() { 941 | let left = Url::parse("https://example.com/", None).expect("Should have parsed url"); 942 | let right = Url::parse("https://zoo.tld/", None).expect("Should have parsed url"); 943 | assert!(left < right); 944 | let left = Url::parse("https://c.tld/", None).expect("Should have parsed url"); 945 | let right = Url::parse("https://a.tld/", None).expect("Should have parsed url"); 946 | assert!(right < left); 947 | } 948 | 949 | #[test] 950 | fn should_parse_simple_url() { 951 | let mut out = Url::parse( 952 | "https://username:password@google.com:9090/search?query#hash", 953 | None, 954 | ) 955 | .expect("Should have parsed a simple url"); 956 | 957 | #[cfg(feature = "std")] 958 | assert_eq!(out.origin(), "https://google.com:9090"); 959 | 960 | assert_eq!( 961 | out.href(), 962 | "https://username:password@google.com:9090/search?query#hash" 963 | ); 964 | 965 | assert_eq!(out.scheme_type(), SchemeType::Https); 966 | 967 | out.set_username(Some("new-username")).unwrap(); 968 | assert_eq!(out.username(), "new-username"); 969 | 970 | out.set_password(Some("new-password")).unwrap(); 971 | assert_eq!(out.password(), "new-password"); 972 | 973 | out.set_port(Some("4242")).unwrap(); 974 | assert_eq!(out.port(), "4242"); 975 | out.set_port(None).unwrap(); 976 | assert_eq!(out.port(), ""); 977 | 978 | out.set_hash(Some("#new-hash")); 979 | assert_eq!(out.hash(), "#new-hash"); 980 | 981 | out.set_host(Some("yagiz.co:9999")).unwrap(); 982 | assert_eq!(out.host(), "yagiz.co:9999"); 983 | 984 | out.set_hostname(Some("domain.com")).unwrap(); 985 | assert_eq!(out.hostname(), "domain.com"); 986 | 987 | out.set_pathname(Some("/new-search")).unwrap(); 988 | assert_eq!(out.pathname(), "/new-search"); 989 | out.set_pathname(None).unwrap(); 990 | assert_eq!(out.pathname(), "/"); 991 | 992 | out.set_search(Some("updated-query")); 993 | assert_eq!(out.search(), "?updated-query"); 994 | 995 | out.set_protocol("wss").unwrap(); 996 | assert_eq!(out.protocol(), "wss:"); 997 | assert_eq!(out.scheme_type(), SchemeType::Wss); 998 | 999 | assert!(out.has_credentials()); 1000 | assert!(out.has_non_empty_username()); 1001 | assert!(out.has_non_empty_password()); 1002 | assert!(out.has_search()); 1003 | assert!(out.has_hash()); 1004 | assert!(out.has_password()); 1005 | 1006 | assert_eq!(out.host_type(), HostType::Domain); 1007 | } 1008 | 1009 | #[test] 1010 | fn scheme_types() { 1011 | assert_eq!( 1012 | Url::parse("file:///foo/bar", None) 1013 | .expect("bad url") 1014 | .scheme_type(), 1015 | SchemeType::File 1016 | ); 1017 | assert_eq!( 1018 | Url::parse("ws://example.com/ws", None) 1019 | .expect("bad url") 1020 | .scheme_type(), 1021 | SchemeType::Ws 1022 | ); 1023 | assert_eq!( 1024 | Url::parse("wss://example.com/wss", None) 1025 | .expect("bad url") 1026 | .scheme_type(), 1027 | SchemeType::Wss 1028 | ); 1029 | assert_eq!( 1030 | Url::parse("ftp://example.com/file.txt", None) 1031 | .expect("bad url") 1032 | .scheme_type(), 1033 | SchemeType::Ftp 1034 | ); 1035 | assert_eq!( 1036 | Url::parse("http://example.com/file.txt", None) 1037 | .expect("bad url") 1038 | .scheme_type(), 1039 | SchemeType::Http 1040 | ); 1041 | assert_eq!( 1042 | Url::parse("https://example.com/file.txt", None) 1043 | .expect("bad url") 1044 | .scheme_type(), 1045 | SchemeType::Https 1046 | ); 1047 | assert_eq!( 1048 | Url::parse("foo://example.com", None) 1049 | .expect("bad url") 1050 | .scheme_type(), 1051 | SchemeType::NotSpecial 1052 | ); 1053 | } 1054 | 1055 | #[test] 1056 | fn can_parse_simple_url() { 1057 | assert!(Url::can_parse("https://google.com", None)); 1058 | assert!(Url::can_parse("/helo", Some("https://www.google.com"))); 1059 | } 1060 | 1061 | #[cfg(feature = "std")] 1062 | #[cfg(feature = "serde")] 1063 | #[test] 1064 | fn test_serde_serialize_deserialize() { 1065 | let input = "https://www.google.com"; 1066 | let output = "\"https://www.google.com/\""; 1067 | let url = Url::parse(&input, None).unwrap(); 1068 | assert_eq!(serde_json::to_string(&url).unwrap(), output); 1069 | 1070 | let deserialized: Url = serde_json::from_str(&output).unwrap(); 1071 | assert_eq!(deserialized.href(), "https://www.google.com/"); 1072 | } 1073 | 1074 | #[test] 1075 | fn should_clone() { 1076 | let first = Url::parse("https://lemire.me", None).unwrap(); 1077 | let mut second = first.clone(); 1078 | second.set_href("https://yagiz.co").unwrap(); 1079 | assert_ne!(first.href(), second.href()); 1080 | assert_eq!(first.href(), "https://lemire.me/"); 1081 | assert_eq!(second.href(), "https://yagiz.co/"); 1082 | } 1083 | 1084 | #[test] 1085 | fn should_handle_empty_host() { 1086 | // Ref: https://github.com/ada-url/rust/issues/74 1087 | let url = Url::parse("file:///C:/Users/User/Documents/example.pdf", None).unwrap(); 1088 | assert_eq!(url.host(), ""); 1089 | assert_eq!(url.hostname(), ""); 1090 | } 1091 | } 1092 | -------------------------------------------------------------------------------- /src/url_search_params.rs: -------------------------------------------------------------------------------- 1 | use crate::{ParseUrlError, ffi}; 2 | 3 | #[derive(Hash)] 4 | pub struct UrlSearchParams(*mut ffi::ada_url_search_params); 5 | 6 | impl Drop for UrlSearchParams { 7 | fn drop(&mut self) { 8 | unsafe { ffi::ada_free_search_params(self.0) } 9 | } 10 | } 11 | 12 | impl UrlSearchParams { 13 | /// Parses an return a UrlSearchParams struct. 14 | /// 15 | /// ``` 16 | /// use ada_url::UrlSearchParams; 17 | /// let params = UrlSearchParams::parse("a=1&b=2") 18 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 19 | /// assert_eq!(params.get("a"), Some("1")); 20 | /// assert_eq!(params.get("b"), Some("2")); 21 | /// ``` 22 | pub fn parse(input: Input) -> Result> 23 | where 24 | Input: AsRef, 25 | { 26 | Ok(Self(unsafe { 27 | ffi::ada_parse_search_params(input.as_ref().as_ptr().cast(), input.as_ref().len()) 28 | })) 29 | } 30 | 31 | /// Returns the unique keys in a UrlSearchParams. 32 | /// 33 | /// ``` 34 | /// use ada_url::UrlSearchParams; 35 | /// let params = UrlSearchParams::parse("a=1&b=2") 36 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 37 | /// assert_eq!(params.len(), 2); 38 | /// let keys = params.keys().into_iter(); 39 | /// assert_eq!(keys.count(), params.len()); 40 | /// ``` 41 | pub fn len(&self) -> usize { 42 | unsafe { ffi::ada_search_params_size(self.0) } 43 | } 44 | 45 | /// Returns true if no entries exist in the UrlSearchParams. 46 | pub fn is_empty(&self) -> bool { 47 | self.len() == 0 48 | } 49 | 50 | /// Sorts the keys of the UrlSearchParams struct. 51 | pub fn sort(&mut self) { 52 | unsafe { ffi::ada_search_params_sort(self.0) } 53 | } 54 | 55 | /// Appends a key/value to the UrlSearchParams struct. 56 | pub fn append(&mut self, key: &str, value: &str) { 57 | unsafe { 58 | ffi::ada_search_params_append( 59 | self.0, 60 | key.as_ptr().cast(), 61 | key.len(), 62 | value.as_ptr().cast(), 63 | value.len(), 64 | ) 65 | } 66 | } 67 | 68 | /// Removes all pre-existing keys from the UrlSearchParams struct 69 | /// and appends the new key/value. 70 | /// 71 | /// ``` 72 | /// use ada_url::UrlSearchParams; 73 | /// let mut params = UrlSearchParams::parse("a=1&b=2") 74 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 75 | /// params.set("a", "3"); 76 | /// assert_eq!(params.get("a"), Some("3")); 77 | /// ``` 78 | pub fn set(&mut self, key: &str, value: &str) { 79 | unsafe { 80 | ffi::ada_search_params_set( 81 | self.0, 82 | key.as_ptr().cast(), 83 | key.len(), 84 | value.as_ptr().cast(), 85 | value.len(), 86 | ) 87 | } 88 | } 89 | 90 | /// Removes a key from the UrlSearchParams struct. 91 | /// 92 | /// ``` 93 | /// use ada_url::UrlSearchParams; 94 | /// let mut params = UrlSearchParams::parse("a=1&b=2") 95 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 96 | /// params.remove_key("a"); 97 | /// assert_eq!(params.get("a"), None); 98 | /// ``` 99 | pub fn remove_key(&mut self, key: &str) { 100 | unsafe { ffi::ada_search_params_remove(self.0, key.as_ptr().cast(), key.len()) } 101 | } 102 | 103 | /// Removes a key with a value from the UrlSearchParams struct. 104 | /// 105 | /// ``` 106 | /// use ada_url::UrlSearchParams; 107 | /// let mut params = UrlSearchParams::parse("a=1&b=2") 108 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 109 | /// params.remove("a", "1"); 110 | /// assert_eq!(params.get("a"), None); 111 | /// ``` 112 | pub fn remove(&mut self, key: &str, value: &str) { 113 | unsafe { 114 | ffi::ada_search_params_remove_value( 115 | self.0, 116 | key.as_ptr().cast(), 117 | key.len(), 118 | value.as_ptr().cast(), 119 | value.len(), 120 | ) 121 | } 122 | } 123 | 124 | /// Returns whether the [`UrlSearchParams`] contains the `key`. 125 | /// 126 | /// ``` 127 | /// use ada_url::UrlSearchParams; 128 | /// let params = UrlSearchParams::parse("a=1&b=2") 129 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 130 | /// assert_eq!(params.contains_key("a"), true); 131 | /// ``` 132 | pub fn contains_key(&self, key: &str) -> bool { 133 | unsafe { ffi::ada_search_params_has(self.0, key.as_ptr().cast(), key.len()) } 134 | } 135 | 136 | /// Returns whether the [`UrlSearchParams`] contains the `key` with the `value`. 137 | /// 138 | /// ``` 139 | /// use ada_url::UrlSearchParams; 140 | /// let params = UrlSearchParams::parse("a=1&b=2") 141 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 142 | /// assert_eq!(params.contains("a", "1"), true); 143 | /// ``` 144 | pub fn contains(&self, key: &str, value: &str) -> bool { 145 | unsafe { 146 | ffi::ada_search_params_has_value( 147 | self.0, 148 | key.as_ptr().cast(), 149 | key.len(), 150 | value.as_ptr().cast(), 151 | value.len(), 152 | ) 153 | } 154 | } 155 | 156 | /// Returns the value of the key. 157 | /// 158 | /// ``` 159 | /// use ada_url::UrlSearchParams; 160 | /// let params = UrlSearchParams::parse("a=1&b=2") 161 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 162 | /// assert_eq!(params.get("a"), Some("1")); 163 | /// assert_eq!(params.get("c"), None); 164 | /// ``` 165 | pub fn get(&self, key: &str) -> Option<&str> { 166 | unsafe { 167 | let out = ffi::ada_search_params_get(self.0, key.as_ptr().cast(), key.len()); 168 | 169 | if out.data.is_null() { 170 | return None; 171 | } 172 | Some(out.as_str()) 173 | } 174 | } 175 | 176 | /// Returns all values of the key. 177 | /// 178 | /// ``` 179 | /// use ada_url::UrlSearchParams; 180 | /// let params = UrlSearchParams::parse("a=1&a=2") 181 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 182 | /// let pairs = params.get_all("a"); 183 | /// assert_eq!(pairs.len(), 2); 184 | /// ``` 185 | pub fn get_all(&self, key: &str) -> UrlSearchParamsEntry { 186 | unsafe { 187 | let strings = ffi::ada_search_params_get_all(self.0, key.as_ptr().cast(), key.len()); 188 | let size = ffi::ada_strings_size(strings); 189 | UrlSearchParamsEntry::new(strings, size) 190 | } 191 | } 192 | 193 | /// Returns all keys as an iterator 194 | /// 195 | /// ``` 196 | /// use ada_url::UrlSearchParams; 197 | /// let params = UrlSearchParams::parse("a=1") 198 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 199 | /// let mut keys = params.keys(); 200 | /// assert!(keys.next().is_some()); 201 | pub fn keys(&self) -> UrlSearchParamsKeyIterator { 202 | let iterator = unsafe { ffi::ada_search_params_get_keys(self.0) }; 203 | UrlSearchParamsKeyIterator::new(iterator) 204 | } 205 | 206 | /// Returns all values as an iterator 207 | /// 208 | /// ``` 209 | /// use ada_url::UrlSearchParams; 210 | /// let params = UrlSearchParams::parse("a=1") 211 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 212 | /// let mut values = params.values(); 213 | /// assert!(values.next().is_some()); 214 | pub fn values(&self) -> UrlSearchParamsValueIterator { 215 | let iterator = unsafe { ffi::ada_search_params_get_values(self.0) }; 216 | UrlSearchParamsValueIterator::new(iterator) 217 | } 218 | 219 | /// Returns all entries as an iterator 220 | /// 221 | /// ``` 222 | /// use ada_url::UrlSearchParams; 223 | /// let params = UrlSearchParams::parse("a=1") 224 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 225 | /// let mut entries = params.entries(); 226 | /// assert_eq!(entries.next(), Some(("a", "1"))); 227 | /// ``` 228 | pub fn entries(&self) -> UrlSearchParamsEntryIterator { 229 | let iterator = unsafe { ffi::ada_search_params_get_entries(self.0) }; 230 | UrlSearchParamsEntryIterator::new(iterator) 231 | } 232 | } 233 | 234 | #[cfg(feature = "std")] 235 | impl core::str::FromStr for UrlSearchParams { 236 | type Err = ParseUrlError>; 237 | 238 | fn from_str(s: &str) -> Result { 239 | Self::parse(s).map_err(|ParseUrlError { input }| ParseUrlError { 240 | input: input.into(), 241 | }) 242 | } 243 | } 244 | 245 | /// Returns the stringified version of the UrlSearchParams struct. 246 | /// 247 | /// ``` 248 | /// use ada_url::UrlSearchParams; 249 | /// let params = UrlSearchParams::parse("a=1&b=2") 250 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 251 | /// assert_eq!(params.to_string(), "a=1&b=2"); 252 | /// ``` 253 | impl core::fmt::Display for UrlSearchParams { 254 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 255 | let str = unsafe { ffi::ada_search_params_to_string(self.0) }; 256 | f.write_str(str.as_ref()) 257 | } 258 | } 259 | 260 | #[cfg(feature = "std")] 261 | impl Extend<(Input, Input)> for UrlSearchParams 262 | where 263 | Input: AsRef, 264 | { 265 | /// Supports extending UrlSearchParams through an iterator. 266 | /// 267 | ///``` 268 | /// use ada_url::UrlSearchParams; 269 | /// let mut params = UrlSearchParams::parse("a=1&b=2") 270 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 271 | /// assert_eq!(params.len(), 2); 272 | /// params.extend([("foo", "bar")]); 273 | /// assert_eq!(params.len(), 3); 274 | /// ``` 275 | fn extend>(&mut self, iter: T) { 276 | for item in iter { 277 | self.append(item.0.as_ref(), item.1.as_ref()); 278 | } 279 | } 280 | } 281 | 282 | #[cfg(feature = "std")] 283 | impl FromIterator<(Input, Input)> for UrlSearchParams 284 | where 285 | Input: AsRef, 286 | { 287 | /// Converts an iterator to UrlSearchParams 288 | /// 289 | /// ``` 290 | /// use ada_url::UrlSearchParams; 291 | /// let iterator = std::iter::repeat(("hello", "world")).take(5); 292 | /// let params = UrlSearchParams::from_iter(iterator); 293 | /// assert_eq!(params.len(), 5); 294 | /// ``` 295 | fn from_iter>(iter: T) -> Self { 296 | let mut params = UrlSearchParams::parse("") 297 | .expect("Should be able to parse empty string. This is likely due to a bug"); 298 | for item in iter { 299 | params.append(item.0.as_ref(), item.1.as_ref()); 300 | } 301 | params 302 | } 303 | } 304 | 305 | #[derive(Hash)] 306 | pub struct UrlSearchParamsKeyIterator<'a> { 307 | iterator: *mut ffi::ada_url_search_params_keys_iter, 308 | _phantom: core::marker::PhantomData<&'a str>, 309 | } 310 | 311 | impl Drop for UrlSearchParamsKeyIterator<'_> { 312 | fn drop(&mut self) { 313 | unsafe { ffi::ada_free_search_params_keys_iter(self.iterator) } 314 | } 315 | } 316 | 317 | impl<'a> Iterator for UrlSearchParamsKeyIterator<'a> { 318 | type Item = &'a str; 319 | 320 | fn next(&mut self) -> Option { 321 | let has_next = unsafe { ffi::ada_search_params_keys_iter_has_next(self.iterator) }; 322 | if has_next { 323 | let string = unsafe { ffi::ada_search_params_keys_iter_next(self.iterator) }; 324 | Some(string.as_str()) 325 | } else { 326 | None 327 | } 328 | } 329 | } 330 | 331 | #[derive(Hash)] 332 | pub struct UrlSearchParamsValueIterator<'a> { 333 | iterator: *mut ffi::ada_url_search_params_values_iter, 334 | _phantom: core::marker::PhantomData<&'a str>, 335 | } 336 | 337 | impl<'a> UrlSearchParamsKeyIterator<'a> { 338 | fn new(iterator: *mut ffi::ada_url_search_params_keys_iter) -> UrlSearchParamsKeyIterator<'a> { 339 | UrlSearchParamsKeyIterator { 340 | iterator, 341 | _phantom: core::marker::PhantomData, 342 | } 343 | } 344 | } 345 | 346 | impl Drop for UrlSearchParamsValueIterator<'_> { 347 | fn drop(&mut self) { 348 | unsafe { ffi::ada_free_search_params_values_iter(self.iterator) } 349 | } 350 | } 351 | 352 | impl<'a> Iterator for UrlSearchParamsValueIterator<'a> { 353 | type Item = &'a str; 354 | 355 | fn next(&mut self) -> Option { 356 | let has_next = unsafe { ffi::ada_search_params_values_iter_has_next(self.iterator) }; 357 | if has_next { 358 | let string = unsafe { ffi::ada_search_params_values_iter_next(self.iterator) }; 359 | Some(string.as_str()) 360 | } else { 361 | None 362 | } 363 | } 364 | } 365 | 366 | impl<'a> UrlSearchParamsValueIterator<'a> { 367 | fn new( 368 | iterator: *mut ffi::ada_url_search_params_values_iter, 369 | ) -> UrlSearchParamsValueIterator<'a> { 370 | UrlSearchParamsValueIterator { 371 | iterator, 372 | _phantom: core::marker::PhantomData, 373 | } 374 | } 375 | } 376 | 377 | pub struct UrlSearchParamsEntry<'a> { 378 | strings: *mut ffi::ada_strings, 379 | size: usize, 380 | _phantom: core::marker::PhantomData<&'a str>, 381 | } 382 | 383 | impl<'a> UrlSearchParamsEntry<'a> { 384 | fn new(strings: *mut ffi::ada_strings, size: usize) -> UrlSearchParamsEntry<'a> { 385 | UrlSearchParamsEntry { 386 | strings, 387 | size, 388 | _phantom: core::marker::PhantomData, 389 | } 390 | } 391 | 392 | /// Returns whether the key value pair is empty or not 393 | /// 394 | /// ``` 395 | /// use ada_url::UrlSearchParams; 396 | /// let params = UrlSearchParams::parse("a=1&b=2") 397 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 398 | /// let pairs = params.get_all("a"); 399 | /// assert_eq!(pairs.is_empty(), false); 400 | /// ``` 401 | pub fn is_empty(&self) -> bool { 402 | self.size == 0 403 | } 404 | 405 | /// Returns the size of the key value pairs 406 | /// 407 | /// ``` 408 | /// use ada_url::UrlSearchParams; 409 | /// let params = UrlSearchParams::parse("a=1&b=2") 410 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 411 | /// let pairs = params.get_all("a"); 412 | /// assert_eq!(pairs.len(), 1); 413 | /// ``` 414 | pub fn len(&self) -> usize { 415 | self.size 416 | } 417 | 418 | /// Get an entry by index 419 | /// 420 | /// ``` 421 | /// use ada_url::UrlSearchParams; 422 | /// let params = UrlSearchParams::parse("a=1&a=2") 423 | /// .expect("String should have been able to be parsed into an UrlSearchParams."); 424 | /// let pairs = params.get_all("a"); 425 | /// assert_eq!(pairs.len(), 2); 426 | /// assert_eq!(pairs.get(0), Some("1")); 427 | /// assert_eq!(pairs.get(1), Some("2")); 428 | /// assert_eq!(pairs.get(2), None); 429 | /// assert_eq!(pairs.get(55), None); 430 | /// ``` 431 | pub fn get(&self, index: usize) -> Option<&str> { 432 | if self.size == 0 || index > self.size - 1 { 433 | return None; 434 | } 435 | 436 | unsafe { 437 | let string = ffi::ada_strings_get(self.strings, index); 438 | Some(string.as_str()) 439 | } 440 | } 441 | } 442 | 443 | impl Drop for UrlSearchParamsEntry<'_> { 444 | fn drop(&mut self) { 445 | unsafe { ffi::ada_free_strings(self.strings) } 446 | } 447 | } 448 | 449 | #[cfg(feature = "std")] 450 | impl<'a> From> for Vec<&'a str> { 451 | fn from(val: UrlSearchParamsEntry<'a>) -> Self { 452 | let mut vec = Vec::with_capacity(val.size); 453 | unsafe { 454 | for index in 0..val.size { 455 | let string = ffi::ada_strings_get(val.strings, index); 456 | let slice = core::slice::from_raw_parts(string.data.cast(), string.length); 457 | vec.push(core::str::from_utf8_unchecked(slice)); 458 | } 459 | } 460 | vec 461 | } 462 | } 463 | 464 | #[derive(Hash)] 465 | pub struct UrlSearchParamsEntryIterator<'a> { 466 | iterator: *mut ffi::ada_url_search_params_entries_iter, 467 | _phantom: core::marker::PhantomData<&'a str>, 468 | } 469 | 470 | impl<'a> UrlSearchParamsEntryIterator<'a> { 471 | fn new( 472 | iterator: *mut ffi::ada_url_search_params_entries_iter, 473 | ) -> UrlSearchParamsEntryIterator<'a> { 474 | UrlSearchParamsEntryIterator { 475 | iterator, 476 | _phantom: core::marker::PhantomData, 477 | } 478 | } 479 | } 480 | 481 | impl Drop for UrlSearchParamsEntryIterator<'_> { 482 | fn drop(&mut self) { 483 | unsafe { ffi::ada_free_search_params_entries_iter(self.iterator) } 484 | } 485 | } 486 | 487 | impl<'a> Iterator for UrlSearchParamsEntryIterator<'a> { 488 | type Item = (&'a str, &'a str); 489 | 490 | fn next(&mut self) -> Option { 491 | let has_next = unsafe { ffi::ada_search_params_entries_iter_has_next(self.iterator) }; 492 | if has_next { 493 | let pair = unsafe { ffi::ada_search_params_entries_iter_next(self.iterator) }; 494 | Some((pair.key.as_str(), pair.value.as_str())) 495 | } else { 496 | None 497 | } 498 | } 499 | } 500 | --------------------------------------------------------------------------------