├── .devcontainer └── devcontainer.json ├── .gitattributes ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── benchmark.yml │ ├── deny.yml │ ├── links.yml │ ├── publish.yml │ ├── scorecard.yml │ ├── test.yml │ └── verify.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── _typos.toml ├── benches ├── equality.rs ├── get_values.rs ├── parse_as3257.rs └── parse_as3257_whois_response.rs ├── deny.toml ├── docs └── benchmark │ ├── README.md │ ├── RPSL-Parser │ ├── main.pl │ └── run.just │ ├── benchmark.just │ ├── graph-spec.json │ ├── graph.svg │ ├── irrdnet_irrd │ ├── main.py │ └── run.just │ └── whois-rpsl │ ├── AS3257.txt │ ├── pom.xml │ ├── run.just │ └── src │ └── main │ └── java │ └── parser │ └── TestParser.java ├── examples ├── print_parsed_as3257.rs └── print_parsed_as3257_whois_response.rs ├── justfile ├── src ├── attribute.rs ├── error.rs ├── lib.rs ├── object.rs └── parser │ ├── api.rs │ ├── core.rs │ └── mod.rs └── tests └── test_parsing.rs /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rust", 3 | "image": "mcr.microsoft.com/devcontainers/rust", 4 | "features": { 5 | "ghcr.io/devcontainers/features/github-cli:1": {}, 6 | "ghcr.io/guiyomh/features/just:0": {} 7 | }, 8 | "customizations": { 9 | "vscode": { 10 | "extensions": [ 11 | "rust-lang.rust-analyzer", 12 | "fill-labs.dependi", 13 | "tamasfe.even-better-toml", 14 | "nefrob.vscode-just-syntax" 15 | ], 16 | "settings": { 17 | "rust-analyzer.updates.askBeforeDownload": false 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Files impacting workflow security 2 | .github/ @SRv6d 3 | justfile @SRv6d 4 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | Thank you for considering contributing to `rpsl-rs`! 4 | This document intends to make contribution more accessible while aligning expectations. 5 | Please don't hesitate to open issues and PRs regardless if anything is unclear. 6 | 7 | By contributing to `rpsl-rs`, you agree that your code will be licensed under the terms of the MIT License without any additional terms or conditions. 8 | 9 | ## Getting Started 10 | 11 | To gain an overview of `rpsl-rs`, please read the [documentation](https://docs.rs/rpsl-rs). 12 | 13 | ## General Guidelines 14 | 15 | - Contributions of all sizes are welcome, including single line grammar / typo fixes. 16 | - For new features, documentation and tests are a requirement. 17 | - Changes must pass CI. PRs with failing CI will be treated as drafts unless you explicitly ask for help. 18 | - Simplicity is a core objective of `rpsl-rs`. Please open an issue before working on a new feature to discuss it. 19 | 20 | ## Development Environment 21 | 22 | ### Devcontainer 23 | 24 | For users of IDEs with support for devcontainers, it's usage is recommended. 25 | 26 | ### Other 27 | 28 | Ensure a [recent version of rustup](https://www.rust-lang.org/tools/install) is available and optionally install [`just`]. 29 | 30 | ## Coding Standards 31 | 32 | `rpsl-rs` uses [`rustfmt`](https://github.com/rust-lang/rustfmt) for uniform fomatting and [`clippy`](https://github.com/rust-lang/rust-clippy) for basic linting and enforcement of best practices. The [`just`] `lint` recipe can be used to run both. 33 | 34 | ```sh 35 | $ just lint 36 | cargo clippy --all-targets --all-features 37 | Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s 38 | cargo fmt --all --check 39 | ... 40 | ``` 41 | 42 | In addition to basic formatting and linting, a high code coverage should be maintained. Property based tests are used to ensure the parser can deal with any kind of RPSL input. All tests need to pass before a PR can be merged. 43 | 44 | ```sh 45 | $ just test 46 | ``` 47 | 48 | ## Performance 49 | 50 | To ensure that the parser stays performant, benchmarks are run on every PR. To execute them locally, run `cargo bench`. 51 | 52 | [`just`]: https://github.com/casey/just 53 | 54 | ## Releasing a new version 55 | 56 | To release a new version of `rpsl-rs`, perform the following steps. 57 | 58 | - Ensure the unreleased section of the [CHANGELOG](../CHANGELOG.md) contains all relevant changes. 59 | 60 | - Checkout a new branch. 61 | 62 | ```sh 63 | $ git switch -c bump-version-1-0-0 64 | ``` 65 | 66 | - Use the just recipe to bump the version. This will create the necessary commits as well as a pull request using the GitHub CLI. 67 | 68 | ```sh 69 | $ just bump-version 1.0.0 70 | ``` 71 | 72 | - Once the branch is merged, a GitHub release for the new version containing the recent changes can be created automatically. 73 | 74 | ```sh 75 | $ just release-latest-version 1.0.0 76 | ``` 77 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | github: [srv6d] 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | groups: 8 | GitHub Actions Minor or Patch: 9 | update-types: [minor, patch] 10 | - package-ecosystem: cargo 11 | directory: / 12 | groups: 13 | Cargo dev-dependencies: 14 | dependency-type: development 15 | schedule: 16 | interval: monthly 17 | - package-ecosystem: devcontainers 18 | directory: / 19 | schedule: 20 | interval: monthly 21 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Benchmark 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | benchmark: 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 18 | - uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 19 | with: 20 | toolchain: stable 21 | - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 22 | - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 23 | with: 24 | tool: cargo-codspeed 25 | 26 | - name: Build the benchmark target(s) 27 | run: cargo codspeed build 28 | - name: Run the benchmarks 29 | uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d 30 | with: 31 | run: cargo codspeed run 32 | token: ${{ secrets.CODSPEED_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/deny.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Cargo Deny 3 | 4 | on: 5 | pull_request: 6 | paths: 7 | - "**/Cargo.lock" 8 | - "**/Cargo.toml" 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | cargo-deny: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 19 | - uses: EmbarkStudios/cargo-deny-action@34899fc7ba81ca6268d5947a7a16b4649013fea1 20 | with: 21 | command: check bans licenses sources advisories 22 | -------------------------------------------------------------------------------- /.github/workflows/links.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Check Links 3 | 4 | on: 5 | repository_dispatch: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: 0 0 * * 0 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | link-check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 18 | - name: Check for broken links 19 | id: lychee 20 | uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 21 | - name: Create Issue From File 22 | if: env.lychee_exit_code != 0 23 | uses: peter-evans/create-issue-from-file@e8ef132d6df98ed982188e460ebb3b5d4ef3a9cd 24 | with: 25 | title: Link Checker Report 26 | content-filepath: ./lychee/out.md 27 | labels: report, automated issue 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish 3 | 4 | on: 5 | release: 6 | types: [published] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref_name }} 10 | cancel-in-progress: true 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | cargo-publish: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 20 | - uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 21 | with: 22 | toolchain: stable 23 | - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 24 | - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff 25 | 26 | - name: Publish 27 | run: just publish 28 | env: 29 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: OpenSSF Scorecard 3 | 4 | on: 5 | # For Branch-Protection check. Only the default branch is supported. See 6 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 7 | branch_protection_rule: 8 | # To guarantee Maintained check is occasionally updated. See 9 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 10 | schedule: 11 | - cron: "16 22 * * 5" 12 | push: 13 | branches: ["main"] 14 | workflow_dispatch: 15 | 16 | permissions: read-all 17 | 18 | jobs: 19 | analysis: 20 | runs-on: ubuntu-latest 21 | permissions: 22 | # Needed to upload the results to code-scanning dashboard. 23 | security-events: write 24 | # Needed to publish results and get a badge (see publish_results below). 25 | id-token: write 26 | 27 | steps: 28 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 29 | with: 30 | persist-credentials: false 31 | 32 | - name: Run analysis 33 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 34 | with: 35 | results_file: results.sarif 36 | results_format: sarif 37 | publish_results: true 38 | 39 | - name: Upload artifact 40 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v3.pre.node20 41 | with: 42 | name: SARIF file 43 | path: results.sarif 44 | retention-days: 5 45 | 46 | - name: "Upload to code-scanning dashboard" 47 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f 48 | with: 49 | sarif_file: results.sarif 50 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | id-token: write 13 | 14 | jobs: 15 | cargo-test: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | rust_version: ["1.80", "stable"] 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 22 | - uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 23 | with: 24 | toolchain: ${{ matrix.rust_version }} 25 | components: llvm-tools-preview 26 | - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 27 | - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 28 | with: 29 | tool: cargo-llvm-cov 30 | - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff 31 | 32 | - name: Test 33 | run: just test 34 | 35 | - name: Upload coverage reports to Codecov 36 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 37 | with: 38 | name: Rust ${{ matrix.rust_version }} 39 | files: lcov.info 40 | use_oidc: true 41 | fail_ci_if_error: true 42 | cargo-hack: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 46 | - uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 47 | with: 48 | toolchain: stable 49 | - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 50 | - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 51 | with: 52 | tool: cargo-hack 53 | - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff 54 | 55 | - name: Test 56 | run: just check-features 57 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Verify 3 | 4 | on: 5 | push: 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 16 | - uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 17 | with: 18 | toolchain: stable 19 | - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 20 | - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff 21 | 22 | - run: just lint 23 | check-lockfile: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 27 | - uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 28 | with: 29 | toolchain: stable 30 | - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 31 | - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff 32 | 33 | - run: just check-lockfile 34 | spellcheck: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 38 | - name: check for typos 39 | uses: crate-ci/typos@0f0ccba9ed1df83948f0c15026e4f5ccfce46109 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [2.0.0] - 2024-11-03 11 | 12 | ### Added 13 | 14 | - A Changelog. 15 | - MSRV policy to README. 16 | - Tracking of test coverage. 17 | - Security policy. 18 | - Serde and JSON object serialization. 19 | 20 | ### Changed 21 | 22 | - Allow for extended ASCII chars in attribute values. 23 | - Parser now returns a single type that can represent both owned and borrowed values. 24 | 25 | ### Fixed 26 | 27 | - Empty multiline attribute values no longer display as empty whitespace. 28 | 29 | ### Internal 30 | 31 | - Replaced nom parser with winnow. 32 | - Improved test coverage to get close to 100%. 33 | 34 | ## [1.0.1] - 2024-12-26 35 | 36 | ### Added 37 | 38 | - Rust MSRV. 39 | - Contribution guidelines. 40 | 41 | ## 1.0.0 - 2023-12-26 42 | 43 | ### Added 44 | 45 | - Validation that newly created attribute names start with alphabetic and end with alphanumeric characters. 46 | - CI benchmarks using codspeed. 47 | - Distinct types for multi line / multi value RPSL values. 48 | - Improved conversion traits. 49 | - Property based testing of the parser using proptest. 50 | - Improvement of parsing speed by 38%. 51 | - `object!` macro to simplify object creation. 52 | 53 | ### Changed 54 | 55 | - Parse RPSL into types containing string references instead of making assignments for each attribute. 56 | - Simplified functions exposed by the API. 57 | - Crate name to rpsl-rs 58 | 59 | ### Removed 60 | 61 | - Python bindings to allow for decoupled development in separate repository. 62 | 63 | ### Internal 64 | 65 | - Complete refactor 66 | 67 | [unreleased]: https://github.com/SRv6d/rpsl-rs/compare/v2.0.0...HEAD 68 | [2.0.0]: https://github.com/SRv6d/rpsl-rs/compare/v1.0.1...v2.0.0 69 | [1.0.1]: https://github.com/SRv6d/rpsl-rs/compare/v1.0.0...v1.0.1 70 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anes" 16 | version = "0.1.6" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.4.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 31 | 32 | [[package]] 33 | name = "bit-set" 34 | version = "0.8.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" 37 | dependencies = [ 38 | "bit-vec", 39 | ] 40 | 41 | [[package]] 42 | name = "bit-vec" 43 | version = "0.8.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 46 | 47 | [[package]] 48 | name = "bitflags" 49 | version = "2.6.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 52 | 53 | [[package]] 54 | name = "bumpalo" 55 | version = "3.16.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 58 | 59 | [[package]] 60 | name = "byteorder" 61 | version = "1.5.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 64 | 65 | [[package]] 66 | name = "cast" 67 | version = "0.3.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 70 | 71 | [[package]] 72 | name = "cfg-if" 73 | version = "1.0.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 76 | 77 | [[package]] 78 | name = "ciborium" 79 | version = "0.2.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 82 | dependencies = [ 83 | "ciborium-io", 84 | "ciborium-ll", 85 | "serde", 86 | ] 87 | 88 | [[package]] 89 | name = "ciborium-io" 90 | version = "0.2.2" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 93 | 94 | [[package]] 95 | name = "ciborium-ll" 96 | version = "0.2.2" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 99 | dependencies = [ 100 | "ciborium-io", 101 | "half", 102 | ] 103 | 104 | [[package]] 105 | name = "clap" 106 | version = "4.5.23" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" 109 | dependencies = [ 110 | "clap_builder", 111 | ] 112 | 113 | [[package]] 114 | name = "clap_builder" 115 | version = "4.5.23" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" 118 | dependencies = [ 119 | "anstyle", 120 | "clap_lex", 121 | ] 122 | 123 | [[package]] 124 | name = "clap_lex" 125 | version = "0.7.4" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 128 | 129 | [[package]] 130 | name = "codspeed" 131 | version = "2.10.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "93f4cce9c27c49c4f101fffeebb1826f41a9df2e7498b7cd4d95c0658b796c6c" 134 | dependencies = [ 135 | "colored", 136 | "libc", 137 | "serde", 138 | "serde_json", 139 | "uuid", 140 | ] 141 | 142 | [[package]] 143 | name = "codspeed-criterion-compat" 144 | version = "2.10.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "c3c23d880a28a2aab52d38ca8481dd7a3187157d0a952196b6db1db3c8499725" 147 | dependencies = [ 148 | "codspeed", 149 | "codspeed-criterion-compat-walltime", 150 | "colored", 151 | ] 152 | 153 | [[package]] 154 | name = "codspeed-criterion-compat-walltime" 155 | version = "2.10.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "7b0a2f7365e347f4f22a67e9ea689bf7bc89900a354e22e26cf8a531a42c8fbb" 158 | dependencies = [ 159 | "anes", 160 | "cast", 161 | "ciborium", 162 | "clap", 163 | "codspeed", 164 | "criterion-plot", 165 | "is-terminal", 166 | "itertools 0.10.5", 167 | "num-traits", 168 | "once_cell", 169 | "oorandom", 170 | "plotters", 171 | "rayon", 172 | "regex", 173 | "serde", 174 | "serde_derive", 175 | "serde_json", 176 | "tinytemplate", 177 | "walkdir", 178 | ] 179 | 180 | [[package]] 181 | name = "colored" 182 | version = "2.2.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 185 | dependencies = [ 186 | "lazy_static", 187 | "windows-sys 0.59.0", 188 | ] 189 | 190 | [[package]] 191 | name = "criterion" 192 | version = "0.6.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" 195 | dependencies = [ 196 | "anes", 197 | "cast", 198 | "ciborium", 199 | "clap", 200 | "criterion-plot", 201 | "itertools 0.13.0", 202 | "num-traits", 203 | "oorandom", 204 | "plotters", 205 | "rayon", 206 | "regex", 207 | "serde", 208 | "serde_json", 209 | "tinytemplate", 210 | "walkdir", 211 | ] 212 | 213 | [[package]] 214 | name = "criterion-plot" 215 | version = "0.5.0" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 218 | dependencies = [ 219 | "cast", 220 | "itertools 0.10.5", 221 | ] 222 | 223 | [[package]] 224 | name = "crossbeam-deque" 225 | version = "0.8.6" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 228 | dependencies = [ 229 | "crossbeam-epoch", 230 | "crossbeam-utils", 231 | ] 232 | 233 | [[package]] 234 | name = "crossbeam-epoch" 235 | version = "0.9.18" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 238 | dependencies = [ 239 | "crossbeam-utils", 240 | ] 241 | 242 | [[package]] 243 | name = "crossbeam-utils" 244 | version = "0.8.21" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 247 | 248 | [[package]] 249 | name = "crunchy" 250 | version = "0.2.2" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 253 | 254 | [[package]] 255 | name = "either" 256 | version = "1.13.0" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 259 | 260 | [[package]] 261 | name = "equivalent" 262 | version = "1.0.1" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 265 | 266 | [[package]] 267 | name = "errno" 268 | version = "0.3.10" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 271 | dependencies = [ 272 | "libc", 273 | "windows-sys 0.59.0", 274 | ] 275 | 276 | [[package]] 277 | name = "fastrand" 278 | version = "2.3.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 281 | 282 | [[package]] 283 | name = "fnv" 284 | version = "1.0.7" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 287 | 288 | [[package]] 289 | name = "futures-core" 290 | version = "0.3.31" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 293 | 294 | [[package]] 295 | name = "futures-macro" 296 | version = "0.3.31" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 299 | dependencies = [ 300 | "proc-macro2", 301 | "quote", 302 | "syn", 303 | ] 304 | 305 | [[package]] 306 | name = "futures-task" 307 | version = "0.3.31" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 310 | 311 | [[package]] 312 | name = "futures-timer" 313 | version = "3.0.3" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 316 | 317 | [[package]] 318 | name = "futures-util" 319 | version = "0.3.31" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 322 | dependencies = [ 323 | "futures-core", 324 | "futures-macro", 325 | "futures-task", 326 | "pin-project-lite", 327 | "pin-utils", 328 | "slab", 329 | ] 330 | 331 | [[package]] 332 | name = "getrandom" 333 | version = "0.2.15" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 336 | dependencies = [ 337 | "cfg-if", 338 | "libc", 339 | "wasi 0.11.0+wasi-snapshot-preview1", 340 | ] 341 | 342 | [[package]] 343 | name = "getrandom" 344 | version = "0.3.1" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 347 | dependencies = [ 348 | "cfg-if", 349 | "libc", 350 | "wasi 0.13.3+wasi-0.2.2", 351 | "windows-targets", 352 | ] 353 | 354 | [[package]] 355 | name = "glob" 356 | version = "0.3.1" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 359 | 360 | [[package]] 361 | name = "half" 362 | version = "2.4.1" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 365 | dependencies = [ 366 | "cfg-if", 367 | "crunchy", 368 | ] 369 | 370 | [[package]] 371 | name = "hashbrown" 372 | version = "0.15.2" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 375 | 376 | [[package]] 377 | name = "hermit-abi" 378 | version = "0.4.0" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 381 | 382 | [[package]] 383 | name = "indexmap" 384 | version = "2.7.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 387 | dependencies = [ 388 | "equivalent", 389 | "hashbrown", 390 | ] 391 | 392 | [[package]] 393 | name = "is-terminal" 394 | version = "0.4.13" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" 397 | dependencies = [ 398 | "hermit-abi", 399 | "libc", 400 | "windows-sys 0.52.0", 401 | ] 402 | 403 | [[package]] 404 | name = "itertools" 405 | version = "0.10.5" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 408 | dependencies = [ 409 | "either", 410 | ] 411 | 412 | [[package]] 413 | name = "itertools" 414 | version = "0.13.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 417 | dependencies = [ 418 | "either", 419 | ] 420 | 421 | [[package]] 422 | name = "itoa" 423 | version = "1.0.14" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 426 | 427 | [[package]] 428 | name = "js-sys" 429 | version = "0.3.76" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" 432 | dependencies = [ 433 | "once_cell", 434 | "wasm-bindgen", 435 | ] 436 | 437 | [[package]] 438 | name = "lazy_static" 439 | version = "1.5.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 442 | 443 | [[package]] 444 | name = "libc" 445 | version = "0.2.169" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 448 | 449 | [[package]] 450 | name = "linux-raw-sys" 451 | version = "0.4.14" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 454 | 455 | [[package]] 456 | name = "log" 457 | version = "0.4.22" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 460 | 461 | [[package]] 462 | name = "memchr" 463 | version = "2.7.4" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 466 | 467 | [[package]] 468 | name = "num-traits" 469 | version = "0.2.19" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 472 | dependencies = [ 473 | "autocfg", 474 | ] 475 | 476 | [[package]] 477 | name = "once_cell" 478 | version = "1.20.2" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 481 | 482 | [[package]] 483 | name = "oorandom" 484 | version = "11.1.4" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" 487 | 488 | [[package]] 489 | name = "pin-project-lite" 490 | version = "0.2.15" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 493 | 494 | [[package]] 495 | name = "pin-utils" 496 | version = "0.1.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 499 | 500 | [[package]] 501 | name = "plotters" 502 | version = "0.3.7" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" 505 | dependencies = [ 506 | "num-traits", 507 | "plotters-backend", 508 | "plotters-svg", 509 | "wasm-bindgen", 510 | "web-sys", 511 | ] 512 | 513 | [[package]] 514 | name = "plotters-backend" 515 | version = "0.3.7" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" 518 | 519 | [[package]] 520 | name = "plotters-svg" 521 | version = "0.3.7" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" 524 | dependencies = [ 525 | "plotters-backend", 526 | ] 527 | 528 | [[package]] 529 | name = "ppv-lite86" 530 | version = "0.2.20" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 533 | dependencies = [ 534 | "zerocopy", 535 | ] 536 | 537 | [[package]] 538 | name = "proc-macro-crate" 539 | version = "3.2.0" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" 542 | dependencies = [ 543 | "toml_edit", 544 | ] 545 | 546 | [[package]] 547 | name = "proc-macro2" 548 | version = "1.0.92" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 551 | dependencies = [ 552 | "unicode-ident", 553 | ] 554 | 555 | [[package]] 556 | name = "proptest" 557 | version = "1.6.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" 560 | dependencies = [ 561 | "bit-set", 562 | "bit-vec", 563 | "bitflags", 564 | "lazy_static", 565 | "num-traits", 566 | "rand", 567 | "rand_chacha", 568 | "rand_xorshift", 569 | "regex-syntax", 570 | "rusty-fork", 571 | "tempfile", 572 | "unarray", 573 | ] 574 | 575 | [[package]] 576 | name = "quick-error" 577 | version = "1.2.3" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 580 | 581 | [[package]] 582 | name = "quote" 583 | version = "1.0.37" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 586 | dependencies = [ 587 | "proc-macro2", 588 | ] 589 | 590 | [[package]] 591 | name = "rand" 592 | version = "0.8.5" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 595 | dependencies = [ 596 | "libc", 597 | "rand_chacha", 598 | "rand_core", 599 | ] 600 | 601 | [[package]] 602 | name = "rand_chacha" 603 | version = "0.3.1" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 606 | dependencies = [ 607 | "ppv-lite86", 608 | "rand_core", 609 | ] 610 | 611 | [[package]] 612 | name = "rand_core" 613 | version = "0.6.4" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 616 | dependencies = [ 617 | "getrandom 0.2.15", 618 | ] 619 | 620 | [[package]] 621 | name = "rand_xorshift" 622 | version = "0.3.0" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" 625 | dependencies = [ 626 | "rand_core", 627 | ] 628 | 629 | [[package]] 630 | name = "rayon" 631 | version = "1.10.0" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 634 | dependencies = [ 635 | "either", 636 | "rayon-core", 637 | ] 638 | 639 | [[package]] 640 | name = "rayon-core" 641 | version = "1.12.1" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 644 | dependencies = [ 645 | "crossbeam-deque", 646 | "crossbeam-utils", 647 | ] 648 | 649 | [[package]] 650 | name = "regex" 651 | version = "1.11.1" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 654 | dependencies = [ 655 | "aho-corasick", 656 | "memchr", 657 | "regex-automata", 658 | "regex-syntax", 659 | ] 660 | 661 | [[package]] 662 | name = "regex-automata" 663 | version = "0.4.9" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 666 | dependencies = [ 667 | "aho-corasick", 668 | "memchr", 669 | "regex-syntax", 670 | ] 671 | 672 | [[package]] 673 | name = "regex-syntax" 674 | version = "0.8.5" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 677 | 678 | [[package]] 679 | name = "relative-path" 680 | version = "1.9.3" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" 683 | 684 | [[package]] 685 | name = "rpsl-rs" 686 | version = "2.0.0" 687 | dependencies = [ 688 | "codspeed-criterion-compat", 689 | "criterion", 690 | "proptest", 691 | "rstest", 692 | "serde", 693 | "serde_json", 694 | "serde_test", 695 | "thiserror", 696 | "winnow 0.7.10", 697 | ] 698 | 699 | [[package]] 700 | name = "rstest" 701 | version = "0.25.0" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" 704 | dependencies = [ 705 | "futures-timer", 706 | "futures-util", 707 | "rstest_macros", 708 | "rustc_version", 709 | ] 710 | 711 | [[package]] 712 | name = "rstest_macros" 713 | version = "0.25.0" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" 716 | dependencies = [ 717 | "cfg-if", 718 | "glob", 719 | "proc-macro-crate", 720 | "proc-macro2", 721 | "quote", 722 | "regex", 723 | "relative-path", 724 | "rustc_version", 725 | "syn", 726 | "unicode-ident", 727 | ] 728 | 729 | [[package]] 730 | name = "rustc_version" 731 | version = "0.4.1" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 734 | dependencies = [ 735 | "semver", 736 | ] 737 | 738 | [[package]] 739 | name = "rustix" 740 | version = "0.38.42" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" 743 | dependencies = [ 744 | "bitflags", 745 | "errno", 746 | "libc", 747 | "linux-raw-sys", 748 | "windows-sys 0.59.0", 749 | ] 750 | 751 | [[package]] 752 | name = "rusty-fork" 753 | version = "0.3.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" 756 | dependencies = [ 757 | "fnv", 758 | "quick-error", 759 | "tempfile", 760 | "wait-timeout", 761 | ] 762 | 763 | [[package]] 764 | name = "ryu" 765 | version = "1.0.18" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 768 | 769 | [[package]] 770 | name = "same-file" 771 | version = "1.0.6" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 774 | dependencies = [ 775 | "winapi-util", 776 | ] 777 | 778 | [[package]] 779 | name = "semver" 780 | version = "1.0.24" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" 783 | 784 | [[package]] 785 | name = "serde" 786 | version = "1.0.219" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 789 | dependencies = [ 790 | "serde_derive", 791 | ] 792 | 793 | [[package]] 794 | name = "serde_derive" 795 | version = "1.0.219" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 798 | dependencies = [ 799 | "proc-macro2", 800 | "quote", 801 | "syn", 802 | ] 803 | 804 | [[package]] 805 | name = "serde_json" 806 | version = "1.0.140" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 809 | dependencies = [ 810 | "itoa", 811 | "memchr", 812 | "ryu", 813 | "serde", 814 | ] 815 | 816 | [[package]] 817 | name = "serde_test" 818 | version = "1.0.177" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" 821 | dependencies = [ 822 | "serde", 823 | ] 824 | 825 | [[package]] 826 | name = "slab" 827 | version = "0.4.9" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 830 | dependencies = [ 831 | "autocfg", 832 | ] 833 | 834 | [[package]] 835 | name = "syn" 836 | version = "2.0.91" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" 839 | dependencies = [ 840 | "proc-macro2", 841 | "quote", 842 | "unicode-ident", 843 | ] 844 | 845 | [[package]] 846 | name = "tempfile" 847 | version = "3.14.0" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" 850 | dependencies = [ 851 | "cfg-if", 852 | "fastrand", 853 | "once_cell", 854 | "rustix", 855 | "windows-sys 0.59.0", 856 | ] 857 | 858 | [[package]] 859 | name = "thiserror" 860 | version = "2.0.12" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 863 | dependencies = [ 864 | "thiserror-impl", 865 | ] 866 | 867 | [[package]] 868 | name = "thiserror-impl" 869 | version = "2.0.12" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 872 | dependencies = [ 873 | "proc-macro2", 874 | "quote", 875 | "syn", 876 | ] 877 | 878 | [[package]] 879 | name = "tinytemplate" 880 | version = "1.2.1" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 883 | dependencies = [ 884 | "serde", 885 | "serde_json", 886 | ] 887 | 888 | [[package]] 889 | name = "toml_datetime" 890 | version = "0.6.8" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 893 | 894 | [[package]] 895 | name = "toml_edit" 896 | version = "0.22.22" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 899 | dependencies = [ 900 | "indexmap", 901 | "toml_datetime", 902 | "winnow 0.6.21", 903 | ] 904 | 905 | [[package]] 906 | name = "unarray" 907 | version = "0.1.4" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" 910 | 911 | [[package]] 912 | name = "unicode-ident" 913 | version = "1.0.14" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 916 | 917 | [[package]] 918 | name = "uuid" 919 | version = "1.15.1" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" 922 | dependencies = [ 923 | "getrandom 0.3.1", 924 | ] 925 | 926 | [[package]] 927 | name = "wait-timeout" 928 | version = "0.2.0" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 931 | dependencies = [ 932 | "libc", 933 | ] 934 | 935 | [[package]] 936 | name = "walkdir" 937 | version = "2.5.0" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 940 | dependencies = [ 941 | "same-file", 942 | "winapi-util", 943 | ] 944 | 945 | [[package]] 946 | name = "wasi" 947 | version = "0.11.0+wasi-snapshot-preview1" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 950 | 951 | [[package]] 952 | name = "wasi" 953 | version = "0.13.3+wasi-0.2.2" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 956 | dependencies = [ 957 | "wit-bindgen-rt", 958 | ] 959 | 960 | [[package]] 961 | name = "wasm-bindgen" 962 | version = "0.2.99" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" 965 | dependencies = [ 966 | "cfg-if", 967 | "once_cell", 968 | "wasm-bindgen-macro", 969 | ] 970 | 971 | [[package]] 972 | name = "wasm-bindgen-backend" 973 | version = "0.2.99" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" 976 | dependencies = [ 977 | "bumpalo", 978 | "log", 979 | "proc-macro2", 980 | "quote", 981 | "syn", 982 | "wasm-bindgen-shared", 983 | ] 984 | 985 | [[package]] 986 | name = "wasm-bindgen-macro" 987 | version = "0.2.99" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" 990 | dependencies = [ 991 | "quote", 992 | "wasm-bindgen-macro-support", 993 | ] 994 | 995 | [[package]] 996 | name = "wasm-bindgen-macro-support" 997 | version = "0.2.99" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" 1000 | dependencies = [ 1001 | "proc-macro2", 1002 | "quote", 1003 | "syn", 1004 | "wasm-bindgen-backend", 1005 | "wasm-bindgen-shared", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "wasm-bindgen-shared" 1010 | version = "0.2.99" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" 1013 | 1014 | [[package]] 1015 | name = "web-sys" 1016 | version = "0.3.76" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" 1019 | dependencies = [ 1020 | "js-sys", 1021 | "wasm-bindgen", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "winapi-util" 1026 | version = "0.1.9" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1029 | dependencies = [ 1030 | "windows-sys 0.59.0", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "windows-sys" 1035 | version = "0.52.0" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1038 | dependencies = [ 1039 | "windows-targets", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "windows-sys" 1044 | version = "0.59.0" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1047 | dependencies = [ 1048 | "windows-targets", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "windows-targets" 1053 | version = "0.52.6" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1056 | dependencies = [ 1057 | "windows_aarch64_gnullvm", 1058 | "windows_aarch64_msvc", 1059 | "windows_i686_gnu", 1060 | "windows_i686_gnullvm", 1061 | "windows_i686_msvc", 1062 | "windows_x86_64_gnu", 1063 | "windows_x86_64_gnullvm", 1064 | "windows_x86_64_msvc", 1065 | ] 1066 | 1067 | [[package]] 1068 | name = "windows_aarch64_gnullvm" 1069 | version = "0.52.6" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1072 | 1073 | [[package]] 1074 | name = "windows_aarch64_msvc" 1075 | version = "0.52.6" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1078 | 1079 | [[package]] 1080 | name = "windows_i686_gnu" 1081 | version = "0.52.6" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1084 | 1085 | [[package]] 1086 | name = "windows_i686_gnullvm" 1087 | version = "0.52.6" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1090 | 1091 | [[package]] 1092 | name = "windows_i686_msvc" 1093 | version = "0.52.6" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1096 | 1097 | [[package]] 1098 | name = "windows_x86_64_gnu" 1099 | version = "0.52.6" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1102 | 1103 | [[package]] 1104 | name = "windows_x86_64_gnullvm" 1105 | version = "0.52.6" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1108 | 1109 | [[package]] 1110 | name = "windows_x86_64_msvc" 1111 | version = "0.52.6" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1114 | 1115 | [[package]] 1116 | name = "winnow" 1117 | version = "0.6.21" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "e6f5bb5257f2407a5425c6e749bfd9692192a73e70a6060516ac04f889087d68" 1120 | dependencies = [ 1121 | "memchr", 1122 | ] 1123 | 1124 | [[package]] 1125 | name = "winnow" 1126 | version = "0.7.10" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 1129 | dependencies = [ 1130 | "memchr", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "wit-bindgen-rt" 1135 | version = "0.33.0" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 1138 | dependencies = [ 1139 | "bitflags", 1140 | ] 1141 | 1142 | [[package]] 1143 | name = "zerocopy" 1144 | version = "0.7.35" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1147 | dependencies = [ 1148 | "byteorder", 1149 | "zerocopy-derive", 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "zerocopy-derive" 1154 | version = "0.7.35" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1157 | dependencies = [ 1158 | "proc-macro2", 1159 | "quote", 1160 | "syn", 1161 | ] 1162 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rpsl-rs" 3 | description = "A Routing Policy Specification Language (RPSL) parser with a focus on speed and correctness." 4 | version = "2.0.0" 5 | keywords = ["rpsl", "parser", "routing", "policy", "whois"] 6 | categories = ["parsing", "database"] 7 | edition = "2021" 8 | authors = ["Marvin Vogt "] 9 | license = "MIT" 10 | homepage = "https://github.com/srv6d/rpsl-rs" 11 | repository = "https://github.com/srv6d/rpsl-rs" 12 | readme = "README.md" 13 | exclude = [".devcontainer", ".github", "doc/benchmark/**", "tests/**"] 14 | # Make sure to also adjust in README and CI 15 | rust-version = "1.80" 16 | 17 | [package.metadata.docs.rs] 18 | all-features = true 19 | rustdoc-args = ["--cfg", "docsrs"] 20 | 21 | [lib] 22 | name = "rpsl" 23 | crate-type = ["cdylib", "rlib"] 24 | 25 | [dependencies] 26 | winnow = "0.7.10" 27 | thiserror = "2.0.12" 28 | serde = { version = "1.0.219", features = ["derive"], optional = true } 29 | serde_json = { version = "1.0.140", optional = true } 30 | 31 | [dev-dependencies] 32 | codspeed-criterion-compat = "=2.10.1" 33 | criterion = "=0.6.0" 34 | proptest = "=1.6.0" 35 | rstest = "=0.25.0" 36 | serde_json = "=1.0.140" 37 | serde_test = "=1.0.177" 38 | 39 | [features] 40 | default = ["simd"] 41 | simd = ["winnow/simd"] 42 | serde = ["dep:serde"] 43 | json = ["serde", "dep:serde_json"] 44 | 45 | [[bench]] 46 | name = "parse_as3257" 47 | harness = false 48 | 49 | [[bench]] 50 | name = "parse_as3257_whois_response" 51 | harness = false 52 | 53 | [[bench]] 54 | name = "equality" 55 | harness = false 56 | 57 | [[bench]] 58 | name = "get_values" 59 | harness = false 60 | 61 | [profile.release] 62 | lto = true 63 | 64 | [profile.test.package.proptest] 65 | opt-level = 3 66 | 67 | [profile.test.package.rand_chacha] 68 | opt-level = 3 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Marvin Vogt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

rpsl-rs

2 |
3 | 4 | CI status 5 | 6 | 7 | 8 | 9 | 10 | CodSpeed Badge 11 | 12 | 13 | Cargo version 14 | 15 | 16 | Rust version 17 | 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 | A Routing Policy Specification Language (RPSL) parser with a focus on speed and correctness. 30 | 31 | ⚡️ 130-250x faster than other parsers\ 32 | 📰 Complete implementation for multiline RPSL values\ 33 | 💬 Able to parse objects directly from whois server responses\ 34 | 🧠 Low memory footprint by leveraging zero-copy\ 35 | 🧪 Robust parsing of any valid input ensured by Property Based Tests 36 | 37 | [](docs/benchmark) 38 | 39 | [**Docs**](https://docs.rs/rpsl-rs/latest/rpsl/) | [**Performance**](https://github.com/SRv6d/rpsl-rs/tree/main/docs/benchmark) 40 | 41 | ## Usage 42 | 43 | ### Parsing RPSL objects 44 | 45 | A string containing an object in RPSL notation can be parsed to an [Object] using the [parse_object] function. 46 | 47 | ```rust 48 | use rpsl::parse_object; 49 | 50 | let role_acme = " 51 | role: ACME Company 52 | address: Packet Street 6 53 | address: 128 Series of Tubes 54 | address: Internet 55 | email: rpsl-rs@github.com 56 | nic-hdl: RPSL1-RIPE 57 | source: RIPE 58 | 59 | "; 60 | let parsed = parse_object(role_acme).unwrap(); 61 | ``` 62 | 63 | The returned [Object] allows access to the attributes contained within in form of [Attribute]s. 64 | 65 | ```rust,ignore 66 | println!("{:#?}", parsed); 67 | 68 | Object( 69 | [ 70 | Attribute { 71 | name: Name("role"), 72 | value: SingleLine(Some("ACME Company")), 73 | }, 74 | Attribute { 75 | name: Name("address"), 76 | value: SingleLine(Some("Packet Street 6")), 77 | }, 78 | Attribute { 79 | name: Name("address"), 80 | value: SingleLine(Some("128 Series of Tubes")), 81 | }, 82 | Attribute { 83 | name: Name("address"), 84 | value: SingleLine(Some("Internet")), 85 | }, 86 | Attribute { 87 | name: Name("email"), 88 | value: SingleLine(Some("rpsl-rs@github.com")), 89 | }, 90 | Attribute { 91 | name: Name("nic-hdl"), 92 | value: SingleLine(Some("RPSL1-RIPE")), 93 | }, 94 | Attribute { 95 | name: Name("source"), 96 | value: SingleLine(Some("RIPE")), 97 | }, 98 | ] 99 | ) 100 | ``` 101 | 102 | [Object]s created from RPSL text use string references that point to attributes and their values 103 | instead of copying them. 104 | 105 | ```text 106 | role: ACME Company ◀─────────────── &"role": &"ACME Company" 107 | address: Packet Street 6 ◀──────────── &"address": &"Packet Street 6" 108 | address: 128 Series of Tubes ◀──────── &"address": &"128 Series of Tubes" 109 | address: Internet ◀─────────────────── &"address": &"Internet" 110 | email: rpsl-rs@github.com ◀───────── &"email": &"rpsl-rs@github.com" 111 | nic-hdl: RPSL1-RIPE ◀───────────────── &"nic-hdl": &"RPSL1-RIPE" 112 | source: RIPE ◀─────────────────────── &"source": &"RIPE" 113 | ``` 114 | 115 | This is what makes `rpsl-rs` performant and memory efficient, since no additional allocation is required during parsing. 116 | 117 | Each [Attribute] can be accessed by its index and has a name and value. 118 | 119 | ```rust,ignore 120 | println!("{:#?}", parsed[1]); 121 | 122 | Attribute { 123 | name: Name("address"), 124 | value: SingleLine(Some("Packet Street 6")), 125 | } 126 | ``` 127 | 128 | Since RPSL attribute values can either be single- or multiline, two different variants are used to represent them. See [Attribute] and [parse_object] for more details and examples. 129 | 130 | ### Parsing a WHOIS server response 131 | 132 | WHOIS servers often respond to queries by returning multiple related objects. 133 | An example ARIN query for `AS32934` will return with the requested `ASNumber` object first, followed by its associated `OrgName`: 134 | 135 | ```sh 136 | $ whois -h whois.arin.net AS32934 137 | ASNumber: 32934 138 | ASName: FACEBOOK 139 | ASHandle: AS32934 140 | RegDate: 2004-08-24 141 | Updated: 2012-02-24 142 | Comment: Please send abuse reports to abuse@facebook.com 143 | Ref: https://rdap.arin.net/registry/autnum/32934 144 | 145 | 146 | OrgName: Facebook, Inc. 147 | OrgId: THEFA-3 148 | Address: 1601 Willow Rd. 149 | City: Menlo Park 150 | StateProv: CA 151 | PostalCode: 94025 152 | Country: US 153 | RegDate: 2004-08-11 154 | Updated: 2012-04-17 155 | Ref: https://rdap.arin.net/registry/entity/THEFA-3 156 | 157 | 158 | ``` 159 | 160 | To extract each individual object, the [parse_whois_response] function can be used to parse the response into a `Vec` containing all individual [Object]s within the response. Examples can be found in the function documentation. 161 | 162 | ## Optional Features 163 | 164 | The following cargo features can be used to enable additional functionality. 165 | 166 | - **simd** _(enabled by default)_: Enables the [Winnow] simd feature which improves string search performance using simd. 167 | - **serde**: Enables [Object] serialization using [Serde]. 168 | - **json**: Provides JSON serialization of an [Object] using [Serde JSON]. 169 | 170 | ## MSRV Policy 171 | 172 | This project requires the minimum supported Rust version to be at least 6 months old. 173 | As long as this requirement is met, the MSRV may be increased as necessary through a minor version update. 174 | For the currently configured MSRV, please check [Cargo.toml](Cargo.toml). 175 | 176 | ## Contributing 177 | 178 | Contributions of all sizes that improve `rpsl-rs` in any way, be it DX/UX, documentation, performance or other are highly appreciated. 179 | To get started, please read the [contribution guidelines](.github/CONTRIBUTING.md). Before starting work on a new feature you would like to contribute that may impact simplicity, reliability or performance, please open an issue first. 180 | 181 | ## License 182 | 183 | The source code of this project is licensed under the MIT License. For more information, see [LICENSE](LICENSE). 184 | 185 | [Object]: https://docs.rs/rpsl-rs/latest/rpsl/struct.Object.html 186 | [Attribute]: https://docs.rs/rpsl-rs/latest/rpsl/struct.Attribute.html 187 | [parse_object]: https://docs.rs/rpsl-rs/latest/rpsl/fn.parse_object.html 188 | [parse_whois_response]: https://docs.rs/rpsl-rs/latest/rpsl/fn.parse_whois_response.html 189 | [Winnow]: https://github.com/winnow-rs/winnow 190 | [Serde]: https://github.com/serde-rs/serde 191 | [Serde JSON]: https://github.com/serde-rs/json 192 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you think you have identified a security issue within this project, please **do not open a public issue**. 4 | 5 | To responsibly report a security issue, 6 | 7 | 1. Navigate to the main page of the repository on GitHub. 8 | 2. Under the repository name, click Security. If you cannot see the "Security" tab, select the dropdown menu, and then click Security. 9 | 3. Click [Report a vulnerability](https://github.com/SRv6d/rpsl-rs/security/advisories/new) to open the advisory form. 10 | 4. Fill in the advisory details form and click Submit report. 11 | 12 | Thank you. 13 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = [ 3 | "examples/print_parsed_as3257*", 4 | "benches/parse_as3257*", 5 | "benches/equality.rs", 6 | "benches/get_values.rs", 7 | "docs/benchmark/*", 8 | ] 9 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | targets = [ 3 | "x86_64-unknown-linux-gnu", 4 | "aarch64-unknown-linux-gnu", 5 | "x86_64-unknown-linux-musl", 6 | "aarch64-unknown-linux-musl", 7 | "x86_64-apple-darwin", 8 | "aarch64-apple-darwin", 9 | "x86_64-pc-windows-msvc", 10 | ] 11 | all-features = true 12 | 13 | [advisories] 14 | version = 2 15 | db-path = "$CARGO_HOME/advisory-dbs" 16 | db-urls = ["https://github.com/rustsec/advisory-db"] 17 | 18 | [licenses] 19 | version = 2 20 | allow = ["MIT", "Apache-2.0", "Unicode-3.0"] 21 | confidence-threshold = 0.8 22 | 23 | [bans] 24 | multiple-versions = "warn" 25 | multiple-versions-include-dev = false 26 | highlight = "all" 27 | wildcards = "deny" 28 | workspace-default-features = "allow" 29 | external-default-features = "allow" 30 | 31 | [sources] 32 | unknown-registry = "deny" 33 | unknown-git = "deny" 34 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 35 | -------------------------------------------------------------------------------- /docs/benchmark/README.md: -------------------------------------------------------------------------------- 1 | # benchmark 2 | 3 | Benchmarks comparing performance to other RPSL parsers. 4 | 5 | ## Results 6 | 7 | ![graph](graph.svg) 8 | 9 | | Parser | Mean | Min | Max | Compiler / Runtime | 10 | | ---------------- | ----------------- | --------- | --------- | ----------------------------------------------- | 11 | | **rpsl-rs** | **508.67 µs** | 509.11 µs | 509.67 µs | rustc 1.81.0 (eeb90cda1 2024-09-04) LLVM 18.1.7 | 12 | | [RPSL::Parser] | 59.4 ms ± 3.2 ms | 58.1 ms | 81.3 ms | perl v5.36.0 | 13 | | [irrdnet/irrd] | 93.5 ms ± 2.5 ms | 90.7 ms | 102.4 ms | Python 3.11.2 | 14 | | [RIPE-NCC/whois] | 114.7 ms ± 6.3 ms | 106.5 ms | 124.6 ms | openjdk version "17.0.12" 2024-07-16 | 15 | 16 | _Parsing of the AS3257 aut-num object on a 2022 M1 Max running macOS 15.0 (24A335)._ 17 | 18 | ## Methology 19 | 20 | For each benchmarked parser, a small executable is created in its native language that parses the AS3257 aut-num object. 21 | With the exception of [RIPE-NCC/whois], the AS3257 object is included as a string literal. To benchmark [RIPE-NCC/whois], the AS3257 object has to be read from a file since Java limits the length of string literals. 22 | 23 | ## Running Benchmarks 24 | 25 | Benchmarks for the parser itself are done using `cargo bench`, while any external parser is benchmarked using [hyperfine].\ 26 | To run a specific benchmark execute `just bench-$PARSER-NAME` or `just benchmark-comparison` to run all benchmarks. This will setup required dependencies and run `sudo` so it is only recommended to be used in an isolated environment and is only tested using the devcontainer, other platforms might require additional dependencies to be installed. A working installation of `just` and `cargo` is assumed. 27 | 28 | [RPSL::Parser]: https://metacpan.org/pod/RPSL::Parser 29 | [irrdnet/irrd]: https://github.com/irrdnet/irrd 30 | [RIPE-NCC/whois]: https://github.com/RIPE-NCC/whois 31 | [hyperfine]: https://github.com/sharkdp/hyperfine 32 | -------------------------------------------------------------------------------- /docs/benchmark/RPSL-Parser/run.just: -------------------------------------------------------------------------------- 1 | export PARSER_VERSION := "0.04000" 2 | 3 | bench-RPSL-Parser: _install_hyperfine 4 | #!/usr/bin/env bash 5 | set -euxo pipefail 6 | 7 | sudo PERL_MM_USE_DEFAULT=1 cpan App::cpanminus 8 | sudo cpanm RPSL::Parser@$PARSER_VERSION 9 | 10 | hyperfine -N --warmup 3 "perl -w docs/benchmark/RPSL-Parser/main.pl" 11 | -------------------------------------------------------------------------------- /docs/benchmark/benchmark.just: -------------------------------------------------------------------------------- 1 | import "irrdnet_irrd/run.just" 2 | import "RPSL-Parser/run.just" 3 | import "whois-rpsl/run.just" 4 | 5 | benchmark-comparison: bench-irrdnet_irrd bench-RPSL-Parser bench-whois-rpsl 6 | 7 | _install_hyperfine: 8 | cargo install --locked hyperfine 9 | -------------------------------------------------------------------------------- /docs/benchmark/graph-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 3 | "data": { 4 | "values": [ 5 | { 6 | "parser": "rpsl-rs", 7 | "time": 1, 8 | "timeFormat": "< 1ms" 9 | }, 10 | { 11 | "parser": "RPSL::Parser", 12 | "time": 59.4, 13 | "timeFormat": "61.8ms" 14 | }, 15 | { 16 | "parser": "irrdnet/irrd", 17 | "time": 93.5, 18 | "timeFormat": "93.5ms" 19 | }, 20 | { 21 | "parser": "RIPE-NCC/whois", 22 | "time": 114.7, 23 | "timeFormat": "> 100ms" 24 | } 25 | ] 26 | }, 27 | "config": { 28 | "params": [ 29 | { 30 | "name": "defaultFont", 31 | "value": "-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"" 32 | }, 33 | { 34 | "name": "titleColor", 35 | "value": "#333333" 36 | }, 37 | { 38 | "name": "labelColor", 39 | "value": "#333333" 40 | } 41 | ], 42 | "header": { 43 | "labelFont": { 44 | "expr": "defaultFont" 45 | }, 46 | "titleFont": { 47 | "expr": "defaultFont" 48 | }, 49 | "titleFontWeight": 500 50 | }, 51 | "text": { 52 | "font": { 53 | "expr": "defaultFont" 54 | }, 55 | "color": { 56 | "expr": "labelColor" 57 | } 58 | }, 59 | "mark": { 60 | "font": { 61 | "expr": "defaultFont" 62 | }, 63 | "color": { 64 | "expr": "labelColor" 65 | } 66 | }, 67 | "title": { 68 | "font": { 69 | "expr": "defaultFont" 70 | }, 71 | "subtitleFont": { 72 | "expr": "defaultFont" 73 | }, 74 | "fontWeight": 500 75 | }, 76 | "axis": { 77 | "labelColor": { 78 | "expr": "labelColor" 79 | }, 80 | "labelFont": { 81 | "expr": "defaultFont" 82 | }, 83 | "titleFont": { 84 | "expr": "defaultFont" 85 | }, 86 | "titleFontWeight": 500, 87 | "titleColor": { 88 | "expr": "titleColor" 89 | }, 90 | "titleFontSize": 12 91 | }, 92 | "legend": { 93 | "titleFontWeight": 500, 94 | "titleColor": { 95 | "expr": "titleColor" 96 | }, 97 | "titleFontSize": 12, 98 | "labelColor": { 99 | "expr": "labelColor" 100 | }, 101 | "labelFont": { 102 | "expr": "defaultFont" 103 | }, 104 | "titleFont": { 105 | "expr": "defaultFont" 106 | } 107 | }, 108 | "view": { 109 | "stroke": null 110 | }, 111 | "background": "transparent" 112 | }, 113 | "background": "transparent", 114 | "encoding": { 115 | "y": { 116 | "field": "parser", 117 | "type": "nominal", 118 | "axis": { 119 | "grid": false, 120 | "title": null, 121 | "labelFontSize": 12, 122 | "ticks": false, 123 | "labelPadding": 10, 124 | "domain": false 125 | }, 126 | "sort": null 127 | }, 128 | "x": { 129 | "field": "time", 130 | "type": "quantitative", 131 | "axis": { 132 | "title": null, 133 | "labelExpr": "datum.value + 'ms'", 134 | "tickCount": 3, 135 | "tickSize": 0, 136 | "labelPadding": 6, 137 | "labelAlign": "center", 138 | "labelFontSize": 12, 139 | "tickColor": "rgba(127,127,127,0.25)", 140 | "gridColor": "rgba(127,127,127,0.25)", 141 | "domain": false 142 | } 143 | } 144 | }, 145 | "height": 140, 146 | "width": "container", 147 | "layer": [ 148 | { 149 | "mark": "bar", 150 | "encoding": { 151 | "size": { 152 | "value": 13 153 | }, 154 | "color": { 155 | "value": "#E15759" 156 | } 157 | } 158 | }, 159 | { 160 | "transform": [ 161 | { 162 | "filter": "datum.parser !== 'rpsl-rs'" 163 | } 164 | ], 165 | "mark": { 166 | "type": "text", 167 | "align": "left", 168 | "baseline": "middle", 169 | "dx": 6, 170 | "fontSize": 12 171 | }, 172 | "encoding": { 173 | "text": { 174 | "field": "timeFormat" 175 | } 176 | } 177 | }, 178 | { 179 | "transform": [ 180 | { 181 | "filter": "datum.parser === 'rpsl-rs'" 182 | } 183 | ], 184 | "mark": { 185 | "type": "text", 186 | "align": "left", 187 | "baseline": "middle", 188 | "dx": 6, 189 | "fontSize": 12, 190 | "fontWeight": "bold" 191 | }, 192 | "encoding": { 193 | "text": { 194 | "field": "timeFormat" 195 | } 196 | } 197 | } 198 | ] 199 | } 200 | -------------------------------------------------------------------------------- /docs/benchmark/graph.svg: -------------------------------------------------------------------------------- 1 | 0ms50ms100msrpsl-rsRPSL::Parserirrdnet/irrdRIPE-NCC/whois61.8ms93.5ms> 100ms< 1ms -------------------------------------------------------------------------------- /docs/benchmark/irrdnet_irrd/run.just: -------------------------------------------------------------------------------- 1 | export UV_VERSION := "0.4.18" 2 | export PYTHON_VERSION := "3.11" 3 | export IRRD_VERSION := "4.4.2" 4 | 5 | bench-irrdnet_irrd: _install_uv _install_hyperfine 6 | #!/usr/bin/env bash 7 | set -euxo pipefail 8 | 9 | uv venv --python $PYTHON_VERSION 10 | source .venv/bin/activate 11 | uv pip install irrd==$IRRD_VERSION 12 | 13 | hyperfine -N --warmup 3 "python3 docs/benchmark/irrdnet_irrd/main.py" 14 | 15 | rm -rf .venv 16 | 17 | _install_uv: 18 | curl -LsSf https://astral.sh/uv/$UV_VERSION/install.sh | sh 19 | -------------------------------------------------------------------------------- /docs/benchmark/whois-rpsl/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | parser.test 7 | parser-test 8 | jar 9 | 0.1.0 10 | 11 | 12 | 1.8 13 | 1.8 14 | 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-shade-plugin 21 | 3.2.4 22 | 23 | 24 | package 25 | 26 | shade 27 | 28 | 29 | 30 | 32 | parser.TestParser 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.rpsl4j 45 | rpsl4j-parser 46 | 1.81 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/benchmark/whois-rpsl/run.just: -------------------------------------------------------------------------------- 1 | bench-whois-rpsl: _install_hyperfine 2 | #!/usr/bin/env bash 3 | set -euxo pipefail 4 | 5 | sudo apt update && sudo apt install -y build-essential openjdk-17-jre openjdk-17-jdk maven 6 | 7 | cd docs/benchmark/whois-rpsl 8 | 9 | mvn package 10 | 11 | hyperfine -N --warmup 3 "java -jar target/parser-test-0.1.0.jar ./AS3257.txt" 12 | -------------------------------------------------------------------------------- /docs/benchmark/whois-rpsl/src/main/java/parser/TestParser.java: -------------------------------------------------------------------------------- 1 | package parser; 2 | 3 | import java.util.Set; 4 | import java.util.HashSet; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.io.IOException; 8 | 9 | import net.ripe.db.whois.common.io.RpslObjectStringReader; 10 | import net.ripe.db.whois.common.rpsl.RpslObject; 11 | 12 | public class TestParser { 13 | public static void main(String[] args) 14 | throws IOException 15 | { 16 | Path RpslFilepath = Path.of(args[0]); 17 | String RPSL = Files.readString(RpslFilepath); 18 | 19 | RpslObjectStringReader reader = new RpslObjectStringReader(RPSL); 20 | for(String objectString : reader) 21 | RpslObject.parse(objectString); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | import "docs/benchmark/benchmark.just" 2 | 3 | export CI := env("CI", "false") 4 | CHANGELOG_FILE := "CHANGELOG.md" 5 | REPO_URL := "https://github.com/SRv6d/rpsl-rs" 6 | 7 | default: check-lockfile lint test 8 | 9 | # Check if the lockfile is up to date 10 | check-lockfile: 11 | cargo update -w --locked 12 | 13 | # Lint code and check formatting 14 | lint: lint-justfile 15 | cargo clippy --all-targets --all-features 16 | cargo fmt --all --check 17 | 18 | lint-justfile: 19 | just --check --fmt --unstable 20 | 21 | cov_output := if CI == "true" { "--lcov --output-path lcov.info" } else { "--summary-only" } 22 | 23 | # Run tests 24 | test $COV=CI: (_install_llvm_cov COV) && doc-test 25 | {{ if COV == "true" { "cargo llvm-cov --all-features" + " " + cov_output } else { "cargo test --lib --all-features" } }} 26 | 27 | # Run documentation and example tests 28 | doc-test: 29 | cargo test --all-features --doc 30 | cargo test --all-features --examples 31 | 32 | # Alias to run tests with coverage 33 | coverage: (test "true") 34 | 35 | # Check feature combinations 36 | check-features: 37 | cargo hack check --each-feature --no-dev-deps 38 | 39 | # Bump our version 40 | bump-version $VERSION: _check_clean_working (_validate_semver VERSION) && (_changelog_add_version VERSION) (_bump_version_pr VERSION) 41 | #!/usr/bin/env bash 42 | set -euxo pipefail 43 | 44 | sed -i 's/^version = .*/version = "'$VERSION'"/g' Cargo.toml 45 | 46 | git add Cargo.toml 47 | git commit -m "Bump version to v{{ VERSION }}" 48 | 49 | # Create a GitHub release containing the latest changes 50 | release-latest-version version: 51 | #!/usr/bin/env bash 52 | set -euxo pipefail 53 | PREVIOUS_RELEASE=$(gh release list --json name,isLatest --jq '.[] | select(.isLatest)|.name') 54 | CURRENT_RELEASE="v{{ version }}" 55 | CHANGES=$(sed -n "/^## \[{{ version }}]/,/^## \[[0-9].*\]/ {//!p}" {{ CHANGELOG_FILE }}) 56 | RELEASE_NOTES=" 57 | ## What's Changed 58 | 59 | $CHANGES 60 | 61 | **Full Changelog**: {{ REPO_URL }}/compare/$PREVIOUS_RELEASE...$CURRENT_RELEASE 62 | " 63 | 64 | gh release create $CURRENT_RELEASE --latest --title $CURRENT_RELEASE --notes-file - <<< "$RELEASE_NOTES" 65 | 66 | # Publish the crate 67 | publish: _validate_version_tag 68 | cargo publish --no-verify 69 | 70 | # Check that Git has a clean working directory 71 | _check_clean_working: 72 | test -z "$(git status --porcelain)" || (echo "The working directory is not clean"; exit 1) 73 | 74 | # Validate that the crate version matches that of the git tag 75 | _validate_version_tag: 76 | #!/usr/bin/env bash 77 | set -euxo pipefail 78 | PROJECT_VERSION="$(grep -Po '(?<=^version = ").*(?=")' Cargo.toml)" 79 | GIT_TAG="$(git describe --exact-match --tags)" 80 | 81 | if [ ! $PROJECT_VERSION == ${GIT_TAG:1} ]; then 82 | echo Project version $PROJECT_VERSION does not match git tag $GIT_TAG 83 | exit 1 84 | fi 85 | 86 | # Validate a version against SemVer 87 | _validate_semver version: 88 | #!/usr/bin/env bash 89 | set -euxo pipefail 90 | if [[ ! "{{ version }}" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then 91 | echo Invalid SemVer {{ version }} 92 | exit 1 93 | fi 94 | 95 | _install_llvm_cov $run: 96 | #!/usr/bin/env bash 97 | set -euxo pipefail 98 | 99 | if [ $run == true ] && [ $CI = false ]; then 100 | cargo install cargo-llvm-cov --locked 101 | fi 102 | 103 | # Update the changelog with a new version 104 | _changelog_add_version version filename=CHANGELOG_FILE: 105 | #!/usr/bin/env bash 106 | set -euxo pipefail 107 | PREV_VERSION=$(sed -n "/^\[unreleased\]:/ { n; s/^\[\([^]]*\)\].*/\1/p }" {{ filename }}) 108 | 109 | sed -i "/^## \[Unreleased\]$/ { N; s/\n/\n\n## [{{ version }}] - {{ datetime('%Y-%m-%d') }}\n/ }" {{ filename }} 110 | sed -i "/^\[unreleased\]:/ s/v[0-9.]\+\b/v{{ version }}.../; /^\[unreleased\]:/ a\ 111 | [{{ version }}]: {{ REPO_URL }}/compare/v$PREV_VERSION...v{{ version }}" {{ filename }} 112 | 113 | git add {{ filename }} 114 | git commit -m "Update {{ filename }}" 115 | 116 | _bump_version_pr version: 117 | gh pr create --title "Bump version to v{{ version }}" --body "" 118 | -------------------------------------------------------------------------------- /src/attribute.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, fmt, ops::Deref, str::FromStr}; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::Serialize; 5 | 6 | use crate::error::{InvalidNameError, InvalidValueError}; 7 | 8 | /// An attribute of an [`Object`](crate::Object). 9 | /// 10 | /// # Example 11 | /// ``` 12 | /// # use rpsl::{parse_object, Attribute}; 13 | /// let object = parse_object(" 14 | /// name: ACME Company 15 | /// 16 | /// ")?; 17 | /// let attribute = Attribute::new("name".parse()?, "ACME Company".parse()?); 18 | /// assert_eq!(object[0], attribute); 19 | /// # Ok::<(), Box>(()) 20 | /// ``` 21 | #[derive(Debug, PartialEq, Eq, Clone)] 22 | #[cfg_attr(feature = "serde", derive(Serialize))] 23 | pub struct Attribute<'a> { 24 | /// The name of the attribute. 25 | pub name: Name<'a>, 26 | /// The value of the attribute. 27 | #[cfg_attr(feature = "serde", serde(rename = "values"))] 28 | pub value: Value<'a>, 29 | } 30 | 31 | impl<'a> Attribute<'a> { 32 | /// Create a new attribute. 33 | #[must_use] 34 | pub fn new(name: Name<'a>, value: Value<'a>) -> Self { 35 | Self { name, value } 36 | } 37 | 38 | #[cfg(test)] 39 | pub(crate) fn unchecked_single(name: &'a str, value: V) -> Self 40 | where 41 | V: Into>, 42 | { 43 | let name = Name::unchecked(name); 44 | let value = Value::unchecked_single(value); 45 | Self { name, value } 46 | } 47 | 48 | #[cfg(test)] 49 | pub(crate) fn unchecked_multi(name: &'a str, values: I) -> Self 50 | where 51 | I: IntoIterator, 52 | V: Into>, 53 | { 54 | let name = Name::unchecked(name); 55 | let value = Value::unchecked_multi(values); 56 | Self { name, value } 57 | } 58 | } 59 | 60 | impl fmt::Display for Attribute<'_> { 61 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 62 | let values = self.value.values(); 63 | 64 | let first_value = values.first().expect("must contain at least one value"); 65 | match first_value { 66 | Some(value) => { 67 | writeln!(f, "{:16}{}", format!("{}:", self.name), value)?; 68 | } 69 | None => writeln!(f, "{}:", self.name)?, 70 | } 71 | 72 | let remaining_values = &values[1..]; 73 | for value in remaining_values { 74 | match value { 75 | Some(value) => { 76 | writeln!(f, "{:16}{}", " ", value)?; 77 | } 78 | None => { 79 | writeln!(f, " ")?; 80 | } 81 | } 82 | } 83 | 84 | Ok(()) 85 | } 86 | } 87 | 88 | /// The name of an [`Attribute`]. 89 | #[derive(Debug, PartialEq, Eq, Clone)] 90 | #[cfg_attr(feature = "serde", derive(Serialize), serde(transparent))] 91 | pub struct Name<'a>(Cow<'a, str>); 92 | 93 | impl<'a> Name<'a> { 94 | pub(crate) fn unchecked(name: &'a str) -> Self { 95 | Self(Cow::Borrowed(name)) 96 | } 97 | 98 | fn validate(name: &str) -> Result<(), InvalidNameError> { 99 | if name.trim().is_empty() { 100 | return Err(InvalidNameError::Empty); 101 | } else if !name.is_ascii() { 102 | return Err(InvalidNameError::NonAscii); 103 | } else if !name.chars().next().unwrap().is_ascii_alphabetic() { 104 | return Err(InvalidNameError::NonAsciiAlphabeticFirstChar); 105 | } else if !name.chars().last().unwrap().is_ascii_alphanumeric() { 106 | return Err(InvalidNameError::NonAsciiAlphanumericLastChar); 107 | } 108 | 109 | Ok(()) 110 | } 111 | } 112 | 113 | impl FromStr for Name<'_> { 114 | type Err = InvalidNameError; 115 | 116 | /// Create a new `Name` from a string slice. 117 | /// 118 | /// A valid name may consist of ASCII letters, digits and the characters "-", "_", 119 | /// while beginning with a letter and ending with a letter or a digit. 120 | /// 121 | /// # Errors 122 | /// Returns an error if the name is empty or invalid. 123 | fn from_str(name: &str) -> Result { 124 | Self::validate(name)?; 125 | Ok(Self(Cow::Owned(name.to_string()))) 126 | } 127 | } 128 | 129 | impl Deref for Name<'_> { 130 | type Target = str; 131 | 132 | fn deref(&self) -> &Self::Target { 133 | self.0.as_ref() 134 | } 135 | } 136 | 137 | impl PartialEq<&str> for Name<'_> { 138 | fn eq(&self, other: &&str) -> bool { 139 | self.0 == *other 140 | } 141 | } 142 | 143 | impl fmt::Display for Name<'_> { 144 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 145 | write!(f, "{}", self.0) 146 | } 147 | } 148 | 149 | /// The value of an [`Attribute`]. 150 | /// Since only some values contain multiple lines and single line values do not require 151 | /// additional heap allocation, an Enum is used to represent both variants. 152 | #[derive(Debug, PartialEq, Eq, Clone)] 153 | #[cfg_attr( 154 | feature = "serde", 155 | derive(Serialize), 156 | serde(into = "Vec>") 157 | )] 158 | pub enum Value<'a> { 159 | /// A single line value. 160 | /// 161 | /// # Example 162 | /// ``` 163 | /// # use rpsl::{parse_object, Value}; 164 | /// let object = parse_object(" 165 | /// name: ACME Company 166 | /// 167 | /// ")?; 168 | /// let value: Value = "ACME Company".parse()?; 169 | /// assert_eq!(object[0].value, value); 170 | /// # Ok::<(), Box>(()) 171 | /// ``` 172 | SingleLine(Option>), 173 | /// A value spanning over multiple lines. 174 | /// 175 | /// # Example 176 | /// ``` 177 | /// # use rpsl::{parse_object, Value}; 178 | /// let object = parse_object(" 179 | /// remarks: Packet Street 6 180 | /// 128 Series of Tubes 181 | /// Internet 182 | /// 183 | /// ")?; 184 | /// let value: Value = vec!["Packet Street 6", "128 Series of Tubes", "Internet"].try_into()?; 185 | /// assert_eq!(object[0].value, value); 186 | /// # Ok::<(), Box>(()) 187 | /// ``` 188 | MultiLine(Vec>>), 189 | } 190 | 191 | impl<'a> Value<'a> { 192 | /// Create a single line value without checking that characters conform to any specification 193 | /// while still coercing empty values to `None`. 194 | pub(crate) fn unchecked_single(value: V) -> Self 195 | where 196 | V: Into>, 197 | { 198 | Self::SingleLine(value.into().and_then(coerce_empty_value).map(Cow::Borrowed)) 199 | } 200 | 201 | /// Create a multi line value without checking that characters conform to any specification 202 | /// while still coercing empty values to `None`. 203 | pub(crate) fn unchecked_multi(values: I) -> Self 204 | where 205 | I: IntoIterator, 206 | V: Into>, 207 | { 208 | let s = Self::MultiLine( 209 | values 210 | .into_iter() 211 | .map(|v| v.into().and_then(coerce_empty_value).map(Cow::Borrowed)) 212 | .collect(), 213 | ); 214 | assert!(s.lines() > 1, "multi line values need at least two lines"); 215 | s 216 | } 217 | 218 | fn validate(value: &str) -> Result<(), InvalidValueError> { 219 | value.chars().try_for_each(Self::validate_char) 220 | } 221 | 222 | /// Even though RFC 2622 requires values to be ASCII, in practice some WHOIS databases 223 | /// (e.g. RIPE) do not enforce this so, to be useful in the real world, we don't either. 224 | #[inline] 225 | pub(crate) fn validate_char(c: char) -> Result<(), InvalidValueError> { 226 | if !is_extended_ascii(c) { 227 | return Err(InvalidValueError::NonExtendedAscii); 228 | } else if c.is_ascii_control() { 229 | return Err(InvalidValueError::ContainsControlChar); 230 | } 231 | 232 | Ok(()) 233 | } 234 | 235 | /// The number of lines contained. 236 | /// 237 | /// # Examples 238 | /// 239 | /// A value with a single line. 240 | /// ``` 241 | /// # use rpsl::Value; 242 | /// let value: Value = "ACME Company".parse()?; 243 | /// assert_eq!(value.lines(), 1); 244 | /// # Ok::<(), Box>(()) 245 | /// ``` 246 | /// 247 | /// A value with multiple lines. 248 | /// ``` 249 | /// # use rpsl::Value; 250 | /// let value: Value = vec!["Packet Street 6", "128 Series of Tubes", "Internet"].try_into()?; 251 | /// assert_eq!(value.lines(), 3); 252 | /// # Ok::<(), Box>(()) 253 | /// ``` 254 | #[must_use] 255 | pub fn lines(&self) -> usize { 256 | match &self { 257 | Self::SingleLine(_) => 1, 258 | Self::MultiLine(values) => values.len(), 259 | } 260 | } 261 | 262 | fn values(&'a self) -> Vec> { 263 | match self { 264 | Value::SingleLine(value) => { 265 | vec![value.as_ref().map(std::convert::AsRef::as_ref)] 266 | } 267 | Value::MultiLine(values) => values 268 | .iter() 269 | .map(|v| v.as_ref().map(std::convert::AsRef::as_ref)) 270 | .collect(), 271 | } 272 | } 273 | 274 | /// The lines that contain content and are non empty. 275 | /// 276 | /// # Example 277 | /// ``` 278 | /// # use rpsl::parse_object; 279 | /// # fn main() -> Result<(), Box> { 280 | /// let remarks = parse_object(" 281 | /// remarks: I have lots 282 | /// 283 | /// to say. 284 | /// 285 | /// ")?; 286 | /// assert_eq!(remarks[0].value.with_content(), vec!["I have lots", "to say."]); 287 | /// # Ok(()) 288 | /// # } 289 | /// ``` 290 | pub fn with_content(&self) -> Vec<&str> { 291 | match self { 292 | Self::SingleLine(v) => { 293 | if let Some(v) = v { 294 | vec![v] 295 | } else { 296 | vec![] 297 | } 298 | } 299 | Self::MultiLine(v) => v.iter().flatten().map(AsRef::as_ref).collect(), 300 | } 301 | } 302 | } 303 | 304 | impl FromStr for Value<'_> { 305 | type Err = InvalidValueError; 306 | 307 | /// Create a new single line value from a string slice. 308 | /// 309 | /// A valid value may consist of any ASCII character, excluding control characters. 310 | /// 311 | /// # Errors 312 | /// Returns an error if the value contains invalid characters. 313 | fn from_str(value: &str) -> Result { 314 | Self::validate(value)?; 315 | Ok(Self::SingleLine( 316 | coerce_empty_value(value).map(|value| Cow::Owned(value.to_string())), 317 | )) 318 | } 319 | } 320 | 321 | impl TryFrom> for Value<'_> { 322 | type Error = InvalidValueError; 323 | 324 | /// Create a new value from a vector of string slices, representing the values lines. 325 | /// 326 | /// # Errors 327 | /// Returns an error if a value contains invalid characters. 328 | /// 329 | /// # Example 330 | /// ``` 331 | /// # use rpsl::Value; 332 | /// let value: Value = vec!["Packet Street 6", "128 Series of Tubes", "Internet"].try_into()?; 333 | /// assert_eq!(value.lines(), 3); 334 | /// # Ok::<(), Box>(()) 335 | /// ``` 336 | fn try_from(values: Vec<&str>) -> Result { 337 | if values.len() == 1 { 338 | let value = values[0].parse()?; 339 | return Ok(value); 340 | } 341 | let values = values 342 | .into_iter() 343 | .map(|v| { 344 | Self::validate(v)?; 345 | Ok(coerce_empty_value(v).map(std::string::ToString::to_string)) 346 | }) 347 | .collect::>, InvalidValueError>>()?; 348 | 349 | Ok(Self::MultiLine( 350 | values.into_iter().map(|v| v.map(Cow::Owned)).collect(), 351 | )) 352 | } 353 | } 354 | 355 | #[allow(clippy::from_over_into)] 356 | impl Into>> for Value<'_> { 357 | fn into(self) -> Vec> { 358 | match self { 359 | Self::SingleLine(value) => { 360 | vec![value.map(|v| v.to_string())] 361 | } 362 | Self::MultiLine(values) => values 363 | .into_iter() 364 | .map(|v| v.map(|v| v.to_string())) 365 | .collect(), 366 | } 367 | } 368 | } 369 | 370 | impl PartialEq<&str> for Value<'_> { 371 | fn eq(&self, other: &&str) -> bool { 372 | match &self { 373 | Self::MultiLine(_) => false, 374 | Self::SingleLine(value) => match value { 375 | Some(value) => value == *other, 376 | None => coerce_empty_value(other).is_none(), 377 | }, 378 | } 379 | } 380 | } 381 | 382 | impl PartialEq> for Value<'_> { 383 | fn eq(&self, other: &Vec<&str>) -> bool { 384 | if self.lines() != other.len() { 385 | return false; 386 | } 387 | 388 | match &self { 389 | Self::SingleLine(value) => { 390 | let s = value.as_deref(); 391 | let other_coerced = coerce_empty_value(other[0]); 392 | s == other_coerced 393 | } 394 | Self::MultiLine(values) => { 395 | let s = values.iter().map(|v| v.as_deref()); 396 | let other_coerced = other.iter().map(|&v| coerce_empty_value(v)); 397 | s.eq(other_coerced) 398 | } 399 | } 400 | } 401 | } 402 | 403 | impl PartialEq>> for Value<'_> { 404 | fn eq(&self, other: &Vec>) -> bool { 405 | if self.lines() != other.len() { 406 | return false; 407 | } 408 | 409 | match &self { 410 | Self::SingleLine(value) => { 411 | let s = value.as_deref(); 412 | let other = other[0]; 413 | s == other 414 | } 415 | Self::MultiLine(values) => { 416 | let s = values.iter().map(|v| v.as_deref()); 417 | let other = other.iter().map(|v| v.as_deref()); 418 | s.eq(other) 419 | } 420 | } 421 | } 422 | } 423 | 424 | /// Coerce an empty value to `None`. 425 | fn coerce_empty_value(value: S) -> Option 426 | where 427 | S: AsRef, 428 | { 429 | if value.as_ref().trim().is_empty() { 430 | None 431 | } else { 432 | Some(value) 433 | } 434 | } 435 | 436 | /// Checks if the given char is part of the extended ASCII set. 437 | #[inline] 438 | fn is_extended_ascii(char: char) -> bool { 439 | matches!(char, '\u{0000}'..='\u{00FF}') 440 | } 441 | 442 | #[cfg(test)] 443 | mod tests { 444 | use proptest::prelude::*; 445 | use rstest::*; 446 | #[cfg(feature = "serde")] 447 | use serde_test::{assert_ser_tokens, Token}; 448 | 449 | use super::*; 450 | 451 | #[rstest] 452 | #[case( 453 | Attribute::unchecked_single("ASNumber", "32934"), 454 | "ASNumber: 32934\n" 455 | )] 456 | #[case(Attribute::unchecked_single("ASNumber", None), "ASNumber:\n")] 457 | #[case( 458 | Attribute::unchecked_single("ASName", "FACEBOOK"), 459 | "ASName: FACEBOOK\n" 460 | )] 461 | #[case( 462 | Attribute::unchecked_single("RegDate", "2004-08-24"), 463 | "RegDate: 2004-08-24\n" 464 | )] 465 | #[case( 466 | Attribute::unchecked_single("Ref", "https://rdap.arin.net/registry/autnum/32934"), 467 | "Ref: https://rdap.arin.net/registry/autnum/32934\n" 468 | )] 469 | fn attribute_display_single_line(#[case] attribute: Attribute, #[case] expected: &str) { 470 | assert_eq!(attribute.to_string(), expected); 471 | } 472 | 473 | #[rstest] 474 | #[case( 475 | Attribute::unchecked_multi( 476 | "remarks", 477 | [ 478 | "AS1299 is matching RPKI validation state and reject", 479 | "invalid prefixes from peers and customers." 480 | ] 481 | 482 | ), 483 | concat!( 484 | "remarks: AS1299 is matching RPKI validation state and reject\n", 485 | " invalid prefixes from peers and customers.\n", 486 | ) 487 | )] 488 | #[case( 489 | Attribute::unchecked_multi( 490 | "remarks", 491 | [ 492 | None, 493 | None 494 | ] 495 | ), 496 | concat!( 497 | "remarks:\n", 498 | " \n", 499 | ) 500 | )] 501 | fn attribute_display_multi_line(#[case] attribute: Attribute, #[case] expected: &str) { 502 | assert_eq!(attribute.to_string(), expected); 503 | } 504 | 505 | #[rstest] 506 | #[case( 507 | Attribute::unchecked_single("ASNumber", "32934"), 508 | &[ 509 | Token::Struct { name: "Attribute", len: 2 }, 510 | Token::Str("name"), 511 | Token::Str("ASNumber"), 512 | Token::Str("values"), 513 | Token::Seq { len: Some(1) }, 514 | Token::Some, 515 | Token::Str("32934"), 516 | Token::SeqEnd, 517 | Token::StructEnd, 518 | ], 519 | )] 520 | #[case( 521 | Attribute::unchecked_multi( 522 | "address", 523 | ["Packet Street 6", "128 Series of Tubes", "Internet"] 524 | ), 525 | &[ 526 | Token::Struct { name: "Attribute", len: 2 }, 527 | Token::Str("name"), 528 | Token::Str("address"), 529 | Token::Str("values"), 530 | Token::Seq { len: Some(3) }, 531 | Token::Some, 532 | Token::Str("Packet Street 6"), 533 | Token::Some, 534 | Token::Str("128 Series of Tubes"), 535 | Token::Some, 536 | Token::Str("Internet"), 537 | Token::SeqEnd, 538 | Token::StructEnd, 539 | ], 540 | )] 541 | #[cfg(feature = "serde")] 542 | fn attribute_serialize(#[case] attribute: Attribute, #[case] expected: &[Token]) { 543 | assert_ser_tokens(&attribute, expected); 544 | } 545 | 546 | #[test] 547 | fn name_display() { 548 | let name_display = Name::unchecked("address").to_string(); 549 | assert_eq!(name_display, "address"); 550 | } 551 | 552 | #[rstest] 553 | #[case("role")] 554 | #[case("person")] 555 | fn name_deref(#[case] s: &str) { 556 | let name = Name::unchecked(s); 557 | assert_eq!(*name, *s); 558 | } 559 | 560 | #[rstest] 561 | #[case("role")] 562 | #[case("person")] 563 | fn name_from_str(#[case] s: &str) { 564 | assert_eq!(Name::from_str(s).unwrap(), Name(Cow::Owned(s.to_string()))); 565 | } 566 | 567 | proptest! { 568 | #[test] 569 | fn name_from_str_space_only_is_err(n in r"\s") { 570 | assert!(Name::from_str(&n).is_err()); 571 | } 572 | 573 | #[test] 574 | fn name_from_str_non_ascii_is_err(n in r"[^[[:ascii:]]]") { 575 | assert!(Name::from_str(&n).is_err()); 576 | } 577 | 578 | #[test] 579 | fn name_from_str_non_letter_first_char_is_err(n in r"[^a-zA-Z][[:ascii:]]*") { 580 | assert!(Name::from_str(&n).is_err()); 581 | } 582 | 583 | #[test] 584 | fn name_from_str_non_letter_or_digit_last_char_is_err(n in r"[[:ascii:]]*[^a-zA-Z0-9]") { 585 | assert!(Name::from_str(&n).is_err()); 586 | } 587 | } 588 | 589 | #[rstest] 590 | #[case(Name::unchecked("ASNumber"), Token::Str("ASNumber"))] 591 | #[cfg(feature = "serde")] 592 | fn name_serialize(#[case] name: Name, #[case] expected: Token) { 593 | assert_ser_tokens(&name, &[expected]); 594 | } 595 | 596 | #[rstest] 597 | #[case("This is a valid attribute value", Value::SingleLine(Some(Cow::Owned("This is a valid attribute value".to_string()))))] 598 | #[case(" ", Value::SingleLine(None))] 599 | fn value_from_str(#[case] s: &str, #[case] expected: Value) { 600 | assert_eq!(Value::from_str(s).unwrap(), expected); 601 | } 602 | 603 | #[rstest] 604 | fn value_from_empty_str(#[values("", " ")] s: &str) { 605 | assert_eq!(Value::from_str(s).unwrap(), Value::SingleLine(None)); 606 | } 607 | 608 | proptest! { 609 | #[test] 610 | fn value_validation_any_non_control_extended_ascii_valid( 611 | s in r"[\x00-\xFF]+" 612 | .prop_filter("Must not contain control chars", |s| !s.chars().any(|c| c.is_ascii_control()))) 613 | { 614 | Value::validate(&s).unwrap(); 615 | } 616 | 617 | #[test] 618 | fn value_validation_any_non_extended_ascii_is_err(s in r"[^\x00-\xFF]+") { 619 | matches!(Value::validate(&s).unwrap_err(), InvalidValueError::NonExtendedAscii); 620 | } 621 | 622 | #[test] 623 | fn value_validation_any_ascii_control_is_err(s in r"[\x00-\x1F\x7F]+") { 624 | matches!(Value::validate(&s).unwrap_err(), InvalidValueError::ContainsControlChar); 625 | } 626 | } 627 | 628 | #[rstest] 629 | #[case( 630 | Value::unchecked_single("32934"), 631 | &[ 632 | Token::Seq { len: Some(1) }, 633 | Token::Some, 634 | Token::Str("32934"), 635 | Token::SeqEnd, 636 | ], 637 | )] 638 | #[case( 639 | Value::unchecked_single(""), 640 | &[ 641 | Token::Seq { len: Some(1) }, 642 | Token::None, 643 | Token::SeqEnd, 644 | ], 645 | )] 646 | #[case( 647 | Value::unchecked_multi(["Packet Street 6", "128 Series of Tubes", "Internet"]), 648 | &[ 649 | Token::Seq { len: Some(3) }, 650 | Token::Some, 651 | Token::Str("Packet Street 6"), 652 | Token::Some, 653 | Token::Str("128 Series of Tubes"), 654 | Token::Some, 655 | Token::Str("Internet"), 656 | Token::SeqEnd, 657 | ], 658 | )] 659 | #[cfg(feature = "serde")] 660 | fn value_serialize(#[case] value: Value, #[case] expected: &[Token]) { 661 | assert_ser_tokens(&value, expected); 662 | } 663 | 664 | #[rstest] 665 | #[case(Value::unchecked_single(""), Value::unchecked_single(None))] 666 | #[case(Value::unchecked_single(" "), Value::unchecked_single(None))] 667 | #[case(Value::unchecked_multi(["", " ", " "]), Value::unchecked_multi([None, None, None]))] 668 | /// Creating unchecked values from empty strings results in None values. 669 | fn value_unchecked_empty_is_none(#[case] value: Value, #[case] expected: Value) { 670 | assert_eq!(value, expected); 671 | } 672 | 673 | #[test] 674 | #[should_panic(expected = "multi line values need at least two lines")] 675 | /// Unchecked multi line attributes cannot be created with only a single value. 676 | fn value_unchecked_multi_with_singe_value_panics() { 677 | Value::unchecked_multi(["just one"]); 678 | } 679 | 680 | #[rstest] 681 | #[case( 682 | vec!["Packet Street 6", "128 Series of Tubes", "Internet"], 683 | Value::MultiLine(vec![ 684 | Some(Cow::Owned("Packet Street 6".to_string())), 685 | Some(Cow::Owned("128 Series of Tubes".to_string())), 686 | Some(Cow::Owned("Internet".to_string())) 687 | ]) 688 | )] 689 | #[case( 690 | vec!["", "128 Series of Tubes", "Internet"], 691 | Value::MultiLine(vec![ 692 | None, 693 | Some(Cow::Owned("128 Series of Tubes".to_string())), 694 | Some(Cow::Owned("Internet".to_string())) 695 | ]) 696 | )] 697 | #[case( 698 | vec!["", " ", " "], 699 | Value::MultiLine(vec![None, None, None]) 700 | )] 701 | fn value_from_vec_of_str(#[case] v: Vec<&str>, #[case] expected: Value) { 702 | let value = Value::try_from(v).unwrap(); 703 | assert_eq!(value, expected); 704 | } 705 | 706 | #[test] 707 | fn value_from_vec_w_1_value_is_single_line() { 708 | assert_eq!( 709 | Value::try_from(vec!["Packet Street 6"]).unwrap(), 710 | Value::SingleLine(Some(Cow::Owned("Packet Street 6".to_string()))) 711 | ); 712 | } 713 | 714 | #[rstest] 715 | #[case("single value", 1)] 716 | #[case(vec!["multi", "value", "attribute"].try_into().unwrap(), 3)] 717 | fn value_lines(#[case] value: Value, #[case] expected: usize) { 718 | assert_eq!(value.lines(), expected); 719 | } 720 | 721 | #[rstest] 722 | #[case( 723 | Value::unchecked_single(None), 724 | vec![] 725 | )] 726 | #[case( 727 | Value::unchecked_single("single value"), 728 | vec!["single value"] 729 | )] 730 | #[case( 731 | Value::unchecked_multi(vec![ 732 | None, 733 | Some("128 Series of Tubes"), 734 | Some("Internet"), 735 | ]), 736 | vec!["128 Series of Tubes", "Internet"] 737 | )] 738 | #[case( 739 | Value::unchecked_multi([ 740 | "Packet Street 6", 741 | "128 Series of Tubes", 742 | "Internet" 743 | ]), 744 | vec!["Packet Street 6", "128 Series of Tubes", "Internet"] 745 | )] 746 | fn value_with_content(#[case] value: Value, #[case] expected: Vec<&str>) { 747 | let content = value.with_content(); 748 | assert_eq!(content, expected); 749 | } 750 | 751 | #[rstest] 752 | #[case("a value")] 753 | #[case("single value")] 754 | /// A value and &str evaluate as equal if the contents match. 755 | fn value_partialeq_str_eq_is_eq(#[case] s: &str) { 756 | let value = Value::unchecked_single(s); 757 | assert_eq!(value, s); 758 | } 759 | 760 | #[rstest] 761 | #[case(Value::unchecked_single("a value"), "a different value")] 762 | #[case( 763 | Value::unchecked_multi([ 764 | "multi", 765 | "value" 766 | ]), 767 | "single value" 768 | )] 769 | /// A value and &str do not evaluate as equal if the contents differ. 770 | fn value_partialeq_str_ne_is_ne(#[case] value: Value, #[case] s: &str) { 771 | assert_ne!(value, s); 772 | } 773 | 774 | #[rstest] 775 | #[case( 776 | Value::unchecked_single("single value"), 777 | vec!["single value"] 778 | )] 779 | #[case( 780 | Value::unchecked_single(None), 781 | vec![" "] 782 | )] 783 | #[case( 784 | Value::unchecked_multi([ 785 | "multi", 786 | "value", 787 | "attribute" 788 | ]), 789 | vec!["multi", "value", "attribute"] 790 | )] 791 | #[case( 792 | Value::unchecked_multi([ 793 | Some("multi"), 794 | None, 795 | Some("attribute") 796 | ]), 797 | vec!["multi", " ", "attribute"] 798 | )] 799 | /// A value and a Vec<&str> evaluate as equal if the contents match. 800 | fn value_partialeq_vec_str_eq_is_eq(#[case] value: Value, #[case] v: Vec<&str>) { 801 | assert_eq!(value, v); 802 | } 803 | 804 | #[rstest] 805 | #[case( 806 | Value::unchecked_single("single value"), 807 | vec!["multi", "value"] 808 | )] 809 | #[case( 810 | Value::unchecked_single("single value"), 811 | vec!["other single value"] 812 | )] 813 | #[case( 814 | Value::unchecked_multi([ 815 | "multi", 816 | "value", 817 | "attribute" 818 | ]), 819 | vec!["different", "multi", "value", "attribute"] 820 | )] 821 | /// A value and a Vec<&str> do not evaluate as equal if the contents differ. 822 | fn value_partialeq_vec_str_ne_is_ne(#[case] value: Value, #[case] v: Vec<&str>) { 823 | assert_ne!(value, v); 824 | } 825 | 826 | #[rstest] 827 | #[case( 828 | Value::unchecked_single("single value"), 829 | vec![Some("single value")] 830 | )] 831 | #[case( 832 | Value::unchecked_multi([ 833 | "multi", 834 | "value", 835 | "attribute" 836 | ]), 837 | vec![Some("multi"), Some("value"), Some("attribute")] 838 | )] 839 | #[case( 840 | Value::unchecked_multi([Some("multi"), None, Some("attribute")]), 841 | vec![Some("multi"), None, Some("attribute")] 842 | )] 843 | /// A value and a Vec> evaluate as equal if the contents match. 844 | fn value_partialeq_vec_option_str_eq_is_eq(#[case] value: Value, #[case] v: Vec>) { 845 | assert_eq!(value, v); 846 | } 847 | 848 | #[rstest] 849 | #[case( 850 | Value::unchecked_single("single value"), 851 | vec![Some("multi"), Some("value")] 852 | )] 853 | #[case( 854 | Value::unchecked_single("single value"), 855 | vec![Some("other single value")] 856 | )] 857 | #[case( 858 | Value::unchecked_single(None), 859 | vec![Some(" ")] 860 | )] 861 | #[case( 862 | Value::unchecked_multi([ 863 | "multi", 864 | "value", 865 | "attribute" 866 | ]), 867 | vec![Some("different"), Some("multi"), Some("value"), Some("attribute")] 868 | )] 869 | /// A value and a Vec> do not evaluate as equal if the contents differ. 870 | fn value_partialeq_vec_option_str_ne_is_ne(#[case] value: Value, #[case] v: Vec>) { 871 | assert_ne!(value, v); 872 | } 873 | 874 | #[rstest] 875 | #[case( 876 | Value::unchecked_single("single value"), 877 | vec![Some("single value".to_string())] 878 | )] 879 | #[case( 880 | Value::unchecked_multi(["multiple", "values"]), 881 | vec![Some("multiple".to_string()), Some("values".to_string())] 882 | )] 883 | #[case( 884 | Value::unchecked_multi(["multiple", "", "separated", "values"]), 885 | vec![Some("multiple".to_string()), None, Some("separated".to_string()), Some("values".to_string())] 886 | )] 887 | fn value_into_vec_of_option_string( 888 | #[case] value: Value, 889 | #[case] expected: Vec>, 890 | ) { 891 | let vec: Vec> = value.into(); 892 | assert_eq!(vec, expected); 893 | } 894 | 895 | proptest! { 896 | #[test] 897 | fn value_from_str_non_ascii_is_err(v in r"[^[[:ascii:]]]") { 898 | assert!(Name::from_str(&v).is_err()); 899 | } 900 | 901 | #[test] 902 | fn value_from_str_ascii_control_is_err(v in r"[[:cntrl:]]") { 903 | assert!(Name::from_str(&v).is_err()); 904 | } 905 | } 906 | } 907 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum InvalidNameError { 7 | #[error("cannot be empty")] 8 | Empty, 9 | #[error("cannot contain characters that are not part of the extended ASCII set")] 10 | NonAscii, 11 | #[error("cannot start with a non-letter ASCII character")] 12 | NonAsciiAlphabeticFirstChar, 13 | #[error("cannot end with a non-letter or non-digit ASCII character")] 14 | NonAsciiAlphanumericLastChar, 15 | } 16 | 17 | #[derive(Error, Debug)] 18 | pub enum InvalidValueError { 19 | #[error("cannot contain characters that are not part of the extended ASCII set")] 20 | NonExtendedAscii, 21 | #[error("cannot contain ASCII control characters")] 22 | ContainsControlChar, 23 | } 24 | 25 | #[derive(Error, Debug)] 26 | /// An error that can occur when parsing or trying to create an attribute that is invalid. 27 | pub enum AttributeError { 28 | /// The name of the attribute is invalid. 29 | #[error("Invalid attribute name: {0}")] 30 | InvalidName(#[from] InvalidNameError), 31 | /// The value of the attribute is invalid. 32 | #[error("Invalid attribute value: {0}")] 33 | InvalidValue(#[from] InvalidValueError), 34 | } 35 | 36 | /// An error that can occur when parsing RPSL text. 37 | /// 38 | /// # Example 39 | /// ``` 40 | /// # use rpsl::parse_object; 41 | /// let rpsl = "\ 42 | /// role; ACME Company 43 | /// 44 | /// "; 45 | /// let err = parse_object(rpsl).unwrap_err(); 46 | /// let message = "\ 47 | /// parse error at line 1, column 5 48 | /// | 49 | /// 1 | role; ACME Company 50 | /// | ^ 51 | /// invalid separator 52 | /// expected `:`"; 53 | /// assert_eq!(err.to_string(), message); 54 | /// ``` 55 | #[derive(Error, Debug)] 56 | pub struct ParseError(String); 57 | 58 | impl fmt::Display for ParseError { 59 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 60 | write!(f, "{}", self.0) 61 | } 62 | } 63 | 64 | impl From> for ParseError { 65 | fn from(value: winnow::error::ParseError<&str, winnow::error::ContextError>) -> Self { 66 | Self(value.to_string()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::pedantic)] 2 | #![warn(missing_docs)] 3 | #![forbid(unsafe_code)] 4 | #![forbid(clippy::panic)] 5 | #![doc = include_str!("../README.md")] 6 | #![cfg_attr(docsrs, feature(doc_cfg))] 7 | 8 | pub use attribute::{Attribute, Name, Value}; 9 | pub use error::{AttributeError, ParseError}; 10 | pub use object::Object; 11 | pub use parser::{parse_object, parse_whois_response}; 12 | 13 | mod attribute; 14 | #[allow(clippy::module_name_repetitions)] 15 | mod error; 16 | mod object; 17 | mod parser; 18 | -------------------------------------------------------------------------------- /src/object.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | ops::{Deref, Index}, 4 | }; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::Serialize; 8 | 9 | use super::Attribute; 10 | 11 | /// A RPSL object. 12 | /// 13 | /// ```text 14 | /// ┌───────────────────────────────────────────────┐ 15 | /// │ Object │ 16 | /// ├───────────────────────────────────────────────┤ 17 | /// │ [role] ──── ACME Company │ 18 | /// │ [address] ──┬─ Packet Street 6 │ 19 | /// │ ├─ 128 Series of Tubes │ 20 | /// │ └─ Internet │ 21 | /// │ [email] ──── rpsl-rs@github.com │ 22 | /// │ [nic-hdl] ──── RPSL1-RIPE │ 23 | /// │ [source] ──── RIPE │ 24 | /// └───────────────────────────────────────────────┘ 25 | /// ``` 26 | /// 27 | /// # Examples 28 | /// 29 | /// A role object for the ACME corporation. 30 | /// ``` 31 | /// # use rpsl::{Attribute, Object}; 32 | /// # fn main() -> Result<(), Box> { 33 | /// let role_acme = Object::new(vec![ 34 | /// Attribute::new("role".parse()?, "ACME Company".parse()?), 35 | /// Attribute::new("address".parse()?, "Packet Street 6".parse()?), 36 | /// Attribute::new("address".parse()?, "128 Series of Tubes".parse()?), 37 | /// Attribute::new("address".parse()?, "Internet".parse()?), 38 | /// Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?), 39 | /// Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?), 40 | /// Attribute::new("source".parse()?, "RIPE".parse()?), 41 | /// ]); 42 | /// # Ok(()) 43 | /// # } 44 | /// ``` 45 | /// 46 | /// Although creating an [`Object`] from a vector of [`Attribute`]s works, the more idiomatic way 47 | /// to do it is by using the [`object!`](crate::object!) macro. 48 | /// ``` 49 | /// # use rpsl::{Attribute, Object, object}; 50 | /// # fn main() -> Result<(), Box> { 51 | /// # let role_acme = Object::new(vec![ 52 | /// # Attribute::new("role".parse()?, "ACME Company".parse()?), 53 | /// # Attribute::new("address".parse()?, "Packet Street 6".parse()?), 54 | /// # Attribute::new("address".parse()?, "128 Series of Tubes".parse()?), 55 | /// # Attribute::new("address".parse()?, "Internet".parse()?), 56 | /// # Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?), 57 | /// # Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?), 58 | /// # Attribute::new("source".parse()?, "RIPE".parse()?), 59 | /// # ]); 60 | /// assert_eq!( 61 | /// role_acme, 62 | /// object! { 63 | /// "role": "ACME Company"; 64 | /// "address": "Packet Street 6"; 65 | /// "address": "128 Series of Tubes"; 66 | /// "address": "Internet"; 67 | /// "email": "rpsl-rs@github.com"; 68 | /// "nic-hdl": "RPSL1-RIPE"; 69 | /// "source": "RIPE"; 70 | /// }, 71 | /// ); 72 | /// # Ok(()) 73 | /// # } 74 | /// ``` 75 | /// 76 | /// Each attribute can be accessed by index. 77 | /// ``` 78 | /// # use rpsl::{Attribute, Object}; 79 | /// # fn main() -> Result<(), Box> { 80 | /// # let role_acme = Object::new(vec![ 81 | /// # Attribute::new("role".parse()?, "ACME Company".parse()?), 82 | /// # Attribute::new("address".parse()?, "Packet Street 6".parse()?), 83 | /// # Attribute::new("address".parse()?, "128 Series of Tubes".parse()?), 84 | /// # Attribute::new("address".parse()?, "Internet".parse()?), 85 | /// # Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?), 86 | /// # Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?), 87 | /// # Attribute::new("source".parse()?, "RIPE".parse()?), 88 | /// # ]); 89 | /// assert_eq!(role_acme[0], Attribute::new("role".parse()?, "ACME Company".parse()?)); 90 | /// assert_eq!(role_acme[6], Attribute::new("source".parse()?, "RIPE".parse()?)); 91 | /// # Ok(()) 92 | /// # } 93 | /// ``` 94 | /// 95 | /// While specific attribute values can be accessed by name. 96 | /// ``` 97 | /// # use rpsl::{Attribute, Object}; 98 | /// # fn main() -> Result<(), Box> { 99 | /// # let role_acme = Object::new(vec![ 100 | /// # Attribute::new("role".parse()?, "ACME Company".parse()?), 101 | /// # Attribute::new("address".parse()?, "Packet Street 6".parse()?), 102 | /// # Attribute::new("address".parse()?, "128 Series of Tubes".parse()?), 103 | /// # Attribute::new("address".parse()?, "Internet".parse()?), 104 | /// # Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?), 105 | /// # Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?), 106 | /// # Attribute::new("source".parse()?, "RIPE".parse()?), 107 | /// # ]); 108 | /// assert_eq!(role_acme.get("role"), vec!["ACME Company"]); 109 | /// assert_eq!(role_acme.get("address"), vec!["Packet Street 6", "128 Series of Tubes", "Internet"]); 110 | /// assert_eq!(role_acme.get("email"), vec!["rpsl-rs@github.com"]); 111 | /// assert_eq!(role_acme.get("nic-hdl"), vec!["RPSL1-RIPE"]); 112 | /// assert_eq!(role_acme.get("source"), vec!["RIPE"]); 113 | /// # Ok(()) 114 | /// # } 115 | /// ``` 116 | /// 117 | /// The entire object can also be represented as RPSL. 118 | /// ``` 119 | /// # use rpsl::{Attribute, Object}; 120 | /// # fn main() -> Result<(), Box> { 121 | /// # let role_acme = Object::new(vec![ 122 | /// # Attribute::new("role".parse()?, "ACME Company".parse()?), 123 | /// # Attribute::new("address".parse()?, "Packet Street 6".parse()?), 124 | /// # Attribute::new("address".parse()?, "128 Series of Tubes".parse()?), 125 | /// # Attribute::new("address".parse()?, "Internet".parse()?), 126 | /// # Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?), 127 | /// # Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?), 128 | /// # Attribute::new("source".parse()?, "RIPE".parse()?), 129 | /// # ]); 130 | /// assert_eq!( 131 | /// role_acme.to_string(), 132 | /// concat!( 133 | /// "role: ACME Company\n", 134 | /// "address: Packet Street 6\n", 135 | /// "address: 128 Series of Tubes\n", 136 | /// "address: Internet\n", 137 | /// "email: rpsl-rs@github.com\n", 138 | /// "nic-hdl: RPSL1-RIPE\n", 139 | /// "source: RIPE\n", 140 | /// "\n" 141 | /// ) 142 | /// ); 143 | /// # Ok(()) 144 | /// # } 145 | /// ``` 146 | /// 147 | /// Or serialized to JSON if the corresponding feature is enabled. 148 | /// ``` 149 | /// # use rpsl::{Attribute, Object}; 150 | /// # #[cfg(feature = "json")] 151 | /// # use serde_json::json; 152 | /// # let role_acme = Object::new(vec![ 153 | /// # Attribute::new("role".parse()?, "ACME Company".parse()?), 154 | /// # Attribute::new("address".parse()?, "Packet Street 6".parse()?), 155 | /// # Attribute::new("address".parse()?, "128 Series of Tubes".parse()?), 156 | /// # Attribute::new("address".parse()?, "Internet".parse()?), 157 | /// # Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?), 158 | /// # Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?), 159 | /// # Attribute::new("source".parse()?, "RIPE".parse()?), 160 | /// # ]); 161 | /// # #[cfg(feature = "json")] 162 | /// assert_eq!( 163 | /// role_acme.json(), 164 | /// json!({ 165 | /// "attributes": [ 166 | /// { "name": "role", "values": ["ACME Company"] }, 167 | /// { "name": "address", "values": ["Packet Street 6"] }, 168 | /// { "name": "address", "values": ["128 Series of Tubes"] }, 169 | /// { "name": "address", "values": ["Internet"] }, 170 | /// { "name": "email", "values": ["rpsl-rs@github.com"] }, 171 | /// { "name": "nic-hdl", "values": ["RPSL1-RIPE"] }, 172 | /// { "name": "source", "values": ["RIPE"] } 173 | /// ] 174 | /// }) 175 | /// ); 176 | /// # Ok::<(), Box>(()) 177 | /// ``` 178 | #[derive(Debug, Clone)] 179 | #[cfg_attr(feature = "serde", derive(Serialize))] 180 | #[allow(clippy::len_without_is_empty)] 181 | pub struct Object<'a> { 182 | attributes: Vec>, 183 | /// Contains the source if the object was created by parsing RPSL. 184 | #[cfg_attr(feature = "serde", serde(skip))] 185 | source: Option<&'a str>, 186 | } 187 | 188 | impl Object<'_> { 189 | /// Create a new RPSL object from a vector of attributes. 190 | /// 191 | /// # Example 192 | /// ``` 193 | /// # use rpsl::{Attribute, Object}; 194 | /// # fn main() -> Result<(), Box> { 195 | /// let role_acme = Object::new(vec![ 196 | /// Attribute::new("role".parse()?, "ACME Company".parse()?), 197 | /// Attribute::new("address".parse()?, "Packet Street 6".parse()?), 198 | /// Attribute::new("address".parse()?, "128 Series of Tubes".parse()?), 199 | /// Attribute::new("address".parse()?, "Internet".parse()?), 200 | /// Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?), 201 | /// Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?), 202 | /// Attribute::new("source".parse()?, "RIPE".parse()?), 203 | /// ]); 204 | /// # Ok(()) 205 | /// # } 206 | /// ``` 207 | #[must_use] 208 | pub fn new(attributes: Vec>) -> Object<'static> { 209 | Object { 210 | attributes, 211 | source: None, 212 | } 213 | } 214 | 215 | /// Create a new RPSL object from a text source and it's corresponding parsed attributes. 216 | pub(crate) fn from_parsed<'a>(source: &'a str, attributes: Vec>) -> Object<'a> { 217 | Object { 218 | attributes, 219 | source: Some(source), 220 | } 221 | } 222 | 223 | /// The number of attributes in the object. 224 | #[must_use] 225 | pub fn len(&self) -> usize { 226 | self.attributes.len() 227 | } 228 | 229 | /// Get the value(s) of specific attribute(s). 230 | #[must_use] 231 | pub fn get(&self, name: &str) -> Vec<&str> { 232 | self.attributes 233 | .iter() 234 | .filter(|a| a.name == name) 235 | .flat_map(|a| a.value.with_content()) 236 | .collect() 237 | } 238 | 239 | #[cfg(feature = "json")] 240 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 241 | #[allow(clippy::missing_panics_doc)] 242 | #[must_use] 243 | /// Serialize the object into a JSON value. 244 | pub fn json(&self) -> serde_json::Value { 245 | serde_json::to_value(self).unwrap() 246 | } 247 | 248 | /// Access the source field for use in tests. 249 | #[cfg(test)] 250 | pub(crate) fn source(&self) -> Option<&str> { 251 | self.source 252 | } 253 | } 254 | 255 | impl<'a> Index for Object<'a> { 256 | type Output = Attribute<'a>; 257 | 258 | fn index(&self, index: usize) -> &Self::Output { 259 | &self.attributes[index] 260 | } 261 | } 262 | 263 | impl<'a> Deref for Object<'a> { 264 | type Target = Vec>; 265 | 266 | fn deref(&self) -> &Self::Target { 267 | &self.attributes 268 | } 269 | } 270 | 271 | impl<'a> IntoIterator for Object<'a> { 272 | type Item = Attribute<'a>; 273 | type IntoIter = std::vec::IntoIter; 274 | 275 | fn into_iter(self) -> Self::IntoIter { 276 | self.attributes.into_iter() 277 | } 278 | } 279 | 280 | impl PartialEq for Object<'_> { 281 | /// Compare two objects. 282 | /// Since objects that are semantically equal may display differently, only `PartialEq` is implemented. 283 | fn eq(&self, other: &Self) -> bool { 284 | self.attributes == other.attributes 285 | } 286 | } 287 | 288 | impl fmt::Display for Object<'_> { 289 | /// Display the object as RPSL. 290 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 291 | if let Some(source) = self.source { 292 | write!(f, "{source}") 293 | } else { 294 | for attribute in &self.attributes { 295 | write!(f, "{attribute}")?; 296 | } 297 | writeln!(f) 298 | } 299 | } 300 | } 301 | 302 | /// Creates an [`Object`] containing the given attributes. 303 | /// 304 | /// - Create an [`Object`] containing only single value attributes: 305 | /// ``` 306 | /// # use rpsl::object; 307 | /// # fn main() -> Result<(), Box> { 308 | /// let obj = object! { 309 | /// "role": "ACME Company"; 310 | /// "address": "Packet Street 6"; 311 | /// "address": "128 Series of Tubes"; 312 | /// "address": "Internet"; 313 | /// }; 314 | /// assert_eq!(obj[0].name, "role"); 315 | /// assert_eq!(obj[0].value, "ACME Company"); 316 | /// assert_eq!(obj[1].name, "address"); 317 | /// assert_eq!(obj[1].value, "Packet Street 6"); 318 | /// assert_eq!(obj[2].name, "address"); 319 | /// assert_eq!(obj[2].value, "128 Series of Tubes"); 320 | /// assert_eq!(obj[3].name, "address"); 321 | /// assert_eq!(obj[3].value, "Internet"); 322 | /// # Ok(()) 323 | /// # } 324 | /// ``` 325 | /// 326 | /// - Create an `Object` containing multi value attributes: 327 | /// ``` 328 | /// # use rpsl::object; 329 | /// # fn main() -> Result<(), Box> { 330 | /// let obj = object! { 331 | /// "role": "ACME Company"; 332 | /// "address": "Packet Street 6", "128 Series of Tubes", "Internet"; 333 | /// }; 334 | /// assert_eq!(obj[0].name, "role"); 335 | /// assert_eq!(obj[0].value, "ACME Company"); 336 | /// assert_eq!(obj[1].name, "address"); 337 | /// assert_eq!(obj[1].value, vec!["Packet Street 6", "128 Series of Tubes", "Internet"]); 338 | /// # Ok(()) 339 | /// # } 340 | #[macro_export] 341 | macro_rules! object { 342 | ( 343 | $( 344 | $name:literal: $($value:literal),+ 345 | );+ $(;)? 346 | ) => { 347 | $crate::Object::new(vec![ 348 | $( 349 | $crate::Attribute::new($name.parse().unwrap(), vec![$($value),+].try_into().unwrap()), 350 | )* 351 | ]) 352 | }; 353 | } 354 | 355 | #[cfg(test)] 356 | mod tests { 357 | use rstest::*; 358 | #[cfg(feature = "json")] 359 | use serde_json::json; 360 | 361 | use super::*; 362 | 363 | #[rstest] 364 | #[case( 365 | Object::new(vec![ 366 | Attribute::unchecked_single("role", "ACME Company"), 367 | Attribute::unchecked_single("address", "Packet Street 6"), 368 | Attribute::unchecked_single("address", "128 Series of Tubes"), 369 | Attribute::unchecked_single("address", "Internet"), 370 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 371 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 372 | Attribute::unchecked_single("source", "RIPE"), 373 | ]), 374 | Object::from_parsed( 375 | concat!( 376 | "role: ACME Company\n", 377 | "address: Packet Street 6\n", 378 | "address: 128 Series of Tubes\n", 379 | "address: Internet\n", 380 | "email: rpsl-rs@github.com\n", 381 | "nic-hdl: RPSL1-RIPE\n", 382 | "source: RIPE\n", 383 | "\n" 384 | ), 385 | vec![ 386 | Attribute::unchecked_single("role", "ACME Company"), 387 | Attribute::unchecked_single("address", "Packet Street 6"), 388 | Attribute::unchecked_single("address", "128 Series of Tubes"), 389 | Attribute::unchecked_single("address", "Internet"), 390 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 391 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 392 | Attribute::unchecked_single("source", "RIPE"), 393 | ] 394 | ), 395 | concat!( 396 | "role: ACME Company\n", 397 | "address: Packet Street 6\n", 398 | "address: 128 Series of Tubes\n", 399 | "address: Internet\n", 400 | "email: rpsl-rs@github.com\n", 401 | "nic-hdl: RPSL1-RIPE\n", 402 | "source: RIPE\n", 403 | "\n" 404 | ) 405 | )] 406 | #[case( 407 | Object::new(vec![ 408 | Attribute::unchecked_single("role", "ACME Company"), 409 | Attribute::unchecked_multi( 410 | "address", 411 | ["Packet Street 6", "128 Series of Tubes", "Internet"] 412 | ), 413 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 414 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 415 | Attribute::unchecked_single("source", "RIPE"), 416 | ]), 417 | Object::from_parsed( 418 | concat!( 419 | "role: ACME Company\n", 420 | "address: Packet Street 6\n", 421 | " 128 Series of Tubes\n", 422 | " Internet\n", 423 | "email: rpsl-rs@github.com\n", 424 | "nic-hdl: RPSL1-RIPE\n", 425 | "source: RIPE\n", 426 | "\n" 427 | ), 428 | vec![ 429 | Attribute::unchecked_single("role", "ACME Company"), 430 | Attribute::unchecked_multi( 431 | "address", 432 | ["Packet Street 6", "128 Series of Tubes", "Internet"] 433 | ), 434 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 435 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 436 | Attribute::unchecked_single("source", "RIPE"), 437 | ] 438 | ), 439 | concat!( 440 | "role: ACME Company\n", 441 | "address: Packet Street 6\n", 442 | " 128 Series of Tubes\n", 443 | " Internet\n", 444 | "email: rpsl-rs@github.com\n", 445 | "nic-hdl: RPSL1-RIPE\n", 446 | "source: RIPE\n", 447 | "\n" 448 | ) 449 | )] 450 | fn object_display( 451 | #[case] owned: Object<'static>, 452 | #[case] borrowed: Object, 453 | #[case] expected: &str, 454 | ) { 455 | assert_eq!(owned.to_string(), expected); 456 | assert_eq!(borrowed.to_string(), expected); 457 | } 458 | 459 | #[rstest] 460 | #[case( 461 | Object::new(vec![ 462 | Attribute::unchecked_single("role", "ACME Company"), 463 | ]), 464 | 1 465 | )] 466 | #[case( 467 | Object::new(vec![ 468 | Attribute::unchecked_single("role", "ACME Company"), 469 | Attribute::unchecked_single("address", "Packet Street 6"), 470 | Attribute::unchecked_single("address", "128 Series of Tubes"), 471 | Attribute::unchecked_single("address", "Internet"), 472 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 473 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 474 | Attribute::unchecked_single("source", "RIPE"), 475 | ]), 476 | 7 477 | )] 478 | fn object_len(#[case] object: Object, #[case] expected: usize) { 479 | assert_eq!(object.len(), expected); 480 | } 481 | 482 | #[rstest] 483 | #[case( 484 | Object::new(vec![ 485 | Attribute::unchecked_single("role", "ACME Company"), 486 | Attribute::unchecked_single("address", "Packet Street 6"), 487 | Attribute::unchecked_single("address", "128 Series of Tubes"), 488 | Attribute::unchecked_single("address", "Internet"), 489 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 490 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 491 | Attribute::unchecked_single("source", "RIPE"), 492 | ]), 493 | 2, 494 | Attribute::unchecked_single("address", "128 Series of Tubes"), 495 | )] 496 | fn object_index(#[case] object: Object, #[case] index: usize, #[case] expected: Attribute) { 497 | assert_eq!(object[index], expected); 498 | } 499 | 500 | #[rstest] 501 | #[case( 502 | vec![ 503 | Attribute::unchecked_single("role", "ACME Company"), 504 | Attribute::unchecked_single("address", "Packet Street 6"), 505 | Attribute::unchecked_single("address", "128 Series of Tubes"), 506 | Attribute::unchecked_single("address", "Internet"), 507 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 508 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 509 | Attribute::unchecked_single("source", "RIPE"), 510 | ], 511 | )] 512 | fn object_deref(#[case] attributes: Vec>) { 513 | let object = Object::new(attributes.clone()); 514 | assert_eq!(*object, attributes); 515 | } 516 | 517 | #[rstest] 518 | #[case( 519 | vec![ 520 | Attribute::unchecked_single("role", "ACME Company"), 521 | Attribute::unchecked_single("address", "Packet Street 6"), 522 | Attribute::unchecked_single("address", "128 Series of Tubes"), 523 | Attribute::unchecked_single("address", "Internet"), 524 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 525 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 526 | Attribute::unchecked_single("source", "RIPE"), 527 | ], 528 | )] 529 | fn object_into_iter(#[case] attributes: Vec>) { 530 | let object = Object::new(attributes.clone()); 531 | 532 | let attr_iter = attributes.into_iter(); 533 | let obj_iter = object.into_iter(); 534 | 535 | for (a, b) in attr_iter.zip(obj_iter) { 536 | assert_eq!(a, b); 537 | } 538 | } 539 | 540 | #[rstest] 541 | #[case( 542 | Object::new(vec![ 543 | Attribute::unchecked_single("role", "ACME Company"), 544 | Attribute::unchecked_single("address", "Packet Street 6"), 545 | Attribute::unchecked_single("address", "128 Series of Tubes"), 546 | Attribute::unchecked_single("address", "Internet"), 547 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 548 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 549 | Attribute::unchecked_single("source", "RIPE"), 550 | ]), 551 | json!({ 552 | "attributes": [ 553 | { "name": "role", "values": ["ACME Company"] }, 554 | { "name": "address", "values": ["Packet Street 6"] }, 555 | { "name": "address", "values": ["128 Series of Tubes"] }, 556 | { "name": "address", "values": ["Internet"] }, 557 | { "name": "email", "values": ["rpsl-rs@github.com"] }, 558 | { "name": "nic-hdl", "values": ["RPSL1-RIPE"] }, 559 | { "name": "source", "values": ["RIPE"] } 560 | ] 561 | }) 562 | )] 563 | #[case( 564 | Object::new(vec![ 565 | Attribute::unchecked_single("role", "ACME Company"), 566 | Attribute::unchecked_multi( 567 | "address", 568 | ["Packet Street 6", "", "128 Series of Tubes", "Internet"] 569 | ), 570 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 571 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 572 | Attribute::unchecked_single("source", "RIPE"), 573 | ]), 574 | json!({ 575 | "attributes": [ 576 | { "name": "role", "values": ["ACME Company"] }, 577 | { 578 | "name": "address", 579 | "values": ["Packet Street 6", null, "128 Series of Tubes", "Internet"] }, 580 | { "name": "email", "values": ["rpsl-rs@github.com"] }, 581 | { "name": "nic-hdl", "values": ["RPSL1-RIPE"] }, 582 | { "name": "source", "values": ["RIPE"] } 583 | ] 584 | }) 585 | )] 586 | #[cfg(feature = "json")] 587 | fn object_json_repr(#[case] object: Object, #[case] expected: serde_json::Value) { 588 | let json = object.json(); 589 | assert_eq!(json, expected); 590 | } 591 | 592 | #[rstest] 593 | #[case( 594 | Object::from_parsed( 595 | concat!( 596 | "role: ACME Company\n", 597 | "address: Packet Street 6\n", 598 | "address: 128 Series of Tubes\n", 599 | "address: Internet\n", 600 | "email: rpsl-rs@github.com\n", 601 | "nic-hdl: RPSL1-RIPE\n", 602 | "source: RIPE\n", 603 | "\n", // Terminated by a trailing newline. 604 | ), 605 | vec![ 606 | Attribute::unchecked_single("role", "ACME Company"), 607 | Attribute::unchecked_single("address", "Packet Street 6"), 608 | Attribute::unchecked_single("address", "128 Series of Tubes"), 609 | Attribute::unchecked_single("address", "Internet"), 610 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 611 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 612 | Attribute::unchecked_single("source", "RIPE"), 613 | ], 614 | ), 615 | concat!( 616 | "role: ACME Company\n", 617 | "address: Packet Street 6\n", 618 | "address: 128 Series of Tubes\n", 619 | "address: Internet\n", 620 | "email: rpsl-rs@github.com\n", 621 | "nic-hdl: RPSL1-RIPE\n", 622 | "source: RIPE\n", 623 | "\n" // Contains a trailing newline. 624 | ) 625 | )] 626 | #[case( 627 | Object::from_parsed( 628 | concat!( 629 | "role: ACME Company\n", 630 | "address: Packet Street 6\n", 631 | "address: 128 Series of Tubes\n", 632 | "address: Internet\n", 633 | "email: rpsl-rs@github.com\n", 634 | "nic-hdl: RPSL1-RIPE\n", 635 | "source: RIPE\n", 636 | // Not terminated by a trailing newline. 637 | ), 638 | vec![ 639 | Attribute::unchecked_single("role", "ACME Company"), 640 | Attribute::unchecked_single("address", "Packet Street 6"), 641 | Attribute::unchecked_single("address", "128 Series of Tubes"), 642 | Attribute::unchecked_single("address", "Internet"), 643 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 644 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 645 | Attribute::unchecked_single("source", "RIPE"), 646 | ], 647 | ), 648 | concat!( 649 | "role: ACME Company\n", 650 | "address: Packet Street 6\n", 651 | "address: 128 Series of Tubes\n", 652 | "address: Internet\n", 653 | "email: rpsl-rs@github.com\n", 654 | "nic-hdl: RPSL1-RIPE\n", 655 | "source: RIPE\n", 656 | // Does not contain a trailing newline. 657 | ) 658 | )] 659 | #[case( 660 | Object::from_parsed( 661 | concat!( 662 | "role: ACME Company\n", 663 | "address: Packet Street 6\n", 664 | // Using space as a continuation char. 665 | " 128 Series of Tubes\n", 666 | " Internet\n", 667 | "email: rpsl-rs@github.com\n", 668 | "nic-hdl: RPSL1-RIPE\n", 669 | "source: RIPE\n", 670 | "\n" 671 | ), 672 | vec![ 673 | Attribute::unchecked_single("role", "ACME Company"), 674 | Attribute::unchecked_multi( 675 | "address", 676 | ["Packet Street 6", "128 Series of Tubes", "Internet"] 677 | ), 678 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 679 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 680 | Attribute::unchecked_single("source", "RIPE"), 681 | ], 682 | ), 683 | concat!( 684 | "role: ACME Company\n", 685 | "address: Packet Street 6\n", 686 | // Using space as a continuation char. 687 | " 128 Series of Tubes\n", 688 | " Internet\n", 689 | "email: rpsl-rs@github.com\n", 690 | "nic-hdl: RPSL1-RIPE\n", 691 | "source: RIPE\n", 692 | "\n" 693 | ) 694 | )] 695 | #[case( 696 | Object::from_parsed( 697 | concat!( 698 | "role: ACME Company\n", 699 | "address: Packet Street 6\n", 700 | // Using + as a continuation char. 701 | "+ 128 Series of Tubes\n", 702 | "+ Internet\n", 703 | "email: rpsl-rs@github.com\n", 704 | "nic-hdl: RPSL1-RIPE\n", 705 | "source: RIPE\n", 706 | "\n" 707 | ), 708 | vec![ 709 | Attribute::unchecked_single("role", "ACME Company"), 710 | Attribute::unchecked_multi( 711 | "address", 712 | ["Packet Street 6", "128 Series of Tubes", "Internet"] 713 | ), 714 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 715 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 716 | Attribute::unchecked_single("source", "RIPE"), 717 | ], 718 | ), 719 | concat!( 720 | "role: ACME Company\n", 721 | "address: Packet Street 6\n", 722 | // Using + as a continuation char. 723 | "+ 128 Series of Tubes\n", 724 | "+ Internet\n", 725 | "email: rpsl-rs@github.com\n", 726 | "nic-hdl: RPSL1-RIPE\n", 727 | "source: RIPE\n", 728 | "\n" 729 | ) 730 | )] 731 | /// Borrowed objects display as the original RPSL they were created from. 732 | fn borrowed_objects_display_like_source(#[case] object: Object, #[case] expected: &str) { 733 | assert_eq!(object.to_string(), expected); 734 | } 735 | 736 | #[rstest] 737 | #[case( 738 | object! { 739 | "role": "ACME Company"; 740 | "address": "Packet Street 6", "128 Series of Tubes", "Internet"; 741 | "email": "rpsl-rs@github.com"; 742 | "nic-hdl": "RPSL1-RIPE"; 743 | "source": "RIPE"; 744 | }, 745 | Object::new(vec![ 746 | Attribute::unchecked_single("role", "ACME Company"), 747 | Attribute::unchecked_multi( 748 | "address", 749 | ["Packet Street 6", "128 Series of Tubes", "Internet"], 750 | ), 751 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 752 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"), 753 | Attribute::unchecked_single("source", "RIPE"), 754 | ]) 755 | )] 756 | fn object_from_macro(#[case] from_macro: Object, #[case] expected: Object) { 757 | assert_eq!(from_macro, expected); 758 | } 759 | 760 | #[rstest] 761 | #[case( 762 | Object::new(vec![ 763 | Attribute::unchecked_single("aut-num", "AS42"), 764 | Attribute::unchecked_single( 765 | "remarks", 766 | "All imported prefixes will be tagged with geographic communities and", 767 | ), 768 | Attribute::unchecked_single( 769 | "remarks", 770 | "the type of peering relationship according to the table below, using the default", 771 | ), 772 | Attribute::unchecked_single( 773 | "remarks", 774 | "announce rule (x=0).", 775 | ), 776 | Attribute::unchecked_single("remarks", None), 777 | Attribute::unchecked_single( 778 | "remarks", 779 | "The following communities can be used by peers and customers", 780 | ), 781 | Attribute::unchecked_multi( 782 | "remarks", 783 | [ 784 | "x = 0 - Announce (default rule)", 785 | "x = 1 - Prepend x1", 786 | "x = 2 - Prepend x2", 787 | "x = 3 - Prepend x3", 788 | "x = 9 - Do not announce", 789 | ], 790 | ), 791 | ]), 792 | vec![ 793 | ("aut-num", vec!["AS42"]), 794 | ( 795 | "remarks", 796 | vec![ 797 | "All imported prefixes will be tagged with geographic communities and", 798 | "the type of peering relationship according to the table below, using the default", 799 | "announce rule (x=0).", 800 | "The following communities can be used by peers and customers", 801 | "x = 0 - Announce (default rule)", 802 | "x = 1 - Prepend x1", 803 | "x = 2 - Prepend x2", 804 | "x = 3 - Prepend x3", 805 | "x = 9 - Do not announce", 806 | ] 807 | ) 808 | ] 809 | )] 810 | fn get_values_by_name(#[case] object: Object, #[case] name_expected: Vec<(&str, Vec<&str>)>) { 811 | for (name, expected) in name_expected { 812 | assert_eq!(object.get(name), expected); 813 | } 814 | } 815 | 816 | #[rstest] 817 | #[case( 818 | Object::new( 819 | vec![ 820 | Attribute::unchecked_single("role", "ACME Company"), 821 | Attribute::unchecked_single("address", "Packet Street 6"), 822 | Attribute::unchecked_single("address", "128 Series of Tubes"), 823 | Attribute::unchecked_single("address", "Internet"), 824 | ]), 825 | Object::new( 826 | vec![ 827 | Attribute::unchecked_single("role", "ACME Company"), 828 | Attribute::unchecked_single("address", "Packet Street 6"), 829 | Attribute::unchecked_single("address", "128 Series of Tubes"), 830 | Attribute::unchecked_single("address", "Internet"), 831 | ]), 832 | )] 833 | #[case( 834 | Object::from_parsed( 835 | concat!( 836 | "role: ACME Company\n", 837 | "address: Packet Street 6\n", 838 | "address: 128 Series of Tubes\n", 839 | "address: Internet\n", 840 | "\n" 841 | ), 842 | vec![ 843 | Attribute::unchecked_single("role", "ACME Company"), 844 | Attribute::unchecked_single("address", "Packet Street 6"), 845 | Attribute::unchecked_single("address", "128 Series of Tubes"), 846 | Attribute::unchecked_single("address", "Internet"), 847 | ], 848 | ), 849 | Object::new( 850 | vec![ 851 | Attribute::unchecked_single("role", "ACME Company"), 852 | Attribute::unchecked_single("address", "Packet Street 6"), 853 | Attribute::unchecked_single("address", "128 Series of Tubes"), 854 | Attribute::unchecked_single("address", "Internet"), 855 | ], 856 | ), 857 | )] 858 | /// Objects with equal attributes evaluate as equal, without taking the source field into consideration. 859 | fn eq_objects_are_eq(#[case] object_1: Object, #[case] object_2: Object) { 860 | assert_eq!(object_1, object_2); 861 | } 862 | 863 | #[rstest] 864 | #[case( 865 | Object::new( 866 | vec![ 867 | Attribute::unchecked_single("role", "Umbrella Corporation"), 868 | Attribute::unchecked_single("address", "Paraguas Street"), 869 | Attribute::unchecked_single("address", "Raccoon City"), 870 | Attribute::unchecked_single("address", "Colorado"), 871 | ]), 872 | Object::new( 873 | vec![ 874 | Attribute::unchecked_single("role", "ACME Company"), 875 | Attribute::unchecked_single("address", "Packet Street 6"), 876 | Attribute::unchecked_single("address", "128 Series of Tubes"), 877 | Attribute::unchecked_single("address", "Internet"), 878 | ]), 879 | )] 880 | /// Objects that have different attributes do not evaluate as equal. 881 | fn ne_objects_are_ne(#[case] object_1: Object, #[case] object_2: Object) { 882 | assert_ne!(object_1, object_2); 883 | } 884 | } 885 | -------------------------------------------------------------------------------- /src/parser/api.rs: -------------------------------------------------------------------------------- 1 | use winnow::{ 2 | ascii::multispace0, 3 | combinator::{delimited, repeat}, 4 | error::ContextError, 5 | Parser, 6 | }; 7 | 8 | use super::core::{object_block, object_block_padded}; 9 | use crate::{Object, ParseError}; 10 | 11 | /// Parse RPSL into an [`Object`], borrowing from the source. 12 | /// 13 | /// ```text 14 | /// role: ACME Company 15 | /// address: Packet Street 6 16 | /// address: 128 Series of Tubes 17 | /// address: Internet 18 | /// email: rpsl-rs@github.com 19 | /// nic-hdl: RPSL1-RIPE 20 | /// source: RIPE 21 | /// ↓ 22 | /// role: ACME Company ◀─────────────── &"role": &"ACME Company" 23 | /// address: Packet Street 6 ◀──────────── &"address": &"Packet Street 6" 24 | /// address: 128 Series of Tubes ◀──────── &"address": &"128 Series of Tubes" 25 | /// address: Internet ◀─────────────────── &"address": &"Internet" 26 | /// email: rpsl-rs@github.com ◀───────── &"email": &"rpsl-rs@github.com" 27 | /// nic-hdl: RPSL1-RIPE ◀───────────────── &"nic-hdl": &"RPSL1-RIPE" 28 | /// source: RIPE ◀─────────────────────── &"source": &"RIPE" 29 | /// ``` 30 | /// 31 | /// # Errors 32 | /// Returns a [`ParseError`] if the input is not valid RPSL. 33 | /// 34 | /// # Examples 35 | /// ``` 36 | /// # use rpsl::{parse_object, object}; 37 | /// # fn main() -> Result<(), Box> { 38 | /// let role_acme = " 39 | /// role: ACME Company 40 | /// address: Packet Street 6 41 | /// address: 128 Series of Tubes 42 | /// address: Internet 43 | /// email: rpsl-rs@github.com 44 | /// nic-hdl: RPSL1-RIPE 45 | /// source: RIPE 46 | /// 47 | /// "; 48 | /// let parsed = parse_object(role_acme)?; 49 | /// assert_eq!( 50 | /// parsed, 51 | /// object! { 52 | /// "role": "ACME Company"; 53 | /// "address": "Packet Street 6"; 54 | /// "address": "128 Series of Tubes"; 55 | /// "address": "Internet"; 56 | /// "email": "rpsl-rs@github.com"; 57 | /// "nic-hdl": "RPSL1-RIPE"; 58 | /// "source": "RIPE"; 59 | /// } 60 | /// ); 61 | /// # Ok(()) 62 | /// # } 63 | /// ``` 64 | /// 65 | /// Values spread over multiple lines can be parsed too. 66 | /// ``` 67 | /// # use rpsl::{parse_object, object}; 68 | /// # fn main() -> Result<(), Box> { 69 | /// let multiline_remark = " 70 | /// remarks: Value 1 71 | /// Value 2 72 | /// 73 | /// "; 74 | /// assert_eq!( 75 | /// parse_object(multiline_remark)?, 76 | /// object! { 77 | /// "remarks": "Value 1", "Value 2"; 78 | /// } 79 | /// ); 80 | /// # Ok(()) 81 | /// # } 82 | /// ``` 83 | /// 84 | /// An attribute that does not have a value is valid. 85 | /// ``` 86 | /// # use rpsl::{parse_object, object}; 87 | /// # fn main() -> Result<(), Box> { 88 | /// let without_value = " 89 | /// as-name: REMARKABLE 90 | /// remarks: 91 | /// remarks: ^^^^^^^^^^ nothing here 92 | /// 93 | /// "; 94 | /// assert_eq!( 95 | /// parse_object(without_value)?, 96 | /// object! { 97 | /// "as-name": "REMARKABLE"; 98 | /// "remarks": ""; 99 | /// "remarks": "^^^^^^^^^^ nothing here"; 100 | /// } 101 | /// ); 102 | /// # Ok(()) 103 | /// # } 104 | /// ``` 105 | /// 106 | /// The same goes for values containing only whitespace. 107 | /// Since whitespace to the left of a value is trimmed, they are equivalent to no value. 108 | /// 109 | /// ``` 110 | /// # use rpsl::{parse_object, object}; 111 | /// # fn main() -> Result<(), Box> { 112 | /// let whitespace_value = " 113 | /// as-name: REMARKABLE 114 | /// remarks: 115 | /// remarks: ^^^^^^^^^^ nothing but hot air (whitespace) 116 | /// 117 | /// "; 118 | /// assert_eq!( 119 | /// parse_object(whitespace_value)?, 120 | /// object! { 121 | /// "as-name": "REMARKABLE"; 122 | /// "remarks": ""; 123 | /// "remarks": "^^^^^^^^^^ nothing but hot air (whitespace)"; 124 | /// } 125 | /// ); 126 | /// # Ok(()) 127 | /// # } 128 | /// ``` 129 | pub fn parse_object(rpsl: &str) -> Result { 130 | let block_parser = object_block::(); 131 | let object = delimited(multispace0, block_parser, multispace0).parse(rpsl)?; 132 | Ok(object) 133 | } 134 | 135 | /// Parse a WHOIS server response into [`Object`]s contained within. 136 | /// 137 | /// # Errors 138 | /// Returns a [`ParseError`] error if the input is not valid RPSL. 139 | /// 140 | /// # Examples 141 | /// ``` 142 | /// # use rpsl::{parse_whois_response, object}; 143 | /// # fn main() -> Result<(), Box> { 144 | /// let whois_response = " 145 | /// ASNumber: 32934 146 | /// ASName: FACEBOOK 147 | /// ASHandle: AS32934 148 | /// RegDate: 2004-08-24 149 | /// Updated: 2012-02-24 150 | /// Comment: Please send abuse reports to abuse@facebook.com 151 | /// Ref: https://rdap.arin.net/registry/autnum/32934 152 | /// 153 | /// 154 | /// OrgName: Facebook, Inc. 155 | /// OrgId: THEFA-3 156 | /// Address: 1601 Willow Rd. 157 | /// City: Menlo Park 158 | /// StateProv: CA 159 | /// PostalCode: 94025 160 | /// Country: US 161 | /// RegDate: 2004-08-11 162 | /// Updated: 2012-04-17 163 | /// Ref: https://rdap.arin.net/registry/entity/THEFA-3 164 | /// 165 | /// "; 166 | /// let objects = parse_whois_response(whois_response)?; 167 | /// assert_eq!( 168 | /// objects, 169 | /// vec![ 170 | /// object! { 171 | /// "ASNumber": "32934"; 172 | /// "ASName": "FACEBOOK"; 173 | /// "ASHandle": "AS32934"; 174 | /// "RegDate": "2004-08-24"; 175 | /// "Updated": "2012-02-24"; 176 | /// "Comment": "Please send abuse reports to abuse@facebook.com"; 177 | /// "Ref": "https://rdap.arin.net/registry/autnum/32934"; 178 | /// }, 179 | /// object! { 180 | /// "OrgName": "Facebook, Inc."; 181 | /// "OrgId": "THEFA-3"; 182 | /// "Address": "1601 Willow Rd."; 183 | /// "City": "Menlo Park"; 184 | /// "StateProv": "CA"; 185 | /// "PostalCode": "94025"; 186 | /// "Country": "US"; 187 | /// "RegDate": "2004-08-11"; 188 | /// "Updated": "2012-04-17"; 189 | /// "Ref": "https://rdap.arin.net/registry/entity/THEFA-3"; 190 | /// } 191 | /// ] 192 | /// ); 193 | /// # Ok(()) 194 | /// # } 195 | pub fn parse_whois_response(response: &str) -> Result, ParseError> { 196 | let block_parser = object_block_padded(object_block::()); 197 | let objects = repeat(1.., block_parser).parse(response)?; 198 | Ok(objects) 199 | } 200 | -------------------------------------------------------------------------------- /src/parser/core.rs: -------------------------------------------------------------------------------- 1 | use winnow::{ 2 | ascii::{newline, space0}, 3 | combinator::{alt, delimited, peek, preceded, repeat, separated_pair, terminated}, 4 | error::{AddContext, ContextError, ParserError, StrContext, StrContextValue}, 5 | token::{one_of, take_while}, 6 | Parser, 7 | }; 8 | 9 | use crate::{Attribute, Name, Object, Value}; 10 | 11 | /// Generate an object block parser. 12 | /// As per [RFC 2622](https://datatracker.ietf.org/doc/html/rfc2622#section-2), an RPSL object 13 | /// is textually represented as a list of attribute-value pairs that ends when a blank line is encountered. 14 | pub fn object_block<'s, E>() -> impl Parser<&'s str, Object<'s>, E> 15 | where 16 | E: ParserError<&'s str> + AddContext<&'s str, StrContext>, 17 | { 18 | terminated(repeat(1.., attribute()), newline) 19 | .with_taken() 20 | .map(|(attributes, source)| Object::from_parsed(source, attributes)) 21 | } 22 | 23 | /// Generate a parser that extends the given object block parser to consume optional padding 24 | /// server messages or newlines. 25 | pub fn object_block_padded<'s, P, E>(block_parser: P) -> impl Parser<&'s str, Object<'s>, E> 26 | where 27 | P: Parser<&'s str, Object<'s>, E>, 28 | E: ParserError<&'s str>, 29 | { 30 | delimited( 31 | consume_opt_messages_or_newlines(), 32 | block_parser, 33 | consume_opt_messages_or_newlines(), 34 | ) 35 | } 36 | 37 | /// Generate a parser that consumes optional messages or newlines. 38 | fn consume_opt_messages_or_newlines<'s, E>() -> impl Parser<&'s str, (), E> 39 | where 40 | E: ParserError<&'s str>, 41 | { 42 | repeat(0.., alt((newline.void(), server_message().void()))) 43 | } 44 | 45 | // A response code or message sent by the whois server. 46 | // Starts with the "%" character and extends until the end of the line. 47 | // In contrast to RPSL, characters are not limited to ASCII. 48 | fn server_message<'s, E>() -> impl Parser<&'s str, &'s str, E> 49 | where 50 | E: ParserError<&'s str>, 51 | { 52 | delimited( 53 | ('%', space0), 54 | take_while(0.., |c: char| !c.is_control()), 55 | newline, 56 | ) 57 | } 58 | 59 | // Generate an attribute parser. 60 | // The attributes name and value are separated by a colon and optional spaces. 61 | fn attribute<'s, E>() -> impl Parser<&'s str, Attribute<'s>, E> 62 | where 63 | E: ParserError<&'s str> + AddContext<&'s str, StrContext>, 64 | { 65 | separated_pair( 66 | attribute_name(), 67 | ( 68 | ':'.context(StrContext::Label("separator")) 69 | .context(StrContext::Expected(StrContextValue::StringLiteral(":"))), 70 | space0, 71 | ), 72 | attribute_value(), 73 | ) 74 | .map(|(name, value)| Attribute::new(name, value)) 75 | } 76 | 77 | /// Generate an attribute value parser that parses an ASCII sequence of letters, 78 | /// digits and the characters "-", "_". The first character must be a letter, 79 | /// while the last character may be a letter or a digit. 80 | fn attribute_name<'s, E>() -> impl Parser<&'s str, Name<'s>, E> 81 | where 82 | E: ParserError<&'s str>, 83 | { 84 | take_while(2.., ('A'..='Z', 'a'..='z', '0'..='9', '-', '_')) 85 | .verify(|s: &str| { 86 | s.starts_with(|c: char| c.is_ascii_alphabetic()) 87 | && s.ends_with(|c: char| c.is_ascii_alphanumeric()) 88 | }) 89 | .map(Name::unchecked) 90 | } 91 | 92 | /// Generate an attribute value parser that includes continuation lines. 93 | fn attribute_value<'s, E>() -> impl Parser<&'s str, Value<'s>, E> 94 | where 95 | E: ParserError<&'s str>, 96 | { 97 | move |input: &mut &'s str| { 98 | let first_value = single_attribute_value().parse_next(input)?; 99 | 100 | if peek(continuation_char::()) 101 | .parse_next(input) 102 | .is_ok() 103 | { 104 | let mut continuation_values: Vec<&str> = repeat( 105 | 1.., 106 | preceded( 107 | continuation_char(), 108 | preceded(space0, single_attribute_value()), 109 | ), 110 | ) 111 | .parse_next(input)?; 112 | continuation_values.insert(0, first_value); 113 | return Ok(Value::unchecked_multi(continuation_values)); 114 | } 115 | 116 | Ok(Value::unchecked_single(first_value)) 117 | } 118 | } 119 | 120 | /// Generate a parser for a singular attribute value without continuation. 121 | fn single_attribute_value<'s, E>() -> impl Parser<&'s str, &'s str, E> 122 | where 123 | E: ParserError<&'s str>, 124 | { 125 | terminated( 126 | take_while(0.., |c| Value::validate_char(c).is_ok()), 127 | newline, 128 | ) 129 | } 130 | 131 | /// Generate a parser for a single continuation character. 132 | fn continuation_char<'s, E>() -> impl Parser<&'s str, char, E> 133 | where 134 | E: ParserError<&'s str>, 135 | { 136 | one_of([' ', '\t', '+']) 137 | } 138 | 139 | #[cfg(test)] 140 | mod tests { 141 | use proptest::prelude::*; 142 | use rstest::*; 143 | use winnow::error::ContextError; 144 | 145 | use super::*; 146 | 147 | #[rstest] 148 | #[case( 149 | &mut concat!( 150 | "email: rpsl-rs@github.com\n", 151 | "nic-hdl: RPSL1-RIPE\n", 152 | "\n" 153 | ), 154 | vec![ 155 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 156 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE") 157 | ] 158 | )] 159 | fn object_block_valid(#[case] given: &mut &str, #[case] attributes: Vec) { 160 | let expected = Object::from_parsed(given, attributes); 161 | 162 | let mut parser = object_block::(); 163 | let parsed = parser.parse_next(given).unwrap(); 164 | 165 | assert_eq!(parsed, expected); 166 | } 167 | 168 | #[test] 169 | /// When parsing RPSL, the resulting object contains the original source it was created from. 170 | fn object_block_parsed_object_contains_source() { 171 | let rpsl = &mut concat!( 172 | "email: rpsl-rs@github.com\n", 173 | "nic-hdl: RPSL1-RIPE\n", 174 | "\n" 175 | ); 176 | let source = *rpsl; 177 | 178 | let mut parser = object_block::(); 179 | let parsed = parser.parse_next(rpsl).unwrap(); 180 | 181 | assert_eq!(parsed.source().unwrap(), source); 182 | } 183 | 184 | #[test] 185 | fn object_block_without_newline_termination_is_err() { 186 | let object = &mut concat!( 187 | "email: rpsl-rs@github.com\n", 188 | "nic-hdl: RPSL1-RIPE\n", 189 | ); 190 | let mut parser = object_block::(); 191 | assert!(parser.parse_next(object).is_err()); 192 | } 193 | 194 | #[rstest] 195 | #[case( 196 | &mut concat!( 197 | "\n\n", 198 | "email: rpsl-rs@github.com\n", 199 | "nic-hdl: RPSL1-RIPE\n", 200 | "\n", 201 | "\n\n\n" 202 | ), 203 | vec![ 204 | Attribute::unchecked_single("email", "rpsl-rs@github.com"), 205 | Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE") 206 | ] 207 | )] 208 | fn object_block_padded_valid(#[case] given: &mut &str, #[case] attributes: Vec) { 209 | let expected = Object::from_parsed(given, attributes); 210 | 211 | let mut parser = object_block_padded::<_, ContextError>(object_block()); 212 | let parsed = parser.parse_next(given).unwrap(); 213 | 214 | assert_eq!(parsed, expected); 215 | } 216 | 217 | #[rstest] 218 | #[case( 219 | &mut "% Note: This is a server message\n" 220 | )] 221 | #[case( 222 | &mut concat!( 223 | "\n", 224 | "% Note: This is a server message followed by an empty line\n" 225 | ) 226 | )] 227 | #[case( 228 | &mut concat!( 229 | "% Note: This is a server message preceding some newlines.\n", 230 | "\n", 231 | "\n", 232 | ) 233 | )] 234 | fn optional_comment_or_newlines_consumed(#[case] given: &mut &str) { 235 | let mut parser = consume_opt_messages_or_newlines::(); 236 | parser.parse_next(given).unwrap(); 237 | assert_eq!(*given, ""); 238 | } 239 | 240 | #[test] 241 | fn optional_comment_or_newlines_optional() { 242 | let mut parser = consume_opt_messages_or_newlines::(); 243 | assert_eq!(parser.parse_next(&mut ""), Ok(())); 244 | } 245 | 246 | #[rstest] 247 | #[case( 248 | &mut "% Note: this output has been filtered.\n", 249 | "Note: this output has been filtered.", 250 | "" 251 | )] 252 | #[case( 253 | &mut "% To receive output for a database update, use the \"-B\" flag.\n", 254 | "To receive output for a database update, use the \"-B\" flag.", 255 | "" 256 | )] 257 | #[case( 258 | &mut "% This query was served by the RIPE Database Query Service version 1.106.1 (BUSA)\n", 259 | "This query was served by the RIPE Database Query Service version 1.106.1 (BUSA)", 260 | "" 261 | )] 262 | fn server_message_valid( 263 | #[case] given: &mut &str, 264 | #[case] expected: &str, 265 | #[case] remaining: &str, 266 | ) { 267 | let mut parser = server_message::(); 268 | let parsed = parser.parse_next(given).unwrap(); 269 | assert_eq!(parsed, expected); 270 | assert_eq!(*given, remaining); 271 | } 272 | 273 | #[rstest] 274 | #[case( 275 | &mut "import: from AS12 accept AS12\n", 276 | Attribute::unchecked_single("import", "from AS12 accept AS12"), 277 | "" 278 | )] 279 | fn attribute_valid_single_value( 280 | #[case] given: &mut &str, 281 | #[case] expected: Attribute, 282 | #[case] remaining: &str, 283 | ) { 284 | let mut parser = attribute::(); 285 | let parsed = parser.parse_next(given).unwrap(); 286 | assert_eq!(parsed, expected); 287 | assert_eq!(*given, remaining); 288 | } 289 | 290 | #[rstest] 291 | #[case( 292 | &mut concat!( 293 | "remarks: Locations\n", 294 | " LA1 - CoreSite One Wilshire\n", 295 | " NY1 - Equinix New York, Newark\n", 296 | "remarks: Peering Policy\n", 297 | ), 298 | Attribute::unchecked_multi( 299 | "remarks", 300 | vec![ 301 | "Locations", 302 | "LA1 - CoreSite One Wilshire", 303 | "NY1 - Equinix New York, Newark", 304 | ] 305 | ), 306 | "remarks: Peering Policy\n" 307 | )] 308 | #[case( 309 | &mut concat!( 310 | "remarks: Test\n", 311 | " continuation value prefixed by a space\n", 312 | "\t continuation value prefixed by a tab\n", 313 | "+ continuation value prefixed by a plus\n", 314 | ), 315 | Attribute::unchecked_multi( 316 | "remarks", 317 | vec![ 318 | "Test", 319 | "continuation value prefixed by a space", 320 | "continuation value prefixed by a tab", 321 | "continuation value prefixed by a plus" 322 | ] 323 | ), 324 | "" 325 | )] 326 | fn attribute_valid_multi_value( 327 | #[case] given: &mut &str, 328 | #[case] expected: Attribute, 329 | #[case] remaining: &str, 330 | ) { 331 | let mut parser = attribute::(); 332 | let parsed = parser.parse_next(given).unwrap(); 333 | assert_eq!(parsed, expected); 334 | assert_eq!(*given, remaining); 335 | } 336 | 337 | #[rstest] 338 | #[case(&mut "remarks:", "remarks", ":")] 339 | #[case(&mut "aut-num:", "aut-num", ":")] 340 | #[case(&mut "ASNumber:", "ASNumber", ":")] 341 | #[case(&mut "route6:", "route6", ":")] 342 | fn attribute_name_valid( 343 | #[case] given: &mut &str, 344 | #[case] expected: &str, 345 | #[case] remaining: &str, 346 | ) { 347 | let mut parser = attribute_name::(); 348 | let parsed = parser.parse_next(given).unwrap(); 349 | assert_eq!(parsed, expected); 350 | assert_eq!(*given, remaining); 351 | } 352 | 353 | #[rstest] 354 | #[case(&mut "1remarks:")] 355 | #[case(&mut "-remarks:")] 356 | #[case(&mut "_remarks:")] 357 | fn attribute_name_non_letter_first_char_is_error(#[case] given: &mut &str) { 358 | let mut parser = attribute_name::(); 359 | assert!(parser.parse_next(given).is_err()); 360 | } 361 | 362 | #[rstest] 363 | #[case(&mut "remarks-:")] 364 | #[case(&mut "remarks_:")] 365 | fn attribute_name_non_letter_or_digit_last_char_is_error(#[case] given: &mut &str) { 366 | let mut parser = attribute_name::(); 367 | assert!(parser.parse_next(given).is_err()); 368 | } 369 | 370 | #[test] 371 | fn attribute_name_single_letter_is_error() { 372 | let mut parser = attribute_name::(); 373 | assert!(parser.parse_next(&mut "a").is_err()); 374 | } 375 | 376 | #[rstest] 377 | #[case( 378 | &mut "This is an example remark\n", 379 | "This is an example remark", 380 | "" 381 | )] 382 | #[case( 383 | &mut "Concerning abuse and spam ... mailto: abuse@asn.net\n", 384 | "Concerning abuse and spam ... mailto: abuse@asn.net", 385 | "" 386 | )] 387 | #[case( 388 | &mut "+49 176 07071964\n", 389 | "+49 176 07071964", 390 | "" 391 | )] 392 | #[case( 393 | &mut "* Equinix FR5, Kleyerstr, Frankfurt am Main\n", 394 | "* Equinix FR5, Kleyerstr, Frankfurt am Main", 395 | "" 396 | )] 397 | fn attribute_value_valid( 398 | #[case] given: &mut &str, 399 | #[case] expected: &str, 400 | #[case] remaining: &str, 401 | ) { 402 | let mut parser = single_attribute_value::(); 403 | let parsed = parser.parse_next(given).unwrap(); 404 | assert_eq!(parsed, expected); 405 | assert_eq!(*given, remaining); 406 | } 407 | 408 | proptest! { 409 | /// Parsing any non extended ASCII returns an error. 410 | #[test] 411 | fn attribute_value_non_extended_ascii_is_err(s in r"[^\x00-\xFF]+") { 412 | let mut parser = single_attribute_value::(); 413 | assert!(parser.parse_next(&mut s.as_str()).is_err()); 414 | } 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | pub use api::{parse_object, parse_whois_response}; 2 | 3 | mod api; 4 | mod core; 5 | -------------------------------------------------------------------------------- /tests/test_parsing.rs: -------------------------------------------------------------------------------- 1 | use proptest::prelude::*; 2 | use rpsl::parse_object; 3 | 4 | proptest! { 5 | /// Property based test to ensure any kind of valid RPSL is parsed correctly. 6 | #[test] 7 | fn rpsl_parsed_to_object((object, rpsl) in strategies::object_w_rpsl()) { 8 | let parsed = parse_object(&rpsl).unwrap(); 9 | prop_assert_eq!(parsed, object); 10 | } 11 | } 12 | 13 | mod strategies { 14 | use proptest::prelude::*; 15 | 16 | /// A valid attribute name. 17 | /// 18 | /// According to RFC 2622, an "" is made up of letters, digits, the character underscore "_", 19 | /// and the character hyphen "-"; the first character of a name must be a letter, and 20 | /// the last character of a name must be a letter or a digit. 21 | /// 22 | /// Creates a string of ASCII characters including letters, digits, underscore and hyphen, 23 | /// where the first character is a letter and the last character is a letter or a digit. 24 | fn attribute_name() -> impl Strategy { 25 | proptest::string::string_regex("[a-zA-Z][a-zA-Z0-9_-]*[a-zA-Z0-9]").unwrap() 26 | } 27 | 28 | /// A valid attribute value. 29 | /// 30 | /// Creates a string of extended ASCII characters while excluding control and not 31 | /// starting with, or consisting entirely of whitespace. 32 | fn attribute_value_content() -> impl Strategy { 33 | proptest::string::string_regex(r"[\x20-\x7E\x80-\xFF]+") 34 | .unwrap() 35 | .prop_filter("Cannot start with whitespace", |s| { 36 | !s.starts_with(|c: char| c.is_whitespace()) 37 | }) 38 | .prop_filter("Cannot consist of whitespace only", |s| { 39 | !s.trim().is_empty() 40 | }) 41 | } 42 | 43 | /// An empty attribute value. 44 | /// 45 | /// An attribute value consisting of zero or more spaces. 46 | fn attribute_value_empty() -> impl Strategy { 47 | proptest::string::string_regex(" {0,}").unwrap() 48 | } 49 | 50 | /// The space separator between an attribute name and its value. 51 | fn space_separator() -> impl Strategy { 52 | proptest::string::string_regex(" {0,20}").unwrap() 53 | } 54 | 55 | fn multiline_continuation_prefix() -> impl Strategy { 56 | prop_oneof![Just(" "), Just("\t"), Just("+")].prop_map(|s| s.to_owned()) 57 | } 58 | 59 | /// A test type that represents an attribute value. 60 | /// Since empty values are only coerced to `None` after parsing, the `Option` type is not used here. 61 | #[derive(Clone, Debug)] 62 | enum AttributeValue { 63 | Single(String), 64 | Multi(Vec), 65 | } 66 | 67 | /// An attribute value. Either a single value or a multi-line value where value(s) may be empty. 68 | fn attribute_value() -> impl Strategy { 69 | prop_oneof![ 70 | prop_oneof![attribute_value_empty(), attribute_value_content()] 71 | .prop_map(AttributeValue::Single), 72 | proptest::collection::vec( 73 | prop_oneof![attribute_value_empty(), attribute_value_content()], 74 | 2..20 75 | ) 76 | .prop_map(AttributeValue::Multi) 77 | ] 78 | } 79 | 80 | /// An attribute and its corresponding RPSL representation. 81 | fn attribute_w_rpsl() -> impl Strategy, String)> { 82 | ( 83 | attribute_name(), 84 | space_separator(), 85 | multiline_continuation_prefix(), 86 | attribute_value(), 87 | ) 88 | .prop_map(|(name, space, cont_prefix, value)| { 89 | let attribute = rpsl::Attribute::new( 90 | name.parse().unwrap(), 91 | match &value { 92 | AttributeValue::Single(value) => value.parse().unwrap(), 93 | AttributeValue::Multi(values) => values 94 | .iter() 95 | .map(AsRef::as_ref) 96 | .collect::>() 97 | .try_into() 98 | .unwrap(), 99 | }, 100 | ); 101 | 102 | let mut rpsl = format!( 103 | "{name}:{space}{value}", 104 | value = { 105 | match &value { 106 | AttributeValue::Single(value) => value.to_owned(), 107 | AttributeValue::Multi(values) => values[0].to_owned(), 108 | } 109 | } 110 | ); 111 | if let AttributeValue::Multi(values) = &value { 112 | for value in &values[1..] { 113 | rpsl.push('\n'); 114 | if value.trim().is_empty() { 115 | rpsl.push(char::from_u32(0x002B).unwrap()); // Add a "+", since entirely empty lines are not allowed in multi-line attributes. 116 | } else { 117 | rpsl.push_str(&format!("{}{}{}", cont_prefix, space, value)); 118 | } 119 | } 120 | } 121 | 122 | (attribute, rpsl) 123 | }) 124 | } 125 | 126 | /// An object and its corresponding RPSL representation. 127 | pub fn object_w_rpsl() -> impl Strategy, String)> { 128 | prop::collection::vec(attribute_w_rpsl(), 2..300).prop_flat_map(|attrs_w_rpsl| { 129 | let mut attributes: Vec = Vec::new(); 130 | let mut rpsl = String::new(); 131 | 132 | for (a, r) in attrs_w_rpsl { 133 | attributes.push(a); 134 | rpsl.push_str(&r); 135 | rpsl.push('\n'); 136 | } 137 | rpsl.push('\n'); 138 | 139 | (Just(rpsl::Object::new(attributes)), Just(rpsl)) 140 | }) 141 | } 142 | } 143 | --------------------------------------------------------------------------------