debian-watch-0.2.20/.cargo_vcs_info.json0000644000000001360000000000100134430ustar { "git": { "sha1": "8ca6523f96af344c8fb8a338be1915eea97c2b34" }, "path_in_vcs": "" }debian-watch-0.2.20/.codespellrc000064400000000000000000000000461046102023000145330ustar 00000000000000[codespell] ignore-words-list = crate debian-watch-0.2.20/.github/CODEOWNERS000064400000000000000000000000121046102023000151570ustar 00000000000000* @jelmer debian-watch-0.2.20/.github/FUNDING.yml000064400000000000000000000000171046102023000154060ustar 00000000000000github: jelmer debian-watch-0.2.20/.github/dependabot.yml000064400000000000000000000006251046102023000164260ustar 00000000000000# Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" rebase-strategy: "disabled" - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly debian-watch-0.2.20/.github/workflows/disperse.yml000064400000000000000000000002741046102023000201740ustar 00000000000000--- name: Disperse configuration "on": - push jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: jelmer/action-disperse-validate@v2 debian-watch-0.2.20/.github/workflows/rust.yml000064400000000000000000000022441046102023000173520ustar 00000000000000--- name: Rust "on": push: pull_request: env: CARGO_TERM_COLOR: always jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] fail-fast: false steps: - uses: actions/checkout@v5 - name: Install dependencies (Ubuntu) run: sudo apt install clang llvm pkg-config nettle-dev if: matrix.os == 'ubuntu-latest' - name: Install dependencies (macOS) run: brew install nettle if: matrix.os == 'macos-latest' - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose - name: Run tests with all features run: cargo test --all-features --verbose if: matrix.os != 'windows-latest' - name: Run tests with no default features run: cargo test --no-default-features --verbose minimal-versions: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install cargo-minimal-versions run: cargo install cargo-minimal-versions cargo-hack - name: Test with minimal versions run: cargo minimal-versions test --verbose debian-watch-0.2.20/.gitignore000064400000000000000000000000131046102023000142150ustar 00000000000000target .*~ debian-watch-0.2.20/CODE_OF_CONDUCT.md000064400000000000000000000125451046102023000150410ustar 00000000000000 # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations debian-watch-0.2.20/Cargo.lock0000644000002045600000000000100114250ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "argon2" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", "cpufeatures", "password-hash", ] [[package]] name = "ascii-canvas" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" dependencies = [ "term", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "bindgen" version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ "bitflags", "cexpr", "clang-sys", "itertools 0.13.0", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", ] [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "blake2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ "digest", ] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array 0.14.7", ] [[package]] name = "buffered-reader" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db26bf1f092fd5e05b5ab3be2f290915aeb6f3f20c4e9f86ce0f07f336c2412f" dependencies = [ "libc", ] [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link", ] [[package]] name = "clang-sys" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "countme" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array 0.14.7", "typenum", ] [[package]] name = "cssparser" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" dependencies = [ "cssparser-macros", "dtoa-short", "itoa", "phf", "smallvec", ] [[package]] name = "cssparser-macros" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", "syn", ] [[package]] name = "deb822-lossless" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4b7d08154e599ffe6652ff71969763c70a93e2acc3cd9c274e576028fddd4f1" dependencies = [ "regex", "rowan", "serde", ] [[package]] name = "debian-watch" version = "0.2.20" dependencies = [ "anyhow", "deb822-lossless", "debversion", "m_lexer", "maplit", "regex", "reqwest", "rowan", "scraper", "sequoia-openpgp", "url", ] [[package]] name = "debversion" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4f5cc9ce1d5067bee8060dd75208525dd0133ffea0b2960fef64ab85d58c4c5" dependencies = [ "chrono", "lazy-regex", "num-bigint", ] [[package]] name = "derive_more" version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "dirs-next" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ "cfg-if", "dirs-sys-next", ] [[package]] name = "dirs-sys-next" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", "winapi", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "dtoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" [[package]] name = "dtoa-short" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" dependencies = [ "dtoa", ] [[package]] name = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ego-tree" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "ena" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" dependencies = [ "log", ] [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.61.2", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "futf" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" dependencies = [ "mac", "new_debug_unreachable", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "fxhash" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" dependencies = [ "byteorder", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "generic-array" version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" dependencies = [ "rustversion", "typenum", ] [[package]] name = "getopts" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ "unicode-width", ] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", ] [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "html5ever" version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", "markup5ever", "match_token", ] [[package]] name = "http" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", "itoa", ] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", "http", "http-body", "pin-project-lite", ] [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", "pin-utils", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http", "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-tls" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", "hyper", "hyper-util", "native-tls", "tokio", "tokio-native-tls", "tower-service", ] [[package]] name = "hyper-util" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64", "bytes", "futures-channel", "futures-core", "futures-util", "http", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", "socket2", "system-configuration", "tokio", "tower-service", "tracing", "windows-registry", ] [[package]] name = "iana-time-zone" version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "icu_collections" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", ] [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", ] [[package]] name = "itertools" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lalrpop" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", "bit-set", "ena", "itertools 0.11.0", "lalrpop-util", "petgraph", "regex", "regex-syntax", "string_cache", "term", "tiny-keccak", "unicode-xid", "walkdir", ] [[package]] name = "lalrpop-util" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ "regex-automata", ] [[package]] name = "lazy-regex" version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "191898e17ddee19e60bccb3945aa02339e81edd4a8c50e21fd4d48cdecda7b29" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c35dc8b0da83d1a9507e12122c80dea71a9c7c613014347392483a83ea593e04" dependencies = [ "proc-macro2", "quote", "regex", "syn", ] [[package]] name = "libc" version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libloading" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", "windows-link", ] [[package]] name = "libredox" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", ] [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "m_lexer" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7e51ebf91162d585a5bae05e4779efc4a276171cb880d61dd6fab11c98467a7" dependencies = [ "regex", ] [[package]] name = "mac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" dependencies = [ "log", "phf", "phf_codegen", "string_cache", "string_cache_codegen", "tendril", ] [[package]] name = "match_token" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memsec" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] [[package]] name = "native-tls" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "nettle" version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44e6ff4a94e5d34a1fd5abbd39418074646e2fa51b257198701330f22fcd6936" dependencies = [ "getrandom 0.2.16", "libc", "nettle-sys", "thiserror 1.0.69", "typenum", ] [[package]] name = "nettle-sys" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61a3f5406064d310d59b1a219d3c5c9a49caf4047b6496032e3f930876488c34" dependencies = [ "bindgen", "cc", "libc", "pkg-config", "tempfile", "vcpkg", ] [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-bigint" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link", ] [[package]] name = "password-hash" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core", "subtle", ] [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap", ] [[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", ] [[package]] name = "phf_codegen" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ "phf_generator", "phf_shared", ] [[package]] name = "phf_generator" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", ] [[package]] name = "phf_macros" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", "syn", ] [[package]] name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "potential_utf" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] [[package]] name = "precomputed-hash" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] [[package]] name = "regex" version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" version = "0.12.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" dependencies = [ "base64", "bytes", "encoding_rs", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", "log", "mime", "native-tls", "percent-encoding", "pin-project-lite", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", "tokio-native-tls", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", ] [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rowan" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" dependencies = [ "countme", "hashbrown 0.14.5", "rustc-hash", "text-size", ] [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustls" version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pki-types" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schannel" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scraper" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc3d051b884f40e309de6c149734eab57aa8cc1347992710dc80bcc1c2194c15" dependencies = [ "cssparser", "ego-tree", "getopts", "html5ever", "precomputed-hash", "selectors", "tendril", ] [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "selectors" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" dependencies = [ "bitflags", "cssparser", "derive_more", "fxhash", "log", "new_debug_unreachable", "phf", "phf_codegen", "precomputed-hash", "servo_arc", "smallvec", ] [[package]] name = "sequoia-openpgp" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0e334ce3ec5b9b47d86a80563b3ecec435f59acf37e86058b3b686a42c5a2ba" dependencies = [ "anyhow", "argon2", "base64", "buffered-reader", "chrono", "dyn-clone", "getrandom 0.2.16", "idna", "lalrpop", "lalrpop-util", "libc", "memsec", "nettle", "regex", "regex-syntax", "sha1collisiondetection", "thiserror 2.0.17", "xxhash-rust", ] [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "servo_arc" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" dependencies = [ "stable_deref_trait", ] [[package]] name = "sha1collisiondetection" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f606421e4a6012877e893c399822a4ed4b089164c5969424e1b9d1e66e6964b" dependencies = [ "digest", "generic-array 1.3.5", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", "windows-sys 0.60.2", ] [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "string_cache" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", "phf_shared", "precomputed-hash", "serde", ] [[package]] name = "string_cache_codegen" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", ] [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "tempfile" version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", ] [[package]] name = "tendril" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" dependencies = [ "futf", "mac", "utf-8", ] [[package]] name = "term" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" dependencies = [ "dirs-next", "rustversion", "winapi", ] [[package]] name = "text-size" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl 2.0.17", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", ] [[package]] name = "tinystr" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tokio" version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "socket2", "windows-sys 0.61.2", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-util" version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tower-layer", "tower-service", ] [[package]] name = "tower-http" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.5", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", "windows_i686_gnullvm 0.53.1", "windows_i686_msvc 0.53.1", "windows_x86_64_gnu 0.53.1", "windows_x86_64_gnullvm 0.53.1", "windows_x86_64_msvc 0.53.1", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xxhash-rust" version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] debian-watch-0.2.20/Cargo.toml0000644000000033060000000000100114430ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "debian-watch" version = "0.2.20" authors = ["Jelmer Vernooij "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "parser for Debian watch files" homepage = "https://github.com/jelmer/debian-watch-rs" readme = "README.md" license = "Apache-2.0" repository = "https://github.com/jelmer/debian-watch-rs.git" [features] blocking = ["reqwest?/blocking"] deb822 = ["dep:deb822-lossless"] default = [] discover = [ "scraper", "reqwest", ] pgp = [ "sequoia-openpgp", "anyhow", ] [lib] name = "debian_watch" path = "src/lib.rs" [dependencies.anyhow] version = "1.0" optional = true [dependencies.deb822-lossless] version = "0.5" optional = true [dependencies.debversion] version = ">=0.4.7, <0.6" [dependencies.m_lexer] version = "0.0.4" [dependencies.regex] version = "1" [dependencies.reqwest] version = "0.12" optional = true [dependencies.rowan] version = "0.16.1" [dependencies.scraper] version = "0.22" optional = true [dependencies.sequoia-openpgp] version = "2.0" features = ["crypto-nettle"] optional = true default-features = false [dependencies.url] version = "2.5.7" [dev-dependencies.maplit] version = "1.0.2" debian-watch-0.2.20/Cargo.toml.orig000064400000000000000000000016241046102023000151250ustar 00000000000000[package] name = "debian-watch" version = "0.2.20" authors = [ "Jelmer Vernooij ",] edition = "2021" license = "Apache-2.0" description = "parser for Debian watch files" repository = "https://github.com/jelmer/debian-watch-rs.git" homepage = "https://github.com/jelmer/debian-watch-rs" [features] default = [] discover = ["scraper", "reqwest"] blocking = ["reqwest?/blocking"] pgp = ["sequoia-openpgp", "anyhow"] deb822 = ["dep:deb822-lossless"] [dependencies] rowan = "0.16.1" m_lexer = "0.0.4" debversion = ">=0.4.7, <0.6" deb822-lossless = { version = "0.5", optional = true } url = "2.5.7" regex = "1" scraper = { version = "0.22", optional = true } reqwest = { version = "0.12", optional = true } sequoia-openpgp = { version = "2.0", optional = true, default-features = false, features = ["crypto-nettle"] } anyhow = { version = "1.0", optional = true } [dev-dependencies] maplit = "1.0.2" debian-watch-0.2.20/README.md000064400000000000000000000021061046102023000135110ustar 00000000000000Format-preserving parser and editor for Debian watch files ========================================================== This crate supports reading, editing and writing Debian watch files, while preserving the original contents byte-for-byte. Example: ```rust let wf = debian_watch::WatchFile::new(None); assert_eq!(wf.version(), debian_watch::DEFAULT_VERSION); assert_eq!("", wf.to_string()); let wf = debian_watch::WatchFile::new(Some(4)); assert_eq!(wf.version(), 4); assert_eq!("version=4\n", wf.to_string()); let wf: debian_watch::WatchFile = r#"version=4 opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz "#.parse().unwrap(); assert_eq!(wf.version(), 4); assert_eq!(wf.entries().collect::>().len(), 1); let entry = wf.entries().next().unwrap(); assert_eq!(entry.opts(), maplit::hashmap! { "foo".to_string() => "blah".to_string(), }); assert_eq!(&entry.url(), "https://foo.com/bar"); assert_eq!(entry.matching_pattern().as_deref(), Some(".*/v?(\\d\\S+)\\.tar\\.gz")); ``` It also supports partial parsing (with some error nodes), which could be useful for e.g. IDEs. debian-watch-0.2.20/TODO.md000064400000000000000000000004271046102023000133250ustar 00000000000000Options to properly support: * dversionmangle * oversionmangle * dirversionmangle * filenamemangle * pagemangle * downloadurlmangle * repack * repacksuffix * compression * mode * pretty * versionmode * component * ctype Add common trait for legacy and deb822-style watchfiles. debian-watch-0.2.20/disperse.toml000064400000000000000000000000531046102023000147440ustar 00000000000000tag-name = "v$VERSION" release-timeout = 5 debian-watch-0.2.20/src/deb822.rs000064400000000000000000000316421046102023000143640ustar 00000000000000//! Watch file implementation for format 5 (RFC822/deb822 style) use deb822_lossless::{Deb822, Paragraph}; use std::str::FromStr; #[derive(Debug)] /// Parse error for watch file parsing pub struct ParseError(String); impl std::error::Error for ParseError {} impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "ParseError: {}", self.0) } } /// A watch file in format 5 (RFC822/deb822 style) #[derive(Debug)] pub struct WatchFileV5(Deb822); /// An entry in a format 5 watch file pub struct EntryV5 { paragraph: Paragraph, defaults: Option, } impl WatchFileV5 { /// Create a new empty format 5 watch file pub fn new() -> Self { // Create a minimal format 5 watch file from a string let content = "Version: 5\n"; WatchFileV5::from_str(content).expect("Failed to create empty watch file") } /// Returns the version of the watch file (always 5 for this type) pub fn version(&self) -> u32 { 5 } /// Returns the defaults paragraph if it exists. /// The defaults paragraph is the second paragraph (after Version) if it has no Source field. pub fn defaults(&self) -> Option { let paragraphs: Vec<_> = self.0.paragraphs().collect(); if paragraphs.len() > 1 { // Check if second paragraph looks like defaults (no Source field) if !paragraphs[1].contains_key("Source") && !paragraphs[1].contains_key("source") { return Some(paragraphs[1].clone()); } } None } /// Returns an iterator over all entries in the watch file. /// The first paragraph contains defaults, subsequent paragraphs are entries. pub fn entries(&self) -> impl Iterator + '_ { let paragraphs: Vec<_> = self.0.paragraphs().collect(); let defaults = self.defaults(); // Skip the first paragraph (version) // The second paragraph (if it exists and has specific fields) contains defaults // Otherwise all paragraphs are entries let start_index = if paragraphs.len() > 1 { // Check if second paragraph looks like defaults (no Source field) if !paragraphs[1].contains_key("Source") && !paragraphs[1].contains_key("source") { 2 // Skip version and defaults } else { 1 // Skip only version } } else { 1 }; paragraphs .into_iter() .skip(start_index) .map(move |p| EntryV5 { paragraph: p, defaults: defaults.clone(), }) } /// Get the underlying Deb822 object pub fn inner(&self) -> &Deb822 { &self.0 } } impl Default for WatchFileV5 { fn default() -> Self { Self::new() } } impl FromStr for WatchFileV5 { type Err = ParseError; fn from_str(s: &str) -> Result { match Deb822::from_str(s) { Ok(deb822) => { // Verify it's version 5 let version = deb822 .paragraphs() .next() .and_then(|p| p.get("Version")) .unwrap_or_else(|| "1".to_string()); if version != "5" { return Err(ParseError(format!("Expected version 5, got {}", version))); } Ok(WatchFileV5(deb822)) } Err(e) => Err(ParseError(e.to_string())), } } } impl std::fmt::Display for WatchFileV5 { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.0) } } impl EntryV5 { /// Get a field value from the entry, with fallback to defaults paragraph. /// First checks the entry's own fields, then falls back to the defaults paragraph if present. pub(crate) fn get_field(&self, key: &str) -> Option { // Try the key as-is first in the entry if let Some(value) = self.paragraph.get(key) { return Some(value); } // If not found, try with different case variations in the entry // deb822-lossless is case-preserving, so we need to check all field names let normalized_key = normalize_key(key); // Iterate through all keys in the paragraph and check for normalized match for (k, v) in self.paragraph.items() { if normalize_key(&k) == normalized_key { return Some(v); } } // If not found in entry, check the defaults paragraph if let Some(ref defaults) = self.defaults { // Try the key as-is first in defaults if let Some(value) = defaults.get(key) { return Some(value); } // Try with case variations in defaults for (k, v) in defaults.items() { if normalize_key(&k) == normalized_key { return Some(v); } } } None } /// Returns the source URL pub fn source(&self) -> Option { self.get_field("Source") } /// Returns the matching pattern pub fn matching_pattern(&self) -> Option { self.get_field("Matching-Pattern") } /// Get the underlying paragraph pub fn as_deb822(&self) -> &Paragraph { &self.paragraph } /// Name of the component, if specified pub fn component(&self) -> Option { self.get_field("Component") } /// Get the an option value from the entry, with fallback to defaults paragraph. pub fn get_option(&self, key: &str) -> Option { match key { "Source" => None, // Source is not an option "Matching-Pattern" => None, // Matching-Pattern is not an option "Component" => None, // Component is not an option "Version" => None, // Version is not an option key => self.get_field(key), } } /// Set an option value in the entry pub fn set_option(&mut self, key: &str, value: &str) { self.paragraph.insert(key, value); } /// Delete an option from the entry pub fn delete_option(&mut self, key: &str) { self.paragraph.remove(key); } } /// Normalize a field key according to RFC822 rules: /// - Convert to lowercase /// - Hyphens and underscores are treated as equivalent fn normalize_key(key: &str) -> String { key.to_lowercase().replace(['-', '_'], "") } #[cfg(test)] mod tests { use super::*; #[test] fn test_create_v5_watchfile() { let wf = WatchFileV5::new(); assert_eq!(wf.version(), 5); let output = wf.to_string(); assert!(output.contains("Version")); assert!(output.contains("5")); } #[test] fn test_parse_v5_basic() { let input = r#"Version: 5 Source: https://github.com/owner/repo/tags Matching-Pattern: .*/v?(\d\S+)\.tar\.gz "#; let wf: WatchFileV5 = input.parse().unwrap(); assert_eq!(wf.version(), 5); let entries: Vec<_> = wf.entries().collect(); assert_eq!(entries.len(), 1); let entry = &entries[0]; assert_eq!( entry.source().as_deref(), Some("https://github.com/owner/repo/tags") ); assert_eq!( entry.matching_pattern(), Some(".*/v?(\\d\\S+)\\.tar\\.gz".to_string()) ); } #[test] fn test_parse_v5_multiple_entries() { let input = r#"Version: 5 Source: https://github.com/owner/repo1/tags Matching-Pattern: .*/v?(\d\S+)\.tar\.gz Source: https://github.com/owner/repo2/tags Matching-Pattern: .*/release-(\d\S+)\.tar\.gz "#; let wf: WatchFileV5 = input.parse().unwrap(); let entries: Vec<_> = wf.entries().collect(); assert_eq!(entries.len(), 2); assert_eq!( entries[0].source().as_deref(), Some("https://github.com/owner/repo1/tags") ); assert_eq!( entries[1].source().as_deref(), Some("https://github.com/owner/repo2/tags") ); } #[test] fn test_v5_case_insensitive_fields() { let input = r#"Version: 5 source: https://example.com/files matching-pattern: .*\.tar\.gz "#; let wf: WatchFileV5 = input.parse().unwrap(); let entries: Vec<_> = wf.entries().collect(); assert_eq!(entries.len(), 1); let entry = &entries[0]; assert_eq!(entry.source().as_deref(), Some("https://example.com/files")); assert_eq!(entry.matching_pattern().as_deref(), Some(".*\\.tar\\.gz")); } #[test] fn test_v5_with_compression_option() { let input = r#"Version: 5 Source: https://example.com/files Matching-Pattern: .*\.tar\.gz Compression: xz "#; let wf: WatchFileV5 = input.parse().unwrap(); let entries: Vec<_> = wf.entries().collect(); assert_eq!(entries.len(), 1); let entry = &entries[0]; let compression = entry.get_option("compression"); assert!(compression.is_some()); } #[test] fn test_v5_with_component() { let input = r#"Version: 5 Source: https://example.com/files Matching-Pattern: .*\.tar\.gz Component: foo "#; let wf: WatchFileV5 = input.parse().unwrap(); let entries: Vec<_> = wf.entries().collect(); assert_eq!(entries.len(), 1); let entry = &entries[0]; assert_eq!(entry.component(), Some("foo".to_string())); } #[test] fn test_v5_rejects_wrong_version() { let input = r#"Version: 4 Source: https://example.com/files Matching-Pattern: .*\.tar\.gz "#; let result: Result = input.parse(); assert!(result.is_err()); } #[test] fn test_v5_roundtrip() { let input = r#"Version: 5 Source: https://example.com/files Matching-Pattern: .*\.tar\.gz "#; let wf: WatchFileV5 = input.parse().unwrap(); let output = wf.to_string(); // The output should be parseable again let wf2: WatchFileV5 = output.parse().unwrap(); assert_eq!(wf2.version(), 5); let entries: Vec<_> = wf2.entries().collect(); assert_eq!(entries.len(), 1); } #[test] fn test_normalize_key() { assert_eq!(normalize_key("Matching-Pattern"), "matchingpattern"); assert_eq!(normalize_key("matching_pattern"), "matchingpattern"); assert_eq!(normalize_key("MatchingPattern"), "matchingpattern"); assert_eq!(normalize_key("MATCHING-PATTERN"), "matchingpattern"); } #[test] fn test_defaults_paragraph() { let input = r#"Version: 5 Compression: xz User-Agent: Custom/1.0 Source: https://example.com/repo1 Matching-Pattern: .*\.tar\.gz Source: https://example.com/repo2 Matching-Pattern: .*\.tar\.gz Compression: gz "#; let wf: WatchFileV5 = input.parse().unwrap(); // Check that defaults paragraph is detected let defaults = wf.defaults(); assert!(defaults.is_some()); let defaults = defaults.unwrap(); assert_eq!(defaults.get("Compression"), Some("xz".to_string())); assert_eq!(defaults.get("User-Agent"), Some("Custom/1.0".to_string())); // Check that entries inherit from defaults let entries: Vec<_> = wf.entries().collect(); assert_eq!(entries.len(), 2); // First entry should inherit Compression and User-Agent from defaults assert_eq!(entries[0].get_option("Compression"), Some("xz".to_string())); assert_eq!( entries[0].get_option("User-Agent"), Some("Custom/1.0".to_string()) ); // Second entry overrides Compression but inherits User-Agent assert_eq!(entries[1].get_option("Compression"), Some("gz".to_string())); assert_eq!( entries[1].get_option("User-Agent"), Some("Custom/1.0".to_string()) ); } #[test] fn test_no_defaults_paragraph() { let input = r#"Version: 5 Source: https://example.com/repo1 Matching-Pattern: .*\.tar\.gz "#; let wf: WatchFileV5 = input.parse().unwrap(); // Check that there's no defaults paragraph (first paragraph has Source) assert!(wf.defaults().is_none()); let entries: Vec<_> = wf.entries().collect(); assert_eq!(entries.len(), 1); } #[test] fn test_defaults_with_case_variations() { let input = r#"Version: 5 compression: xz user-agent: Custom/1.0 Source: https://example.com/repo1 Matching-Pattern: .*\.tar\.gz "#; let wf: WatchFileV5 = input.parse().unwrap(); // Check that defaults work with different case let entries: Vec<_> = wf.entries().collect(); assert_eq!(entries.len(), 1); // Should find defaults even with different case assert_eq!(entries[0].get_option("Compression"), Some("xz".to_string())); assert_eq!( entries[0].get_option("User-Agent"), Some("Custom/1.0".to_string()) ); } } debian-watch-0.2.20/src/lex.rs000064400000000000000000000100501046102023000141540ustar 00000000000000use crate::SyntaxKind; use crate::SyntaxKind::*; /// Split the input string into a flat list of tokens pub(crate) fn lex(text: &str) -> Vec<(SyntaxKind, String)> { fn tok(t: SyntaxKind) -> m_lexer::TokenKind { let sk = rowan::SyntaxKind::from(t); m_lexer::TokenKind(sk.0) } fn kind(t: m_lexer::TokenKind) -> SyntaxKind { match t.0 { 0 => KEY, 1 => VALUE, 2 => EQUALS, 3 => QUOTE, 4 => COMMA, 5 => CONTINUATION, 6 => NEWLINE, 7 => WHITESPACE, 8 => COMMENT, 9 => ERROR, _ => unreachable!(), } } let lexer = m_lexer::LexerBuilder::new() .error_token(tok(ERROR)) .tokens(&[ (tok(KEY), r"[a-z]+"), (tok(QUOTE), "\""), (tok(VALUE), r#"[^\s=,"]*[^\s=\\,"]"#), (tok(CONTINUATION), r"\\\n"), (tok(EQUALS), r"="), (tok(COMMA), r","), (tok(NEWLINE), r"\n"), (tok(WHITESPACE), r"\s+"), (tok(COMMENT), r"#[^\n]*"), ]) .build(); lexer .tokenize(text) .into_iter() .map(|t| (t.len, kind(t.kind))) .scan(0usize, |start_offset, (len, kind)| { let s: String = text[*start_offset..*start_offset + len].into(); *start_offset += len; Some((kind, s)) }) .collect() } #[cfg(test)] mod tests { use crate::SyntaxKind::*; #[test] fn test_empty() { assert_eq!(super::lex(""), vec![]); } #[test] fn test_simple() { assert_eq!( super::lex( r#"version=4 opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \ https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz "# ), vec![ (KEY, "version".into()), (EQUALS, "=".into()), (VALUE, "4".into()), (NEWLINE, "\n".into()), (KEY, "opts".into()), (EQUALS, "=".into()), (KEY, "bare".into()), (COMMA, ",".into()), (KEY, "filenamemangle".into()), (EQUALS, "=".into()), ( VALUE, "s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into() ), (WHITESPACE, " ".into()), (CONTINUATION, "\\\n".into()), (WHITESPACE, " ".into()), ( VALUE, "https://github.com/syncthing/syncthing-gtk/tags".into() ), (WHITESPACE, " ".into()), (VALUE, ".*/v?(\\d\\S+)\\.tar\\.gz".into()), (NEWLINE, "\n".into()), ] ); } #[test] fn test_quoted() { assert_eq!( super::lex( r#"version=4 opts="bare, filenamemangle=foo" \ https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz "# ), vec![ (KEY, "version".into()), (EQUALS, "=".into()), (VALUE, "4".into()), (NEWLINE, "\n".into()), (KEY, "opts".into()), (EQUALS, "=".into()), (QUOTE, "\"".into()), (KEY, "bare".into()), (COMMA, ",".into()), (WHITESPACE, " ".into()), (KEY, "filenamemangle".into()), (EQUALS, "=".into()), (KEY, "foo".into()), (QUOTE, "\"".into()), (WHITESPACE, " ".into()), (CONTINUATION, "\\\n".into()), (WHITESPACE, " ".into()), ( VALUE, "https://github.com/syncthing/syncthing-gtk/tags".into() ), (WHITESPACE, " ".into()), (VALUE, ".*/v?(\\d\\S+)\\.tar\\.gz".into()), (NEWLINE, "\n".into()), ] ); } } debian-watch-0.2.20/src/lib.rs000064400000000000000000000101471046102023000141410ustar 00000000000000#![deny(missing_docs)] //! Formatting-preserving parser and editor for Debian watch files //! //! # Example //! //! ```rust //! let wf = debian_watch::WatchFile::new(None); //! assert_eq!(wf.version(), debian_watch::DEFAULT_VERSION); //! assert_eq!("", wf.to_string()); //! //! let wf = debian_watch::WatchFile::new(Some(4)); //! assert_eq!(wf.version(), 4); //! assert_eq!("version=4\n", wf.to_string()); //! //! let wf: debian_watch::WatchFile = r#"version=4 //! opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz //! "#.parse().unwrap(); //! assert_eq!(wf.version(), 4); //! assert_eq!(wf.entries().collect::>().len(), 1); //! let entry = wf.entries().next().unwrap(); //! assert_eq!(entry.opts(), maplit::hashmap! { //! "foo".to_string() => "blah".to_string(), //! }); //! assert_eq!(&entry.url(), "https://foo.com/bar"); //! assert_eq!(entry.matching_pattern().as_deref(), Some(".*/v?(\\d\\S+)\\.tar\\.gz")); //! ``` mod lex; mod parse; #[cfg(feature = "deb822")] pub mod deb822; pub mod mangle; #[cfg(feature = "pgp")] pub mod pgp; pub mod release; pub mod search; /// Any watch files without a version are assumed to be /// version 1. pub const DEFAULT_VERSION: u32 = 1; /// Default user agent string used for HTTP requests pub const DEFAULT_USER_AGENT: &str = concat!("debian-watch-rs/", env!("CARGO_PKG_VERSION")); mod types; pub use release::Release; pub use types::*; /// Let's start with defining all kinds of tokens and /// composite nodes. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[allow(non_camel_case_types, missing_docs, clippy::upper_case_acronyms)] #[repr(u16)] pub(crate) enum SyntaxKind { KEY = 0, VALUE, EQUALS, QUOTE, COMMA, CONTINUATION, NEWLINE, WHITESPACE, // whitespaces is explicit COMMENT, // comments ERROR, // as well as errors // composite nodes ROOT, // The entire file VERSION, // "version=x\n" ENTRY, // "opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz\n" OPTS_LIST, // "opts=foo=blah" OPTION, // "foo=blah" OPTION_SEPARATOR, // "," (comma separator between options) URL, // "https://foo.com/bar" MATCHING_PATTERN, // ".*/v?(\d\S+)\.tar\.gz" VERSION_POLICY, // "debian" SCRIPT, // "uupdate" } /// Convert our `SyntaxKind` into the rowan `SyntaxKind`. impl From for rowan::SyntaxKind { fn from(kind: SyntaxKind) -> Self { Self(kind as u16) } } pub use crate::parse::Entry; pub use crate::parse::EntryBuilder; pub use crate::parse::ParseError; pub use crate::parse::WatchFile; #[cfg(test)] mod tests { #[test] fn test_create_watchfile() { let wf = super::WatchFile::new(None); assert_eq!(wf.version(), super::DEFAULT_VERSION); assert_eq!("", wf.to_string()); let wf = super::WatchFile::new(Some(4)); assert_eq!(wf.version(), 4); assert_eq!("version=4\n", wf.to_string()); } #[test] fn test_set_version() { let mut wf = super::WatchFile::new(Some(4)); assert_eq!(wf.version(), 4); wf.set_version(5); assert_eq!(wf.version(), 5); assert_eq!("version=5\n", wf.to_string()); // Test setting version on a file without version let mut wf = super::WatchFile::new(None); assert_eq!(wf.version(), super::DEFAULT_VERSION); wf.set_version(4); assert_eq!(wf.version(), 4); assert_eq!("version=4\n", wf.to_string()); } #[test] fn test_set_version_on_parsed() { // Test that parsed WatchFiles can be mutated let mut wf: super::WatchFile = "version=4\n".parse().unwrap(); assert_eq!(wf.version(), 4); wf.set_version(5); assert_eq!(wf.version(), 5); assert_eq!("version=5\n", wf.to_string()); // Test setting version on a parsed file without version let mut wf: super::WatchFile = "".parse().unwrap(); assert_eq!(wf.version(), super::DEFAULT_VERSION); wf.set_version(4); assert_eq!(wf.version(), 4); assert_eq!("version=4\n", wf.to_string()); } } debian-watch-0.2.20/src/mangle.rs000064400000000000000000000263601046102023000146420ustar 00000000000000//! Functions for parsing and applying version and URL mangling expressions. //! //! Debian watch files use sed-style expressions for transforming versions and URLs. use regex::Regex; /// Error type for mangling expression parsing #[derive(Debug, Clone, PartialEq, Eq)] pub enum MangleError { /// Not a substitution or translation expression NotMangleExpr(String), /// Invalid substitution expression InvalidSubstExpr(String), /// Invalid translation expression InvalidTranslExpr(String), /// Regex compilation error RegexError(String), } impl std::fmt::Display for MangleError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { MangleError::NotMangleExpr(s) => { write!(f, "not a substitution or translation expression: {}", s) } MangleError::InvalidSubstExpr(s) => write!(f, "invalid substitution expression: {}", s), MangleError::InvalidTranslExpr(s) => write!(f, "invalid translation expression: {}", s), MangleError::RegexError(s) => write!(f, "regex error: {}", s), } } } impl std::error::Error for MangleError {} /// Type of mangling expression #[derive(Debug, Clone, PartialEq, Eq)] pub enum MangleExprKind { /// Substitution (s/pattern/replacement/flags) Subst, /// Translation (tr/pattern/replacement/flags or y/pattern/replacement/flags) Transl, } /// A parsed mangling expression #[derive(Debug, Clone, PartialEq, Eq)] pub struct MangleExpr { /// The kind of expression pub kind: MangleExprKind, /// The pattern to match pub pattern: String, /// The replacement string pub replacement: String, /// Optional flags pub flags: Option, } /// Parse a mangling expression /// /// # Examples /// /// ``` /// use debian_watch::mangle::parse_mangle_expr; /// /// let expr = parse_mangle_expr("s/foo/bar/g").unwrap(); /// assert_eq!(expr.pattern, "foo"); /// assert_eq!(expr.replacement, "bar"); /// assert_eq!(expr.flags.as_deref(), Some("g")); /// ``` pub fn parse_mangle_expr(vm: &str) -> Result { if vm.starts_with('s') { parse_subst_expr(vm) } else if vm.starts_with("tr") { parse_transl_expr(vm) } else if vm.starts_with('y') { parse_transl_expr(vm) } else { Err(MangleError::NotMangleExpr(vm.to_string())) } } /// Parse a substitution expression (s/pattern/replacement/flags) /// /// # Examples /// /// ``` /// use debian_watch::mangle::parse_subst_expr; /// /// let expr = parse_subst_expr("s/foo/bar/g").unwrap(); /// assert_eq!(expr.pattern, "foo"); /// assert_eq!(expr.replacement, "bar"); /// assert_eq!(expr.flags.as_deref(), Some("g")); /// /// let expr = parse_subst_expr("s|foo|bar|").unwrap(); /// assert_eq!(expr.pattern, "foo"); /// assert_eq!(expr.replacement, "bar"); /// ``` pub fn parse_subst_expr(vm: &str) -> Result { if !vm.starts_with('s') { return Err(MangleError::InvalidSubstExpr( "not a substitution expression".to_string(), )); } if vm.len() < 2 { return Err(MangleError::InvalidSubstExpr( "expression too short".to_string(), )); } let delimiter = vm.chars().nth(1).unwrap(); let rest = &vm[2..]; // Split by unescaped delimiter let parts = split_by_unescaped_delimiter(rest, delimiter); if parts.len() < 2 { return Err(MangleError::InvalidSubstExpr( "not enough parts".to_string(), )); } let pattern = parts[0].clone(); let replacement = parts[1].clone(); let flags = if parts.len() > 2 && !parts[2].is_empty() { Some(parts[2].clone()) } else { None }; Ok(MangleExpr { kind: MangleExprKind::Subst, pattern, replacement, flags, }) } /// Parse a translation expression (tr/pattern/replacement/flags or y/pattern/replacement/flags) /// /// # Examples /// /// ``` /// use debian_watch::mangle::parse_transl_expr; /// /// let expr = parse_transl_expr("tr/a-z/A-Z/").unwrap(); /// assert_eq!(expr.pattern, "a-z"); /// assert_eq!(expr.replacement, "A-Z"); /// ``` pub fn parse_transl_expr(vm: &str) -> Result { let rest = if vm.starts_with("tr") { &vm[2..] } else if vm.starts_with('y') { &vm[1..] } else { return Err(MangleError::InvalidTranslExpr( "not a translation expression".to_string(), )); }; if rest.is_empty() { return Err(MangleError::InvalidTranslExpr( "expression too short".to_string(), )); } let delimiter = rest.chars().next().unwrap(); let rest = &rest[1..]; // Split by unescaped delimiter let parts = split_by_unescaped_delimiter(rest, delimiter); if parts.len() < 2 { return Err(MangleError::InvalidTranslExpr( "not enough parts".to_string(), )); } let pattern = parts[0].clone(); let replacement = parts[1].clone(); let flags = if parts.len() > 2 && !parts[2].is_empty() { Some(parts[2].clone()) } else { None }; Ok(MangleExpr { kind: MangleExprKind::Transl, pattern, replacement, flags, }) } /// Split a string by an unescaped delimiter fn split_by_unescaped_delimiter(s: &str, delimiter: char) -> Vec { let mut parts = Vec::new(); let mut current = String::new(); let mut escaped = false; for c in s.chars() { if escaped { current.push(c); escaped = false; } else if c == '\\' { current.push(c); escaped = true; } else if c == delimiter { parts.push(current.clone()); current.clear(); } else { current.push(c); } } // Don't forget the last part parts.push(current); parts } /// Apply a mangling expression to a string /// /// # Examples /// /// ``` /// use debian_watch::mangle::apply_mangle; /// /// let result = apply_mangle("s/foo/bar/", "foo baz foo").unwrap(); /// assert_eq!(result, "bar baz foo"); /// /// let result = apply_mangle("s/foo/bar/g", "foo baz foo").unwrap(); /// assert_eq!(result, "bar baz bar"); /// ``` pub fn apply_mangle(vm: &str, orig: &str) -> Result { let expr = parse_mangle_expr(vm)?; match expr.kind { MangleExprKind::Subst => { let re = Regex::new(&expr.pattern).map_err(|e| MangleError::RegexError(e.to_string()))?; // Check if 'g' flag is present for global replacement let global = expr.flags.as_ref().map_or(false, |f| f.contains('g')); if global { Ok(re.replace_all(orig, expr.replacement.as_str()).to_string()) } else { Ok(re.replace(orig, expr.replacement.as_str()).to_string()) } } MangleExprKind::Transl => { // Translation: character-by-character replacement apply_translation(&expr.pattern, &expr.replacement, orig) } } } /// Apply character-by-character translation fn apply_translation(pattern: &str, replacement: &str, orig: &str) -> Result { // Expand ranges like a-z let from_chars = expand_char_range(pattern); let to_chars = expand_char_range(replacement); if from_chars.len() != to_chars.len() { return Err(MangleError::InvalidTranslExpr( "pattern and replacement must have same length".to_string(), )); } let mut result = String::new(); for c in orig.chars() { if let Some(pos) = from_chars.iter().position(|&fc| fc == c) { result.push(to_chars[pos]); } else { result.push(c); } } Ok(result) } /// Expand character ranges like a-z to actual characters fn expand_char_range(s: &str) -> Vec { let mut result = Vec::new(); let chars: Vec = s.chars().collect(); let mut i = 0; while i < chars.len() { if i + 2 < chars.len() && chars[i + 1] == '-' { // Range found let start = chars[i]; let end = chars[i + 2]; for c in (start as u32)..=(end as u32) { if let Some(ch) = char::from_u32(c) { result.push(ch); } } i += 3; } else { result.push(chars[i]); i += 1; } } result } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_subst_expr() { let expr = parse_subst_expr("s/foo/bar/g").unwrap(); assert_eq!(expr.pattern, "foo"); assert_eq!(expr.replacement, "bar"); assert_eq!(expr.flags.as_deref(), Some("g")); let expr = parse_subst_expr("s|foo|bar|").unwrap(); assert_eq!(expr.pattern, "foo"); assert_eq!(expr.replacement, "bar"); assert_eq!(expr.flags, None); let expr = parse_subst_expr("s#a/b#c/d#").unwrap(); assert_eq!(expr.pattern, "a/b"); assert_eq!(expr.replacement, "c/d"); } #[test] fn test_parse_transl_expr() { let expr = parse_transl_expr("tr/a-z/A-Z/").unwrap(); assert_eq!(expr.pattern, "a-z"); assert_eq!(expr.replacement, "A-Z"); let expr = parse_transl_expr("y/abc/xyz/").unwrap(); assert_eq!(expr.pattern, "abc"); assert_eq!(expr.replacement, "xyz"); } #[test] fn test_apply_mangle_subst() { let result = apply_mangle("s/foo/bar/", "foo baz foo").unwrap(); assert_eq!(result, "bar baz foo"); let result = apply_mangle("s/foo/bar/g", "foo baz foo").unwrap(); assert_eq!(result, "bar baz bar"); // Test with regex let result = apply_mangle("s/[0-9]+/X/g", "a1b2c3").unwrap(); assert_eq!(result, "aXbXcX"); } #[test] fn test_apply_mangle_transl() { let result = apply_mangle("tr/a-z/A-Z/", "hello").unwrap(); assert_eq!(result, "HELLO"); let result = apply_mangle("y/abc/xyz/", "aabbcc").unwrap(); assert_eq!(result, "xxyyzz"); } #[test] fn test_expand_char_range() { let result = expand_char_range("a-z"); assert_eq!(result.len(), 26); assert_eq!(result[0], 'a'); assert_eq!(result[25], 'z'); let result = expand_char_range("a-c"); assert_eq!(result, vec!['a', 'b', 'c']); let result = expand_char_range("abc"); assert_eq!(result, vec!['a', 'b', 'c']); } #[test] fn test_split_by_unescaped_delimiter() { let result = split_by_unescaped_delimiter("foo/bar/baz", '/'); assert_eq!(result, vec!["foo", "bar", "baz"]); let result = split_by_unescaped_delimiter("foo\\/bar/baz", '/'); assert_eq!(result, vec!["foo\\/bar", "baz"]); } #[test] fn test_real_world_examples() { // Example from Python code: dversionmangle=s/\+ds// let result = apply_mangle(r"s/\+ds//", "1.0+ds").unwrap(); assert_eq!(result, "1.0"); // Example: filenamemangle let result = apply_mangle( r"s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1.tar.gz/", "https://github.com/syncthing/syncthing-gtk/archive/v0.9.4.tar.gz", ) .unwrap(); assert_eq!(result, "syncthing-gtk-0.9.4.tar.gz"); } } debian-watch-0.2.20/src/parse.rs000064400000000000000000003413571046102023000145170ustar 00000000000000use crate::lex::lex; use crate::types::*; use crate::SyntaxKind; use crate::SyntaxKind::*; use crate::DEFAULT_VERSION; use std::io::Read; use std::str::FromStr; #[derive(Debug, Clone, PartialEq, Eq, Hash)] /// Error type for parse errors pub struct ParseError(Vec); impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { for err in &self.0 { writeln!(f, "{}", err)?; } Ok(()) } } impl std::error::Error for ParseError {} /// Second, implementing the `Language` trait teaches rowan to convert between /// these two SyntaxKind types, allowing for a nicer SyntaxNode API where /// "kinds" are values from our `enum SyntaxKind`, instead of plain u16 values. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] enum Lang {} impl rowan::Language for Lang { type Kind = SyntaxKind; fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind { unsafe { std::mem::transmute::(raw.0) } } fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind { kind.into() } } /// GreenNode is an immutable tree, which is cheap to change, /// but doesn't contain offsets and parent pointers. use rowan::GreenNode; /// You can construct GreenNodes by hand, but a builder /// is helpful for top-down parsers: it maintains a stack /// of currently in-progress nodes use rowan::GreenNodeBuilder; /// The parse results are stored as a "green tree". /// We'll discuss working with the results later struct Parse { green_node: GreenNode, #[allow(unused)] errors: Vec, #[allow(unused)] version: i32, } fn parse(text: &str) -> Parse { struct Parser { /// input tokens, including whitespace, /// in *reverse* order. tokens: Vec<(SyntaxKind, String)>, /// the in-progress tree. builder: GreenNodeBuilder<'static>, /// the list of syntax errors we've accumulated /// so far. errors: Vec, } impl Parser { fn parse_version(&mut self) -> Option { let mut version = None; if self.tokens.last() == Some(&(KEY, "version".to_string())) { self.builder.start_node(VERSION.into()); self.bump(); self.skip_ws(); if self.current() != Some(EQUALS) { self.builder.start_node(ERROR.into()); self.errors.push("expected `=`".to_string()); self.bump(); self.builder.finish_node(); } else { self.bump(); } if self.current() != Some(VALUE) { self.builder.start_node(ERROR.into()); self.errors .push(format!("expected value, got {:?}", self.current())); self.bump(); self.builder.finish_node(); } else { let version_str = self.tokens.last().unwrap().1.clone(); match version_str.parse() { Ok(v) => { version = Some(v); self.bump(); } Err(_) => { self.builder.start_node(ERROR.into()); self.errors .push(format!("invalid version: {}", version_str)); self.bump(); self.builder.finish_node(); } } } if self.current() != Some(NEWLINE) { self.builder.start_node(ERROR.into()); self.errors.push("expected newline".to_string()); self.bump(); self.builder.finish_node(); } else { self.bump(); } self.builder.finish_node(); } version } fn parse_watch_entry(&mut self) -> bool { self.skip_ws(); if self.current().is_none() { return false; } if self.current() == Some(NEWLINE) { self.bump(); return false; } self.builder.start_node(ENTRY.into()); self.parse_options_list(); for i in 0..4 { if self.current() == Some(NEWLINE) { break; } if self.current() == Some(CONTINUATION) { self.bump(); self.skip_ws(); continue; } if self.current() != Some(VALUE) && self.current() != Some(KEY) { self.builder.start_node(ERROR.into()); self.errors.push(format!( "expected value, got {:?} (i={})", self.current(), i )); if self.current().is_some() { self.bump(); } self.builder.finish_node(); } else { // Wrap each field in its appropriate node match i { 0 => { // URL self.builder.start_node(URL.into()); self.bump(); self.builder.finish_node(); } 1 => { // Matching pattern self.builder.start_node(MATCHING_PATTERN.into()); self.bump(); self.builder.finish_node(); } 2 => { // Version policy self.builder.start_node(VERSION_POLICY.into()); self.bump(); self.builder.finish_node(); } 3 => { // Script self.builder.start_node(SCRIPT.into()); self.bump(); self.builder.finish_node(); } _ => { self.bump(); } } } self.skip_ws(); } if self.current() != Some(NEWLINE) && self.current().is_some() { self.builder.start_node(ERROR.into()); self.errors .push(format!("expected newline, not {:?}", self.current())); if self.current().is_some() { self.bump(); } self.builder.finish_node(); } else { self.bump(); } self.builder.finish_node(); true } fn parse_option(&mut self) -> bool { if self.current().is_none() { return false; } while self.current() == Some(CONTINUATION) { self.bump(); } if self.current() == Some(WHITESPACE) { return false; } self.builder.start_node(OPTION.into()); if self.current() != Some(KEY) { self.builder.start_node(ERROR.into()); self.errors.push("expected key".to_string()); self.bump(); self.builder.finish_node(); } else { self.bump(); } if self.current() == Some(EQUALS) { self.bump(); if self.current() != Some(VALUE) && self.current() != Some(KEY) { self.builder.start_node(ERROR.into()); self.errors .push(format!("expected value, got {:?}", self.current())); self.bump(); self.builder.finish_node(); } else { self.bump(); } } else if self.current() == Some(COMMA) { } else { self.builder.start_node(ERROR.into()); self.errors.push("expected `=`".to_string()); if self.current().is_some() { self.bump(); } self.builder.finish_node(); } self.builder.finish_node(); true } fn parse_options_list(&mut self) { self.skip_ws(); if self.tokens.last() == Some(&(KEY, "opts".to_string())) || self.tokens.last() == Some(&(KEY, "options".to_string())) { self.builder.start_node(OPTS_LIST.into()); self.bump(); self.skip_ws(); if self.current() != Some(EQUALS) { self.builder.start_node(ERROR.into()); self.errors.push("expected `=`".to_string()); if self.current().is_some() { self.bump(); } self.builder.finish_node(); } else { self.bump(); } let quoted = if self.current() == Some(QUOTE) { self.bump(); true } else { false }; loop { if quoted { if self.current() == Some(QUOTE) { self.bump(); break; } self.skip_ws(); } if !self.parse_option() { break; } if self.current() == Some(COMMA) { self.builder.start_node(OPTION_SEPARATOR.into()); self.bump(); self.builder.finish_node(); } else if !quoted { break; } } self.builder.finish_node(); self.skip_ws(); } } fn parse(mut self) -> Parse { let mut version = 1; // Make sure that the root node covers all source self.builder.start_node(ROOT.into()); if let Some(v) = self.parse_version() { version = v; } // TODO: use version to influence parsing loop { if !self.parse_watch_entry() { break; } } // Don't forget to eat *trailing* whitespace self.skip_ws(); // Close the root node. self.builder.finish_node(); // Turn the builder into a GreenNode Parse { green_node: self.builder.finish(), errors: self.errors, version, } } /// Advance one token, adding it to the current branch of the tree builder. fn bump(&mut self) { let (kind, text) = self.tokens.pop().unwrap(); self.builder.token(kind.into(), text.as_str()); } /// Peek at the first unprocessed token fn current(&self) -> Option { self.tokens.last().map(|(kind, _)| *kind) } fn skip_ws(&mut self) { while self.current() == Some(WHITESPACE) || self.current() == Some(CONTINUATION) || self.current() == Some(COMMENT) { self.bump() } } } let mut tokens = lex(text); tokens.reverse(); Parser { tokens, builder: GreenNodeBuilder::new(), errors: Vec::new(), } .parse() } /// To work with the parse results we need a view into the /// green tree - the Syntax tree. /// It is also immutable, like a GreenNode, /// but it contains parent pointers, offsets, and /// has identity semantics. type SyntaxNode = rowan::SyntaxNode; #[allow(unused)] type SyntaxToken = rowan::SyntaxToken; #[allow(unused)] type SyntaxElement = rowan::NodeOrToken; impl Parse { fn syntax(&self) -> SyntaxNode { SyntaxNode::new_root_mut(self.green_node.clone()) } fn root(&self) -> WatchFile { WatchFile::cast(self.syntax()).unwrap() } } /// Calculate line and column (both 0-indexed) for the given offset in the tree. /// Column is measured in bytes from the start of the line. fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) { let root = node.ancestors().last().unwrap_or_else(|| node.clone()); let mut line = 0; let mut last_newline_offset = rowan::TextSize::from(0); for element in root.preorder_with_tokens() { if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element { if token.text_range().start() >= offset { break; } // Count newlines and track position of last one for (idx, _) in token.text().match_indices('\n') { line += 1; last_newline_offset = token.text_range().start() + rowan::TextSize::from((idx + 1) as u32); } } } let column: usize = (offset - last_newline_offset).into(); (line, column) } macro_rules! ast_node { ($ast:ident, $kind:ident) => { #[derive(Clone, PartialEq, Eq, Hash)] #[repr(transparent)] /// A node in the syntax tree for $ast pub struct $ast(SyntaxNode); impl $ast { #[allow(unused)] fn cast(node: SyntaxNode) -> Option { if node.kind() == $kind { Some(Self(node)) } else { None } } /// Get the line number (0-indexed) where this node starts. pub fn line(&self) -> usize { line_col_at_offset(&self.0, self.0.text_range().start()).0 } /// Get the column number (0-indexed, in bytes) where this node starts. pub fn column(&self) -> usize { line_col_at_offset(&self.0, self.0.text_range().start()).1 } /// Get both line and column (0-indexed) where this node starts. /// Returns (line, column) where column is measured in bytes from the start of the line. pub fn line_col(&self) -> (usize, usize) { line_col_at_offset(&self.0, self.0.text_range().start()) } } impl ToString for $ast { fn to_string(&self) -> String { self.0.text().to_string() } } }; } ast_node!(WatchFile, ROOT); ast_node!(Version, VERSION); ast_node!(Entry, ENTRY); ast_node!(OptionList, OPTS_LIST); ast_node!(_Option, OPTION); ast_node!(Url, URL); ast_node!(MatchingPattern, MATCHING_PATTERN); ast_node!(VersionPolicyNode, VERSION_POLICY); ast_node!(ScriptNode, SCRIPT); impl WatchFile { /// Create a new watch file with specified version pub fn new(version: Option) -> WatchFile { let mut builder = GreenNodeBuilder::new(); builder.start_node(ROOT.into()); if let Some(version) = version { builder.start_node(VERSION.into()); builder.token(KEY.into(), "version"); builder.token(EQUALS.into(), "="); builder.token(VALUE.into(), version.to_string().as_str()); builder.token(NEWLINE.into(), "\n"); builder.finish_node(); } builder.finish_node(); WatchFile(SyntaxNode::new_root_mut(builder.finish())) } /// Returns the version AST node of the watch file. pub fn version_node(&self) -> Option { self.0.children().find_map(Version::cast) } /// Returns the version of the watch file. pub fn version(&self) -> u32 { self.version_node() .map(|it| it.version()) .unwrap_or(DEFAULT_VERSION) } /// Returns an iterator over all entries in the watch file. pub fn entries(&self) -> impl Iterator + '_ { self.0.children().filter_map(Entry::cast) } /// Set the version of the watch file. pub fn set_version(&mut self, new_version: u32) { // Build the new version node let mut builder = GreenNodeBuilder::new(); builder.start_node(VERSION.into()); builder.token(KEY.into(), "version"); builder.token(EQUALS.into(), "="); builder.token(VALUE.into(), new_version.to_string().as_str()); builder.token(NEWLINE.into(), "\n"); builder.finish_node(); let new_version_green = builder.finish(); // Create a syntax node (splice_children will detach and reattach it) let new_version_node = SyntaxNode::new_root_mut(new_version_green); // Find existing version node if any let version_pos = self.0.children().position(|child| child.kind() == VERSION); if let Some(pos) = version_pos { // Replace existing version node self.0 .splice_children(pos..pos + 1, vec![new_version_node.into()]); } else { // Insert version node at the beginning self.0.splice_children(0..0, vec![new_version_node.into()]); } } /// Discover releases for all entries in the watch file (async version) /// /// Fetches URLs and searches for version matches for all entries. /// Requires the 'discover' feature. /// /// # Examples /// /// ```ignore /// # use debian_watch::WatchFile; /// # async fn example() { /// let wf: WatchFile = r#"version=4 /// https://example.com/releases/ .*/v?(\d+\.\d+)\.tar\.gz /// "#.parse().unwrap(); /// let all_releases = wf.uscan(|| "mypackage".to_string()).await.unwrap(); /// for (entry_idx, releases) in all_releases.iter().enumerate() { /// println!("Entry {}: {} releases found", entry_idx, releases.len()); /// } /// # } /// ``` #[cfg(feature = "discover")] pub async fn uscan( &self, package: impl Fn() -> String, ) -> Result>, Box> { let mut all_releases = Vec::new(); for entry in self.entries() { let releases = entry.discover(|| package()).await?; all_releases.push(releases); } Ok(all_releases) } /// Discover releases for all entries in the watch file (blocking version) /// /// Fetches URLs and searches for version matches for all entries. /// Requires both 'discover' and 'blocking' features. /// /// # Examples /// /// ```ignore /// # use debian_watch::WatchFile; /// let wf: WatchFile = r#"version=4 /// https://example.com/releases/ .*/v?(\d+\.\d+)\.tar\.gz /// "#.parse().unwrap(); /// let all_releases = wf.uscan_blocking(|| "mypackage".to_string()).unwrap(); /// for (entry_idx, releases) in all_releases.iter().enumerate() { /// println!("Entry {}: {} releases found", entry_idx, releases.len()); /// } /// ``` #[cfg(all(feature = "discover", feature = "blocking"))] pub fn uscan_blocking( &self, package: impl Fn() -> String, ) -> Result>, Box> { let mut all_releases = Vec::new(); for entry in self.entries() { let releases = entry.discover_blocking(|| package())?; all_releases.push(releases); } Ok(all_releases) } /// Add an entry to the watch file. /// /// Appends a new entry to the end of the watch file. /// /// # Examples /// /// ``` /// use debian_watch::{WatchFile, EntryBuilder}; /// /// let mut wf = WatchFile::new(Some(4)); /// /// // Add an entry using EntryBuilder /// let entry = EntryBuilder::new("https://github.com/example/tags") /// .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz") /// .build(); /// wf.add_entry(entry); /// /// // Or use the builder pattern directly /// wf.add_entry( /// EntryBuilder::new("https://example.com/releases") /// .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz") /// .opt("compression", "xz") /// .version_policy("debian") /// .build() /// ); /// ``` pub fn add_entry(&mut self, entry: Entry) { // Find the position to insert (after the last entry or after version) let insert_pos = self.0.children_with_tokens().count(); // Detach the entry node from its current parent and get its green node let entry_green = entry.0.green().into_owned(); let new_entry_node = SyntaxNode::new_root_mut(entry_green); // Insert the entry at the end self.0 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]); } /// Read a watch file from a Read object. pub fn from_reader(reader: R) -> Result { let mut buf_reader = std::io::BufReader::new(reader); let mut content = String::new(); buf_reader .read_to_string(&mut content) .map_err(|e| ParseError(vec![e.to_string()]))?; content.parse() } /// Read a watch file from a Read object, allowing syntax errors. pub fn from_reader_relaxed(mut r: R) -> Result { let mut content = String::new(); r.read_to_string(&mut content)?; let parsed = parse(&content); Ok(parsed.root()) } /// Parse a debian watch file from a string, allowing syntax errors. pub fn from_str_relaxed(s: &str) -> Self { let parsed = parse(s); parsed.root() } } impl FromStr for WatchFile { type Err = ParseError; fn from_str(s: &str) -> Result { let parsed = parse(s); if parsed.errors.is_empty() { Ok(parsed.root()) } else { Err(ParseError(parsed.errors)) } } } impl Version { /// Returns the version of the watch file. pub fn version(&self) -> u32 { self.0 .children_with_tokens() .find_map(|it| match it { SyntaxElement::Token(token) => { if token.kind() == VALUE { Some(token.text().parse().unwrap()) } else { None } } _ => None, }) .unwrap_or(DEFAULT_VERSION) } } /// Builder for creating new watchfile entries. /// /// Provides a fluent API for constructing entries with various components. /// /// # Examples /// /// ``` /// use debian_watch::EntryBuilder; /// /// // Minimal entry with just URL and pattern /// let entry = EntryBuilder::new("https://github.com/example/tags") /// .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz") /// .build(); /// /// // Entry with options /// let entry = EntryBuilder::new("https://github.com/example/tags") /// .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz") /// .opt("compression", "xz") /// .flag("repack") /// .version_policy("debian") /// .script("uupdate") /// .build(); /// ``` #[derive(Debug, Clone, Default)] pub struct EntryBuilder { url: Option, matching_pattern: Option, version_policy: Option, script: Option, opts: std::collections::HashMap, } impl EntryBuilder { /// Create a new entry builder with the specified URL. pub fn new(url: impl Into) -> Self { EntryBuilder { url: Some(url.into()), matching_pattern: None, version_policy: None, script: None, opts: std::collections::HashMap::new(), } } /// Set the matching pattern for the entry. pub fn matching_pattern(mut self, pattern: impl Into) -> Self { self.matching_pattern = Some(pattern.into()); self } /// Set the version policy for the entry. pub fn version_policy(mut self, policy: impl Into) -> Self { self.version_policy = Some(policy.into()); self } /// Set the script for the entry. pub fn script(mut self, script: impl Into) -> Self { self.script = Some(script.into()); self } /// Add an option to the entry. pub fn opt(mut self, key: impl Into, value: impl Into) -> Self { self.opts.insert(key.into(), value.into()); self } /// Add a boolean flag option to the entry. /// /// Boolean options like "repack", "bare", "decompress" don't have values. pub fn flag(mut self, key: impl Into) -> Self { self.opts.insert(key.into(), String::new()); self } /// Build the entry. /// /// # Panics /// /// Panics if no URL was provided. pub fn build(self) -> Entry { let url = self.url.expect("URL is required for entry"); let mut builder = GreenNodeBuilder::new(); builder.start_node(ENTRY.into()); // Add options list if provided if !self.opts.is_empty() { builder.start_node(OPTS_LIST.into()); builder.token(KEY.into(), "opts"); builder.token(EQUALS.into(), "="); let mut first = true; for (key, value) in self.opts.iter() { if !first { builder.token(COMMA.into(), ","); } first = false; builder.start_node(OPTION.into()); builder.token(KEY.into(), key); if !value.is_empty() { builder.token(EQUALS.into(), "="); builder.token(VALUE.into(), value); } builder.finish_node(); } builder.finish_node(); builder.token(WHITESPACE.into(), " "); } // Add URL (required) builder.start_node(URL.into()); builder.token(VALUE.into(), &url); builder.finish_node(); // Add matching pattern if provided if let Some(pattern) = self.matching_pattern { builder.token(WHITESPACE.into(), " "); builder.start_node(MATCHING_PATTERN.into()); builder.token(VALUE.into(), &pattern); builder.finish_node(); } // Add version policy if provided if let Some(policy) = self.version_policy { builder.token(WHITESPACE.into(), " "); builder.start_node(VERSION_POLICY.into()); builder.token(VALUE.into(), &policy); builder.finish_node(); } // Add script if provided if let Some(script_val) = self.script { builder.token(WHITESPACE.into(), " "); builder.start_node(SCRIPT.into()); builder.token(VALUE.into(), &script_val); builder.finish_node(); } builder.token(NEWLINE.into(), "\n"); builder.finish_node(); Entry(SyntaxNode::new_root_mut(builder.finish())) } } impl Entry { /// Create a new entry builder. /// /// This is a convenience method that returns an `EntryBuilder`. /// /// # Examples /// /// ``` /// use debian_watch::Entry; /// /// let entry = Entry::builder("https://github.com/example/tags") /// .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz") /// .build(); /// ``` pub fn builder(url: impl Into) -> EntryBuilder { EntryBuilder::new(url) } /// List of options pub fn option_list(&self) -> Option { self.0.children().find_map(OptionList::cast) } /// Get the value of an option pub fn get_option(&self, key: &str) -> Option { self.option_list().and_then(|ol| ol.get_option(key)) } /// Check if an option is set pub fn has_option(&self, key: &str) -> bool { self.option_list().map_or(false, |ol| ol.has_option(key)) } /// The name of the secondary source tarball pub fn component(&self) -> Option { self.get_option("component") } /// Component type pub fn ctype(&self) -> Result, ()> { self.get_option("ctype").map(|s| s.parse()).transpose() } /// Compression method pub fn compression(&self) -> Result, ()> { self.get_option("compression") .map(|s| s.parse()) .transpose() } /// Repack the tarball pub fn repack(&self) -> bool { self.has_option("repack") } /// Repack suffix pub fn repacksuffix(&self) -> Option { self.get_option("repacksuffix") } /// Retrieve the mode of the watch file entry. pub fn mode(&self) -> Result { Ok(self .get_option("mode") .map(|s| s.parse()) .transpose()? .unwrap_or_default()) } /// Return the git pretty mode pub fn pretty(&self) -> Result { Ok(self .get_option("pretty") .map(|s| s.parse()) .transpose()? .unwrap_or_default()) } /// Set the date string used by the pretty option to an arbitrary format as an optional /// opts argument when the matching-pattern is HEAD or heads/branch for git mode. pub fn date(&self) -> String { self.get_option("date") .unwrap_or_else(|| "%Y%m%d".to_string()) } /// Return the git export mode pub fn gitexport(&self) -> Result { Ok(self .get_option("gitexport") .map(|s| s.parse()) .transpose()? .unwrap_or_default()) } /// Return the git mode pub fn gitmode(&self) -> Result { Ok(self .get_option("gitmode") .map(|s| s.parse()) .transpose()? .unwrap_or_default()) } /// Return the pgp mode pub fn pgpmode(&self) -> Result { Ok(self .get_option("pgpmode") .map(|s| s.parse()) .transpose()? .unwrap_or_default()) } /// Return the search mode pub fn searchmode(&self) -> Result { Ok(self .get_option("searchmode") .map(|s| s.parse()) .transpose()? .unwrap_or_default()) } /// Return the decompression mode pub fn decompress(&self) -> bool { self.has_option("decompress") } /// Whether to disable all site specific special case code such as URL director uses and page /// content alterations. pub fn bare(&self) -> bool { self.has_option("bare") } /// Set the user-agent string used to contact the HTTP(S) server as user-agent-string. (persistent) pub fn user_agent(&self) -> Option { self.get_option("user-agent") } /// Use PASV mode for the FTP connection. pub fn passive(&self) -> Option { if self.has_option("passive") || self.has_option("pasv") { Some(true) } else if self.has_option("active") || self.has_option("nopasv") { Some(false) } else { None } } /// Add the extra options to use with the unzip command, such as -a, -aa, and -b, when executed /// by mk-origtargz. pub fn unzipoptions(&self) -> Option { self.get_option("unzipopt") } /// Normalize the downloaded web page string. pub fn dversionmangle(&self) -> Option { self.get_option("dversionmangle") .or_else(|| self.get_option("versionmangle")) } /// Normalize the directory path string matching the regex in a set of parentheses of /// http://URL as the sortable version index string. This is used /// as the directory path sorting index only. pub fn dirversionmangle(&self) -> Option { self.get_option("dirversionmangle") } /// Normalize the downloaded web page string. pub fn pagemangle(&self) -> Option { self.get_option("pagemangle") } /// Normalize the candidate upstream version strings extracted from hrefs in the /// source of the web page. This is used as the version sorting index when selecting the /// latest upstream version. pub fn uversionmangle(&self) -> Option { self.get_option("uversionmangle") .or_else(|| self.get_option("versionmangle")) } /// Syntactic shorthand for uversionmangle=rules, dversionmangle=rules pub fn versionmangle(&self) -> Option { self.get_option("versionmangle") } /// Convert the selected upstream tarball href string from the percent-encoded hexadecimal /// string to the decoded normal URL string for obfuscated /// web sites. Only percent-encoding is available and it is decoded with /// s/%([A-Fa-f\d]{2})/chr hex $1/eg. pub fn hrefdecode(&self) -> bool { self.get_option("hrefdecode").is_some() } /// Convert the selected upstream tarball href string into the accessible URL for obfuscated /// web sites. This is run after hrefdecode. pub fn downloadurlmangle(&self) -> Option { self.get_option("downloadurlmangle") } /// Generate the upstream tarball filename from the selected href string if matching-pattern /// can extract the latest upstream version from the selected href string. /// Otherwise, generate the upstream tarball filename from its full URL string and set the /// missing from the generated upstream tarball filename. /// /// Without this option, the default upstream tarball filename is generated by taking the last /// component of the URL and removing everything after any '?' or '#'. pub fn filenamemangle(&self) -> Option { self.get_option("filenamemangle") } /// Generate the candidate upstream signature file URL string from the upstream tarball URL. pub fn pgpsigurlmangle(&self) -> Option { self.get_option("pgpsigurlmangle") } /// Generate the version string of the source tarball _.orig.tar.gz /// from . This should be used to add a suffix such as +dfsg to a MUT package. pub fn oversionmangle(&self) -> Option { self.get_option("oversionmangle") } /// Apply uversionmangle to a version string /// /// # Examples /// /// ``` /// # use debian_watch::WatchFile; /// let wf: WatchFile = r#"version=4 /// opts=uversionmangle=s/\+ds// https://example.com/ .* /// "#.parse().unwrap(); /// let entry = wf.entries().next().unwrap(); /// assert_eq!(entry.apply_uversionmangle("1.0+ds").unwrap(), "1.0"); /// ``` pub fn apply_uversionmangle( &self, version: &str, ) -> Result { if let Some(vm) = self.uversionmangle() { crate::mangle::apply_mangle(&vm, version) } else { Ok(version.to_string()) } } /// Apply dversionmangle to a version string /// /// # Examples /// /// ``` /// # use debian_watch::WatchFile; /// let wf: WatchFile = r#"version=4 /// opts=dversionmangle=s/\+dfsg$// https://example.com/ .* /// "#.parse().unwrap(); /// let entry = wf.entries().next().unwrap(); /// assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0"); /// ``` pub fn apply_dversionmangle( &self, version: &str, ) -> Result { if let Some(vm) = self.dversionmangle() { crate::mangle::apply_mangle(&vm, version) } else { Ok(version.to_string()) } } /// Apply oversionmangle to a version string /// /// # Examples /// /// ``` /// # use debian_watch::WatchFile; /// let wf: WatchFile = r#"version=4 /// opts=oversionmangle=s/$/-1/ https://example.com/ .* /// "#.parse().unwrap(); /// let entry = wf.entries().next().unwrap(); /// assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1"); /// ``` pub fn apply_oversionmangle( &self, version: &str, ) -> Result { if let Some(vm) = self.oversionmangle() { crate::mangle::apply_mangle(&vm, version) } else { Ok(version.to_string()) } } /// Apply dirversionmangle to a directory path string /// /// # Examples /// /// ``` /// # use debian_watch::WatchFile; /// let wf: WatchFile = r#"version=4 /// opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .* /// "#.parse().unwrap(); /// let entry = wf.entries().next().unwrap(); /// assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0"); /// ``` pub fn apply_dirversionmangle( &self, version: &str, ) -> Result { if let Some(vm) = self.dirversionmangle() { crate::mangle::apply_mangle(&vm, version) } else { Ok(version.to_string()) } } /// Apply filenamemangle to a URL or filename string /// /// # Examples /// /// ``` /// # use debian_watch::WatchFile; /// let wf: WatchFile = r#"version=4 /// opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .* /// "#.parse().unwrap(); /// let entry = wf.entries().next().unwrap(); /// assert_eq!( /// entry.apply_filenamemangle("https://example.com/v1.0.tar.gz").unwrap(), /// "mypackage-1.0.tar.gz" /// ); /// ``` pub fn apply_filenamemangle(&self, url: &str) -> Result { if let Some(vm) = self.filenamemangle() { crate::mangle::apply_mangle(&vm, url) } else { Ok(url.to_string()) } } /// Apply pagemangle to page content bytes /// /// # Examples /// /// ``` /// # use debian_watch::WatchFile; /// let wf: WatchFile = r#"version=4 /// opts=pagemangle=s/&/&/g https://example.com/ .* /// "#.parse().unwrap(); /// let entry = wf.entries().next().unwrap(); /// assert_eq!( /// entry.apply_pagemangle(b"foo & bar").unwrap(), /// b"foo & bar" /// ); /// ``` pub fn apply_pagemangle(&self, page: &[u8]) -> Result, crate::mangle::MangleError> { if let Some(vm) = self.pagemangle() { let page_str = String::from_utf8_lossy(page); let mangled = crate::mangle::apply_mangle(&vm, &page_str)?; Ok(mangled.into_bytes()) } else { Ok(page.to_vec()) } } /// Apply downloadurlmangle to a URL string /// /// # Examples /// /// ``` /// # use debian_watch::WatchFile; /// let wf: WatchFile = r#"version=4 /// opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .* /// "#.parse().unwrap(); /// let entry = wf.entries().next().unwrap(); /// assert_eq!( /// entry.apply_downloadurlmangle("https://example.com/archive/file.tar.gz").unwrap(), /// "https://example.com/download/file.tar.gz" /// ); /// ``` pub fn apply_downloadurlmangle(&self, url: &str) -> Result { if let Some(vm) = self.downloadurlmangle() { crate::mangle::apply_mangle(&vm, url) } else { Ok(url.to_string()) } } /// Discover releases for this entry (async version) /// /// Fetches the URL and searches for version matches. /// Requires the 'discover' feature. /// /// # Examples /// /// ```ignore /// # use debian_watch::WatchFile; /// # async fn example() { /// let wf: WatchFile = r#"version=4 /// https://example.com/releases/ .*/v?(\d+\.\d+)\.tar\.gz /// "#.parse().unwrap(); /// let entry = wf.entries().next().unwrap(); /// let releases = entry.discover(|| "mypackage".to_string()).await.unwrap(); /// for release in releases { /// println!("{}: {}", release.version, release.url); /// } /// # } /// ``` #[cfg(feature = "discover")] pub async fn discover( &self, package: impl FnOnce() -> String, ) -> Result, Box> { let url = self.format_url(package); let user_agent = self .user_agent() .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string()); let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html); let client = reqwest::Client::builder().user_agent(user_agent).build()?; let response = client.get(url.as_str()).send().await?; let body = response.bytes().await?; // Apply pagemangle if present let mangled_body = self.apply_pagemangle(&body)?; let matching_pattern = self .matching_pattern() .ok_or("matching_pattern is required")?; let package_name = String::new(); // Not used in search currently let results = crate::search::search( match searchmode { crate::SearchMode::Html => "html", crate::SearchMode::Plain => "plain", }, std::io::Cursor::new(mangled_body.as_ref() as &[u8]), &subst(&matching_pattern, || package_name.clone()), &package_name, url.as_str(), )?; let mut releases = Vec::new(); for (version, full_url) in results { // Apply uversionmangle let mangled_version = self.apply_uversionmangle(&version)?; // Apply downloadurlmangle let mangled_url = self.apply_downloadurlmangle(&full_url)?; // Apply pgpsigurlmangle if present let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() { Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?) } else { None }; // Apply filenamemangle if present let target_filename = if self.filenamemangle().is_some() { Some(self.apply_filenamemangle(&mangled_url)?) } else { None }; // Apply oversionmangle if present let package_version = if self.oversionmangle().is_some() { Some(self.apply_oversionmangle(&mangled_version)?) } else { None }; releases.push(crate::Release::new_full( mangled_version, mangled_url, pgpsigurl, target_filename, package_version, )); } Ok(releases) } /// Discover releases for this entry (blocking version) /// /// Fetches the URL and searches for version matches. /// Requires both 'discover' and 'blocking' features. /// /// # Examples /// /// ```ignore /// # use debian_watch::WatchFile; /// let wf: WatchFile = r#"version=4 /// https://example.com/releases/ .*/v?(\d+\.\d+)\.tar\.gz /// "#.parse().unwrap(); /// let entry = wf.entries().next().unwrap(); /// let releases = entry.discover_blocking(|| "mypackage".to_string()).unwrap(); /// for release in releases { /// println!("{}: {}", release.version, release.url); /// } /// ``` #[cfg(all(feature = "discover", feature = "blocking"))] pub fn discover_blocking( &self, package: impl FnOnce() -> String, ) -> Result, Box> { let url = self.format_url(package); let user_agent = self .user_agent() .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string()); let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html); let client = reqwest::blocking::Client::builder() .user_agent(user_agent) .build()?; let response = client.get(url.as_str()).send()?; let body = response.bytes()?; // Apply pagemangle if present let mangled_body = self.apply_pagemangle(&body)?; let matching_pattern = self .matching_pattern() .ok_or("matching_pattern is required")?; let package_name = String::new(); // Not used in search currently let results = crate::search::search( match searchmode { crate::SearchMode::Html => "html", crate::SearchMode::Plain => "plain", }, std::io::Cursor::new(mangled_body.as_ref() as &[u8]), &subst(&matching_pattern, || package_name.clone()), &package_name, url.as_str(), )?; let mut releases = Vec::new(); for (version, full_url) in results { // Apply uversionmangle let mangled_version = self.apply_uversionmangle(&version)?; // Apply downloadurlmangle let mangled_url = self.apply_downloadurlmangle(&full_url)?; // Apply pgpsigurlmangle if present let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() { Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?) } else { None }; // Apply filenamemangle if present let target_filename = if self.filenamemangle().is_some() { Some(self.apply_filenamemangle(&mangled_url)?) } else { None }; // Apply oversionmangle if present let package_version = if self.oversionmangle().is_some() { Some(self.apply_oversionmangle(&mangled_version)?) } else { None }; releases.push(crate::Release::new_full( mangled_version, mangled_url, pgpsigurl, target_filename, package_version, )); } Ok(releases) } /// Returns options set pub fn opts(&self) -> std::collections::HashMap { let mut options = std::collections::HashMap::new(); if let Some(ol) = self.option_list() { for opt in ol.options() { let key = opt.key(); let value = opt.value(); if let (Some(key), Some(value)) = (key, value) { options.insert(key.to_string(), value.to_string()); } } } options } fn items(&self) -> impl Iterator + '_ { self.0.children_with_tokens().filter_map(|it| match it { SyntaxElement::Token(token) => { if token.kind() == VALUE || token.kind() == KEY { Some(token.text().to_string()) } else { None } } SyntaxElement::Node(node) => { // Extract values from entry field nodes match node.kind() { URL => Url::cast(node).map(|n| n.url()), MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()), VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()), SCRIPT => ScriptNode::cast(node).map(|n| n.script()), _ => None, } } }) } /// Returns the URL AST node of the entry. pub fn url_node(&self) -> Option { self.0.children().find_map(Url::cast) } /// Returns the URL of the entry. pub fn url(&self) -> String { self.url_node().map(|it| it.url()).unwrap_or_else(|| { // Fallback for entries without URL node (shouldn't happen with new parser) self.items().next().unwrap() }) } /// Returns the matching pattern AST node of the entry. pub fn matching_pattern_node(&self) -> Option { self.0.children().find_map(MatchingPattern::cast) } /// Returns the matching pattern of the entry. pub fn matching_pattern(&self) -> Option { self.matching_pattern_node() .map(|it| it.pattern()) .or_else(|| { // Fallback for entries without MATCHING_PATTERN node self.items().nth(1) }) } /// Returns the version policy AST node of the entry. pub fn version_node(&self) -> Option { self.0.children().find_map(VersionPolicyNode::cast) } /// Returns the version policy pub fn version(&self) -> Result, String> { self.version_node() .map(|it| it.policy().parse()) .transpose() .or_else(|_e| { // Fallback for entries without VERSION_POLICY node self.items().nth(2).map(|it| it.parse()).transpose() }) } /// Returns the script AST node of the entry. pub fn script_node(&self) -> Option { self.0.children().find_map(ScriptNode::cast) } /// Returns the script of the entry. pub fn script(&self) -> Option { self.script_node().map(|it| it.script()).or_else(|| { // Fallback for entries without SCRIPT node self.items().nth(3) }) } /// Replace all substitutions and return the resulting URL. pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url { subst(self.url().as_str(), package).parse().unwrap() } /// Set the URL of the entry. pub fn set_url(&mut self, new_url: &str) { // Build the new URL node let mut builder = GreenNodeBuilder::new(); builder.start_node(URL.into()); builder.token(VALUE.into(), new_url); builder.finish_node(); let new_url_green = builder.finish(); // Create a syntax node (splice_children will detach and reattach it) let new_url_node = SyntaxNode::new_root_mut(new_url_green); // Find existing URL node position (need to use children_with_tokens for correct indexing) let url_pos = self .0 .children_with_tokens() .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL)); if let Some(pos) = url_pos { // Replace existing URL node self.0 .splice_children(pos..pos + 1, vec![new_url_node.into()]); } } /// Set the matching pattern of the entry. /// /// TODO: This currently only replaces an existing matching pattern. /// If the entry doesn't have a matching pattern, this method does nothing. /// Future implementation should insert the node at the correct position. pub fn set_matching_pattern(&mut self, new_pattern: &str) { // Build the new MATCHING_PATTERN node let mut builder = GreenNodeBuilder::new(); builder.start_node(MATCHING_PATTERN.into()); builder.token(VALUE.into(), new_pattern); builder.finish_node(); let new_pattern_green = builder.finish(); // Create a syntax node (splice_children will detach and reattach it) let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green); // Find existing MATCHING_PATTERN node position let pattern_pos = self.0.children_with_tokens().position( |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN), ); if let Some(pos) = pattern_pos { // Replace existing MATCHING_PATTERN node self.0 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]); } // TODO: else insert new node after URL } /// Set the version policy of the entry. /// /// TODO: This currently only replaces an existing version policy. /// If the entry doesn't have a version policy, this method does nothing. /// Future implementation should insert the node at the correct position. pub fn set_version_policy(&mut self, new_policy: &str) { // Build the new VERSION_POLICY node let mut builder = GreenNodeBuilder::new(); builder.start_node(VERSION_POLICY.into()); // Version policy can be KEY (e.g., "debian") or VALUE builder.token(VALUE.into(), new_policy); builder.finish_node(); let new_policy_green = builder.finish(); // Create a syntax node (splice_children will detach and reattach it) let new_policy_node = SyntaxNode::new_root_mut(new_policy_green); // Find existing VERSION_POLICY node position let policy_pos = self.0.children_with_tokens().position( |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY), ); if let Some(pos) = policy_pos { // Replace existing VERSION_POLICY node self.0 .splice_children(pos..pos + 1, vec![new_policy_node.into()]); } // TODO: else insert new node after MATCHING_PATTERN (or URL if no pattern) } /// Set the script of the entry. /// /// TODO: This currently only replaces an existing script. /// If the entry doesn't have a script, this method does nothing. /// Future implementation should insert the node at the correct position. pub fn set_script(&mut self, new_script: &str) { // Build the new SCRIPT node let mut builder = GreenNodeBuilder::new(); builder.start_node(SCRIPT.into()); // Script can be KEY (e.g., "uupdate") or VALUE builder.token(VALUE.into(), new_script); builder.finish_node(); let new_script_green = builder.finish(); // Create a syntax node (splice_children will detach and reattach it) let new_script_node = SyntaxNode::new_root_mut(new_script_green); // Find existing SCRIPT node position let script_pos = self .0 .children_with_tokens() .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT)); if let Some(pos) = script_pos { // Replace existing SCRIPT node self.0 .splice_children(pos..pos + 1, vec![new_script_node.into()]); } // TODO: else insert new node after VERSION_POLICY (or MATCHING_PATTERN/URL if no policy) } /// Set or update an option value. /// /// If the option already exists, it will be updated with the new value. /// If the option doesn't exist, it will be added to the options list. /// If there's no options list, one will be created. pub fn set_opt(&mut self, key: &str, value: &str) { // Find the OPTS_LIST position in Entry let opts_pos = self.0.children_with_tokens().position( |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST), ); if let Some(_opts_idx) = opts_pos { if let Some(mut ol) = self.option_list() { // Find if the option already exists if let Some(mut opt) = ol.find_option(key) { // Update the existing option's value opt.set_value(value); // Mutations should propagate automatically - no need to replace } else { // Add new option ol.add_option(key, value); // Mutations should propagate automatically - no need to replace } } } else { // Create a new options list let mut builder = GreenNodeBuilder::new(); builder.start_node(OPTS_LIST.into()); builder.token(KEY.into(), "opts"); builder.token(EQUALS.into(), "="); builder.start_node(OPTION.into()); builder.token(KEY.into(), key); builder.token(EQUALS.into(), "="); builder.token(VALUE.into(), value); builder.finish_node(); builder.finish_node(); let new_opts_green = builder.finish(); let new_opts_node = SyntaxNode::new_root_mut(new_opts_green); // Find position to insert (before URL if it exists, otherwise at start) let url_pos = self .0 .children_with_tokens() .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL)); if let Some(url_idx) = url_pos { // Insert options list and a space before the URL // Build a parent node containing both space and whitespace to extract from let mut combined_builder = GreenNodeBuilder::new(); combined_builder.start_node(ROOT.into()); // Temporary parent combined_builder.token(WHITESPACE.into(), " "); combined_builder.finish_node(); let temp_green = combined_builder.finish(); let temp_root = SyntaxNode::new_root_mut(temp_green); let space_element = temp_root.children_with_tokens().next().unwrap(); self.0 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]); } else { self.0.splice_children(0..0, vec![new_opts_node.into()]); } } } /// Delete an option. /// /// Removes the option with the specified key from the options list. /// If the option doesn't exist, this method does nothing. /// If deleting the option results in an empty options list, the entire /// opts= declaration is removed. pub fn del_opt(&mut self, key: &str) { if let Some(mut ol) = self.option_list() { let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count(); if option_count == 1 && ol.has_option(key) { // This is the last option, remove the entire OPTS_LIST from Entry let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST); if let Some(opts_idx) = opts_pos { // Remove the OPTS_LIST self.0.splice_children(opts_idx..opts_idx + 1, vec![]); // Remove any leading whitespace/continuation that was after the OPTS_LIST while self.0.children_with_tokens().next().map_or(false, |e| { matches!( e, SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION ) }) { self.0.splice_children(0..1, vec![]); } } } else { // Defer to OptionList to remove the option ol.remove_option(key); } } } } const SUBSTITUTIONS: &[(&str, &str)] = &[ // This is substituted with the source package name found in the first line // of the debian/changelog file. // "@PACKAGE@": None, // This is substituted by the legal upstream version regex (capturing). ("@ANY_VERSION@", r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"), // This is substituted by the typical archive file extension regex // (non-capturing). ( "@ARCHIVE_EXT@", r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)", ), // This is substituted by the typical signature file extension regex // (non-capturing). ( "@SIGNATURE_EXT@", r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)\.(?:asc|pgp|gpg|sig|sign)", ), // This is substituted by the typical Debian extension regexp (capturing). ("@DEB_EXT@", r"[\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$"), ]; pub fn subst(text: &str, package: impl FnOnce() -> String) -> String { let mut substs = SUBSTITUTIONS.to_vec(); let package_name; if text.contains("@PACKAGE@") { package_name = Some(package()); substs.push(("@PACKAGE@", package_name.as_deref().unwrap())); } let mut text = text.to_string(); for (k, v) in substs { text = text.replace(k, v); } text } #[test] fn test_subst() { assert_eq!( subst("@ANY_VERSION@", || unreachable!()), r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)" ); assert_eq!(subst("@PACKAGE@", || "dulwich".to_string()), "dulwich"); } impl std::fmt::Debug for OptionList { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("OptionList") .field("text", &self.0.text().to_string()) .finish() } } impl OptionList { /// Returns an iterator over all option nodes in the options list. pub fn options(&self) -> impl Iterator + '_ { self.0.children().filter_map(_Option::cast) } /// Find an option node by key. pub fn find_option(&self, key: &str) -> Option<_Option> { self.options().find(|opt| opt.key().as_deref() == Some(key)) } pub fn has_option(&self, key: &str) -> bool { self.options().any(|it| it.key().as_deref() == Some(key)) } pub fn get_option(&self, key: &str) -> Option { for child in self.options() { if child.key().as_deref() == Some(key) { return child.value(); } } None } /// Add a new option to the end of the options list. fn add_option(&mut self, key: &str, value: &str) { let option_count = self.0.children().filter(|n| n.kind() == OPTION).count(); // Build a structure containing separator (if needed) + option wrapped in a temporary parent let mut builder = GreenNodeBuilder::new(); builder.start_node(ROOT.into()); // Temporary parent if option_count > 0 { builder.start_node(OPTION_SEPARATOR.into()); builder.token(COMMA.into(), ","); builder.finish_node(); } builder.start_node(OPTION.into()); builder.token(KEY.into(), key); builder.token(EQUALS.into(), "="); builder.token(VALUE.into(), value); builder.finish_node(); builder.finish_node(); // Close temporary parent let combined_green = builder.finish(); // Create a temporary root to extract children from let temp_root = SyntaxNode::new_root_mut(combined_green); let new_children: Vec<_> = temp_root.children_with_tokens().collect(); let insert_pos = self.0.children_with_tokens().count(); self.0.splice_children(insert_pos..insert_pos, new_children); } /// Remove an option by key. Returns true if an option was removed. fn remove_option(&mut self, key: &str) -> bool { if let Some(mut opt) = self.find_option(key) { opt.remove(); true } else { false } } } impl _Option { /// Returns the key of the option. pub fn key(&self) -> Option { self.0.children_with_tokens().find_map(|it| match it { SyntaxElement::Token(token) => { if token.kind() == KEY { Some(token.text().to_string()) } else { None } } _ => None, }) } /// Returns the value of the option. pub fn value(&self) -> Option { self.0 .children_with_tokens() .filter_map(|it| match it { SyntaxElement::Token(token) => { if token.kind() == VALUE || token.kind() == KEY { Some(token.text().to_string()) } else { None } } _ => None, }) .nth(1) } /// Set the value of the option. pub fn set_value(&mut self, new_value: &str) { let key = self.key().expect("Option must have a key"); // Build a new OPTION node with the updated value let mut builder = GreenNodeBuilder::new(); builder.start_node(OPTION.into()); builder.token(KEY.into(), &key); builder.token(EQUALS.into(), "="); builder.token(VALUE.into(), new_value); builder.finish_node(); let new_option_green = builder.finish(); let new_option_node = SyntaxNode::new_root_mut(new_option_green); // Replace this option in the parent OptionList if let Some(parent) = self.0.parent() { let idx = self.0.index(); parent.splice_children(idx..idx + 1, vec![new_option_node.into()]); } } /// Remove this option and its associated separator from the parent OptionList. pub fn remove(&mut self) { // Find adjacent separator to remove before detaching this node let next_sep = self .0 .next_sibling() .filter(|n| n.kind() == OPTION_SEPARATOR); let prev_sep = self .0 .prev_sibling() .filter(|n| n.kind() == OPTION_SEPARATOR); // Detach separator first if it exists if let Some(sep) = next_sep { sep.detach(); } else if let Some(sep) = prev_sep { sep.detach(); } // Now detach the option itself self.0.detach(); } } impl Url { /// Returns the URL string. pub fn url(&self) -> String { self.0 .children_with_tokens() .find_map(|it| match it { SyntaxElement::Token(token) => { if token.kind() == VALUE { Some(token.text().to_string()) } else { None } } _ => None, }) .unwrap() } } impl MatchingPattern { /// Returns the matching pattern string. pub fn pattern(&self) -> String { self.0 .children_with_tokens() .find_map(|it| match it { SyntaxElement::Token(token) => { if token.kind() == VALUE { Some(token.text().to_string()) } else { None } } _ => None, }) .unwrap() } } impl VersionPolicyNode { /// Returns the version policy string. pub fn policy(&self) -> String { self.0 .children_with_tokens() .find_map(|it| match it { SyntaxElement::Token(token) => { // Can be KEY (e.g., "debian") or VALUE if token.kind() == VALUE || token.kind() == KEY { Some(token.text().to_string()) } else { None } } _ => None, }) .unwrap() } } impl ScriptNode { /// Returns the script string. pub fn script(&self) -> String { self.0 .children_with_tokens() .find_map(|it| match it { SyntaxElement::Token(token) => { // Can be KEY (e.g., "uupdate") or VALUE if token.kind() == VALUE || token.kind() == KEY { Some(token.text().to_string()) } else { None } } _ => None, }) .unwrap() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_entry_node_structure() { // Test that entries properly use the new node types let wf: super::WatchFile = r#"version=4 opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); // Verify URL node exists and works assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true); assert_eq!(entry.url(), "https://example.com/releases"); // Verify MATCHING_PATTERN node exists and works assert_eq!( entry .0 .children() .find(|n| n.kind() == MATCHING_PATTERN) .is_some(), true ); assert_eq!( entry.matching_pattern(), Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into()) ); // Verify VERSION_POLICY node exists and works assert_eq!( entry .0 .children() .find(|n| n.kind() == VERSION_POLICY) .is_some(), true ); assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian))); // Verify SCRIPT node exists and works assert_eq!( entry.0.children().find(|n| n.kind() == SCRIPT).is_some(), true ); assert_eq!(entry.script(), Some("uupdate".into())); } #[test] fn test_entry_node_structure_partial() { // Test entry with only URL and pattern (no version or script) let wf: super::WatchFile = r#"version=4 https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); // Should have URL and MATCHING_PATTERN nodes assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true); assert_eq!( entry .0 .children() .find(|n| n.kind() == MATCHING_PATTERN) .is_some(), true ); // Should NOT have VERSION_POLICY or SCRIPT nodes assert_eq!( entry .0 .children() .find(|n| n.kind() == VERSION_POLICY) .is_some(), false ); assert_eq!( entry.0.children().find(|n| n.kind() == SCRIPT).is_some(), false ); // Verify accessors work correctly assert_eq!(entry.url(), "https://github.com/example/tags"); assert_eq!( entry.matching_pattern(), Some(".*/v?(\\d\\S+)\\.tar\\.gz".into()) ); assert_eq!(entry.version(), Ok(None)); assert_eq!(entry.script(), None); } #[test] fn test_parse_v1() { const WATCHV1: &str = r#"version=4 opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \ https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz "#; let parsed = parse(WATCHV1); //assert_eq!(parsed.errors, Vec::::new()); let node = parsed.syntax(); assert_eq!( format!("{:#?}", node), r#"ROOT@0..161 VERSION@0..10 KEY@0..7 "version" EQUALS@7..8 "=" VALUE@8..9 "4" NEWLINE@9..10 "\n" ENTRY@10..161 OPTS_LIST@10..86 KEY@10..14 "opts" EQUALS@14..15 "=" OPTION@15..19 KEY@15..19 "bare" OPTION_SEPARATOR@19..20 COMMA@19..20 "," OPTION@20..86 KEY@20..34 "filenamemangle" EQUALS@34..35 "=" VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..." WHITESPACE@86..87 " " CONTINUATION@87..89 "\\\n" WHITESPACE@89..91 " " URL@91..138 VALUE@91..138 "https://github.com/sy ..." WHITESPACE@138..139 " " MATCHING_PATTERN@139..160 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz" NEWLINE@160..161 "\n" "# ); let root = parsed.root(); assert_eq!(root.version(), 4); let entries = root.entries().collect::>(); assert_eq!(entries.len(), 1); let entry = &entries[0]; assert_eq!( entry.url(), "https://github.com/syncthing/syncthing-gtk/tags" ); assert_eq!( entry.matching_pattern(), Some(".*/v?(\\d\\S+)\\.tar\\.gz".into()) ); assert_eq!(entry.version(), Ok(None)); assert_eq!(entry.script(), None); assert_eq!(node.text(), WATCHV1); } #[test] fn test_parse_v2() { let parsed = parse( r#"version=4 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz # comment "#, ); assert_eq!(parsed.errors, Vec::::new()); let node = parsed.syntax(); assert_eq!( format!("{:#?}", node), r###"ROOT@0..90 VERSION@0..10 KEY@0..7 "version" EQUALS@7..8 "=" VALUE@8..9 "4" NEWLINE@9..10 "\n" ENTRY@10..80 URL@10..57 VALUE@10..57 "https://github.com/sy ..." WHITESPACE@57..58 " " MATCHING_PATTERN@58..79 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz" NEWLINE@79..80 "\n" COMMENT@80..89 "# comment" NEWLINE@89..90 "\n" "### ); let root = parsed.root(); assert_eq!(root.version(), 4); let entries = root.entries().collect::>(); assert_eq!(entries.len(), 1); let entry = &entries[0]; assert_eq!( entry.url(), "https://github.com/syncthing/syncthing-gtk/tags" ); assert_eq!( entry.format_url(|| "syncthing-gtk".to_string()), "https://github.com/syncthing/syncthing-gtk/tags" .parse() .unwrap() ); } #[test] fn test_parse_v3() { let parsed = parse( r#"version=4 https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz # comment "#, ); assert_eq!(parsed.errors, Vec::::new()); let root = parsed.root(); assert_eq!(root.version(), 4); let entries = root.entries().collect::>(); assert_eq!(entries.len(), 1); let entry = &entries[0]; assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags"); assert_eq!( entry.format_url(|| "syncthing-gtk".to_string()), "https://github.com/syncthing/syncthing-gtk/tags" .parse() .unwrap() ); } #[test] fn test_parse_v4() { let cl: super::WatchFile = r#"version=4 opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \ https://github.com/example/example-cat/tags \ (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate "# .parse() .unwrap(); assert_eq!(cl.version(), 4); let entries = cl.entries().collect::>(); assert_eq!(entries.len(), 1); let entry = &entries[0]; assert_eq!(entry.url(), "https://github.com/example/example-cat/tags"); assert_eq!( entry.matching_pattern(), Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into()) ); assert!(entry.repack()); assert_eq!(entry.compression(), Ok(Some(Compression::Xz))); assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into())); assert_eq!(entry.repacksuffix(), Some("+ds".into())); assert_eq!(entry.script(), Some("uupdate".into())); assert_eq!( entry.format_url(|| "example-cat".to_string()), "https://github.com/example/example-cat/tags" .parse() .unwrap() ); assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian))); } #[test] fn test_git_mode() { let text = r#"version=3 opts="mode=git, gitmode=shallow, pgpmode=gittag" \ https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \ refs/tags/(.*) debian "#; let parsed = parse(text); assert_eq!(parsed.errors, Vec::::new()); let cl = parsed.root(); assert_eq!(cl.version(), 3); let entries = cl.entries().collect::>(); assert_eq!(entries.len(), 1); let entry = &entries[0]; assert_eq!( entry.url(), "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git" ); assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into())); assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian))); assert_eq!(entry.script(), None); assert_eq!(entry.gitmode(), Ok(GitMode::Shallow)); assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag)); assert_eq!(entry.mode(), Ok(Mode::Git)); } #[test] fn test_parse_quoted() { const WATCHV1: &str = r#"version=4 opts="bare, filenamemangle=blah" \ https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz "#; let parsed = parse(WATCHV1); //assert_eq!(parsed.errors, Vec::::new()); let node = parsed.syntax(); let root = parsed.root(); assert_eq!(root.version(), 4); let entries = root.entries().collect::>(); assert_eq!(entries.len(), 1); let entry = &entries[0]; assert_eq!( entry.url(), "https://github.com/syncthing/syncthing-gtk/tags" ); assert_eq!( entry.matching_pattern(), Some(".*/v?(\\d\\S+)\\.tar\\.gz".into()) ); assert_eq!(entry.version(), Ok(None)); assert_eq!(entry.script(), None); assert_eq!(node.text(), WATCHV1); } #[test] fn test_set_url() { // Test setting URL on a simple entry without options let wf: super::WatchFile = r#"version=4 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!( entry.url(), "https://github.com/syncthing/syncthing-gtk/tags" ); entry.set_url("https://newurl.example.org/path"); assert_eq!(entry.url(), "https://newurl.example.org/path"); assert_eq!( entry.matching_pattern(), Some(".*/v?(\\d\\S+)\\.tar\\.gz".into()) ); // Verify the exact serialized output assert_eq!( entry.to_string(), "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n" ); } #[test] fn test_set_url_with_options() { // Test setting URL on an entry with options let wf: super::WatchFile = r#"version=4 opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!(entry.url(), "https://foo.com/bar"); assert_eq!(entry.get_option("foo"), Some("blah".to_string())); entry.set_url("https://example.com/baz"); assert_eq!(entry.url(), "https://example.com/baz"); // Verify options are preserved assert_eq!(entry.get_option("foo"), Some("blah".to_string())); assert_eq!( entry.matching_pattern(), Some(".*/v?(\\d\\S+)\\.tar\\.gz".into()) ); // Verify the exact serialized output assert_eq!( entry.to_string(), "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n" ); } #[test] fn test_set_url_complex() { // Test with a complex watch file with multiple options and continuation let wf: super::WatchFile = r#"version=4 opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \ https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!( entry.url(), "https://github.com/syncthing/syncthing-gtk/tags" ); entry.set_url("https://gitlab.com/newproject/tags"); assert_eq!(entry.url(), "https://gitlab.com/newproject/tags"); // Verify all options are preserved assert!(entry.bare()); assert_eq!( entry.filenamemangle(), Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into()) ); assert_eq!( entry.matching_pattern(), Some(".*/v?(\\d\\S+)\\.tar\\.gz".into()) ); // Verify the exact serialized output preserves structure assert_eq!( entry.to_string(), r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \ https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz "# ); } #[test] fn test_set_url_with_all_fields() { // Test with all fields: options, URL, matching pattern, version, and script let wf: super::WatchFile = r#"version=4 opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \ https://github.com/example/example-cat/tags \ (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!(entry.url(), "https://github.com/example/example-cat/tags"); assert_eq!( entry.matching_pattern(), Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into()) ); assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian))); assert_eq!(entry.script(), Some("uupdate".into())); entry.set_url("https://gitlab.example.org/project/releases"); assert_eq!(entry.url(), "https://gitlab.example.org/project/releases"); // Verify all other fields are preserved assert!(entry.repack()); assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz))); assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into())); assert_eq!(entry.repacksuffix(), Some("+ds".into())); assert_eq!( entry.matching_pattern(), Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into()) ); assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian))); assert_eq!(entry.script(), Some("uupdate".into())); // Verify the exact serialized output assert_eq!( entry.to_string(), r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \ https://gitlab.example.org/project/releases \ (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate "# ); } #[test] fn test_set_url_quoted_options() { // Test with quoted options let wf: super::WatchFile = r#"version=4 opts="bare, filenamemangle=blah" \ https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!( entry.url(), "https://github.com/syncthing/syncthing-gtk/tags" ); entry.set_url("https://example.org/new/path"); assert_eq!(entry.url(), "https://example.org/new/path"); // Verify the exact serialized output assert_eq!( entry.to_string(), r#"opts="bare, filenamemangle=blah" \ https://example.org/new/path .*/v?(\d\S+)\.tar\.gz "# ); } #[test] fn test_set_opt_update_existing() { // Test updating an existing option let wf: super::WatchFile = r#"version=4 opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!(entry.get_option("foo"), Some("blah".to_string())); assert_eq!(entry.get_option("bar"), Some("baz".to_string())); entry.set_opt("foo", "updated"); assert_eq!(entry.get_option("foo"), Some("updated".to_string())); assert_eq!(entry.get_option("bar"), Some("baz".to_string())); // Verify the exact serialized output assert_eq!( entry.to_string(), "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n" ); } #[test] fn test_set_opt_add_new() { // Test adding a new option to existing options let wf: super::WatchFile = r#"version=4 opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!(entry.get_option("foo"), Some("blah".to_string())); assert_eq!(entry.get_option("bar"), None); entry.set_opt("bar", "baz"); assert_eq!(entry.get_option("foo"), Some("blah".to_string())); assert_eq!(entry.get_option("bar"), Some("baz".to_string())); // Verify the exact serialized output assert_eq!( entry.to_string(), "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n" ); } #[test] fn test_set_opt_create_options_list() { // Test creating a new options list when none exists let wf: super::WatchFile = r#"version=4 https://example.com/releases .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!(entry.option_list(), None); entry.set_opt("compression", "xz"); assert_eq!(entry.get_option("compression"), Some("xz".to_string())); // Verify the exact serialized output assert_eq!( entry.to_string(), "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n" ); } #[test] fn test_del_opt_remove_single() { // Test removing a single option from multiple options let wf: super::WatchFile = r#"version=4 opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!(entry.get_option("foo"), Some("blah".to_string())); assert_eq!(entry.get_option("bar"), Some("baz".to_string())); assert_eq!(entry.get_option("qux"), Some("quux".to_string())); entry.del_opt("bar"); assert_eq!(entry.get_option("foo"), Some("blah".to_string())); assert_eq!(entry.get_option("bar"), None); assert_eq!(entry.get_option("qux"), Some("quux".to_string())); // Verify the exact serialized output assert_eq!( entry.to_string(), "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n" ); } #[test] fn test_del_opt_remove_first() { // Test removing the first option let wf: super::WatchFile = r#"version=4 opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); entry.del_opt("foo"); assert_eq!(entry.get_option("foo"), None); assert_eq!(entry.get_option("bar"), Some("baz".to_string())); // Verify the exact serialized output assert_eq!( entry.to_string(), "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n" ); } #[test] fn test_del_opt_remove_last() { // Test removing the last option let wf: super::WatchFile = r#"version=4 opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); entry.del_opt("bar"); assert_eq!(entry.get_option("foo"), Some("blah".to_string())); assert_eq!(entry.get_option("bar"), None); // Verify the exact serialized output assert_eq!( entry.to_string(), "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n" ); } #[test] fn test_del_opt_remove_only_option() { // Test removing the only option (should remove entire opts list) let wf: super::WatchFile = r#"version=4 opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!(entry.get_option("foo"), Some("blah".to_string())); entry.del_opt("foo"); assert_eq!(entry.get_option("foo"), None); assert_eq!(entry.option_list(), None); // Verify the exact serialized output (opts should be gone) assert_eq!( entry.to_string(), "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n" ); } #[test] fn test_del_opt_nonexistent() { // Test deleting a non-existent option (should do nothing) let wf: super::WatchFile = r#"version=4 opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); let original = entry.to_string(); entry.del_opt("nonexistent"); assert_eq!(entry.to_string(), original); } #[test] fn test_set_opt_multiple_operations() { // Test multiple set_opt operations let wf: super::WatchFile = r#"version=4 https://example.com/releases .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); entry.set_opt("compression", "xz"); entry.set_opt("repack", ""); entry.set_opt("dversionmangle", "s/\\+ds//"); assert_eq!(entry.get_option("compression"), Some("xz".to_string())); assert_eq!( entry.get_option("dversionmangle"), Some("s/\\+ds//".to_string()) ); } #[test] fn test_set_matching_pattern() { // Test setting matching pattern on a simple entry let wf: super::WatchFile = r#"version=4 https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!( entry.matching_pattern(), Some(".*/v?(\\d\\S+)\\.tar\\.gz".into()) ); entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz"); assert_eq!( entry.matching_pattern(), Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into()) ); // Verify URL is preserved assert_eq!(entry.url(), "https://github.com/example/tags"); // Verify the exact serialized output assert_eq!( entry.to_string(), "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n" ); } #[test] fn test_set_matching_pattern_with_all_fields() { // Test with all fields present let wf: super::WatchFile = r#"version=4 opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!( entry.matching_pattern(), Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into()) ); entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz"); assert_eq!( entry.matching_pattern(), Some(".*/version-([\\d.]+)\\.tar\\.xz".into()) ); // Verify all other fields are preserved assert_eq!(entry.url(), "https://example.com/releases"); assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian))); assert_eq!(entry.script(), Some("uupdate".into())); assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz))); // Verify the exact serialized output assert_eq!( entry.to_string(), "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n" ); } #[test] fn test_set_version_policy() { // Test setting version policy let wf: super::WatchFile = r#"version=4 https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian))); entry.set_version_policy("previous"); assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous))); // Verify all other fields are preserved assert_eq!(entry.url(), "https://example.com/releases"); assert_eq!( entry.matching_pattern(), Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into()) ); assert_eq!(entry.script(), Some("uupdate".into())); // Verify the exact serialized output assert_eq!( entry.to_string(), "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n" ); } #[test] fn test_set_version_policy_with_options() { // Test with options and continuation let wf: super::WatchFile = r#"version=4 opts=repack,compression=xz \ https://github.com/example/example-cat/tags \ (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian))); entry.set_version_policy("ignore"); assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore))); // Verify all other fields are preserved assert_eq!(entry.url(), "https://github.com/example/example-cat/tags"); assert_eq!( entry.matching_pattern(), Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into()) ); assert_eq!(entry.script(), Some("uupdate".into())); assert!(entry.repack()); // Verify the exact serialized output assert_eq!( entry.to_string(), r#"opts=repack,compression=xz \ https://github.com/example/example-cat/tags \ (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate "# ); } #[test] fn test_set_script() { // Test setting script let wf: super::WatchFile = r#"version=4 https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!(entry.script(), Some("uupdate".into())); entry.set_script("uscan"); assert_eq!(entry.script(), Some("uscan".into())); // Verify all other fields are preserved assert_eq!(entry.url(), "https://example.com/releases"); assert_eq!( entry.matching_pattern(), Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into()) ); assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian))); // Verify the exact serialized output assert_eq!( entry.to_string(), "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n" ); } #[test] fn test_set_script_with_options() { // Test with options let wf: super::WatchFile = r#"version=4 opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate "# .parse() .unwrap(); let mut entry = wf.entries().next().unwrap(); assert_eq!(entry.script(), Some("uupdate".into())); entry.set_script("custom-script.sh"); assert_eq!(entry.script(), Some("custom-script.sh".into())); // Verify all other fields are preserved assert_eq!(entry.url(), "https://example.com/releases"); assert_eq!( entry.matching_pattern(), Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into()) ); assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian))); assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz))); // Verify the exact serialized output assert_eq!( entry.to_string(), "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n" ); } #[test] fn test_apply_dversionmangle() { // Test basic dversionmangle let wf: super::WatchFile = r#"version=4 opts=dversionmangle=s/\+dfsg$// https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0"); assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0"); // Test with versionmangle (fallback) let wf: super::WatchFile = r#"version=4 opts=versionmangle=s/^v// https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0"); // Test with both dversionmangle and versionmangle (dversionmangle takes precedence) let wf: super::WatchFile = r#"version=4 opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0"); // Test without any mangle options let wf: super::WatchFile = r#"version=4 https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg"); } #[test] fn test_apply_oversionmangle() { // Test basic oversionmangle - adding suffix let wf: super::WatchFile = r#"version=4 opts=oversionmangle=s/$/-1/ https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1"); assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1"); // Test oversionmangle for adding +dfsg suffix let wf: super::WatchFile = r#"version=4 opts=oversionmangle=s/$/.dfsg/ https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg"); // Test without any mangle options let wf: super::WatchFile = r#"version=4 https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0"); } #[test] fn test_apply_dirversionmangle() { // Test basic dirversionmangle - removing 'v' prefix let wf: super::WatchFile = r#"version=4 opts=dirversionmangle=s/^v// https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0"); assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3"); // Test dirversionmangle with capture groups let wf: super::WatchFile = r#"version=4 opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0"); // Test without any mangle options let wf: super::WatchFile = r#"version=4 https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0"); } #[test] fn test_apply_filenamemangle() { // Test filenamemangle to generate tarball filename let wf: super::WatchFile = r#"version=4 opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!( entry .apply_filenamemangle("https://example.com/v1.0.tar.gz") .unwrap(), "mypackage-1.0.tar.gz" ); assert_eq!( entry .apply_filenamemangle("https://example.com/2.5.3.tar.gz") .unwrap(), "mypackage-2.5.3.tar.gz" ); // Test filenamemangle with different pattern let wf: super::WatchFile = r#"version=4 opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!( entry .apply_filenamemangle("https://example.com/path/to/file.tar.gz") .unwrap(), "file.tar.gz" ); // Test without any mangle options let wf: super::WatchFile = r#"version=4 https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!( entry .apply_filenamemangle("https://example.com/file.tar.gz") .unwrap(), "https://example.com/file.tar.gz" ); } #[test] fn test_apply_pagemangle() { // Test pagemangle to decode HTML entities let wf: super::WatchFile = r#"version=4 opts=pagemangle=s/&/&/g https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!( entry.apply_pagemangle(b"foo & bar").unwrap(), b"foo & bar" ); assert_eq!( entry .apply_pagemangle(b"& foo & bar &") .unwrap(), b"& foo & bar &" ); // Test pagemangle with different pattern let wf: super::WatchFile = r#"version=4 opts=pagemangle=s/<[^>]+>//g https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!(entry.apply_pagemangle(b"
text
").unwrap(), b"text"); // Test without any mangle options let wf: super::WatchFile = r#"version=4 https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!( entry.apply_pagemangle(b"foo & bar").unwrap(), b"foo & bar" ); } #[test] fn test_apply_downloadurlmangle() { // Test downloadurlmangle to change URL path let wf: super::WatchFile = r#"version=4 opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!( entry .apply_downloadurlmangle("https://example.com/archive/file.tar.gz") .unwrap(), "https://example.com/download/file.tar.gz" ); // Test downloadurlmangle with different pattern let wf: super::WatchFile = r#"version=4 opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!( entry .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz") .unwrap(), "https://raw.githubusercontent.com/user/repo/file.tar.gz" ); // Test without any mangle options let wf: super::WatchFile = r#"version=4 https://example.com/ .* "# .parse() .unwrap(); let entry = wf.entries().next().unwrap(); assert_eq!( entry .apply_downloadurlmangle("https://example.com/archive/file.tar.gz") .unwrap(), "https://example.com/archive/file.tar.gz" ); } #[test] fn test_entry_builder_minimal() { // Test creating a minimal entry with just URL and pattern let entry = super::EntryBuilder::new("https://github.com/example/tags") .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz") .build(); assert_eq!(entry.url(), "https://github.com/example/tags"); assert_eq!( entry.matching_pattern().as_deref(), Some(".*/v?(\\d\\S+)\\.tar\\.gz") ); assert_eq!(entry.version(), Ok(None)); assert_eq!(entry.script(), None); assert!(entry.opts().is_empty()); } #[test] fn test_entry_builder_url_only() { // Test creating an entry with just URL let entry = super::EntryBuilder::new("https://example.com/releases").build(); assert_eq!(entry.url(), "https://example.com/releases"); assert_eq!(entry.matching_pattern(), None); assert_eq!(entry.version(), Ok(None)); assert_eq!(entry.script(), None); assert!(entry.opts().is_empty()); } #[test] fn test_entry_builder_with_all_fields() { // Test creating an entry with all fields let entry = super::EntryBuilder::new("https://github.com/example/tags") .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz") .version_policy("debian") .script("uupdate") .opt("compression", "xz") .flag("repack") .build(); assert_eq!(entry.url(), "https://github.com/example/tags"); assert_eq!( entry.matching_pattern().as_deref(), Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz") ); assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian))); assert_eq!(entry.script(), Some("uupdate".into())); assert_eq!(entry.get_option("compression"), Some("xz".to_string())); assert!(entry.has_option("repack")); assert!(entry.repack()); } #[test] fn test_entry_builder_multiple_options() { // Test creating an entry with multiple options let entry = super::EntryBuilder::new("https://example.com/tags") .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz") .opt("compression", "xz") .opt("dversionmangle", "s/\\+ds//") .opt("repacksuffix", "+ds") .build(); assert_eq!(entry.get_option("compression"), Some("xz".to_string())); assert_eq!( entry.get_option("dversionmangle"), Some("s/\\+ds//".to_string()) ); assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string())); } #[test] fn test_entry_builder_via_entry() { // Test using Entry::builder() convenience method let entry = super::Entry::builder("https://github.com/example/tags") .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz") .version_policy("debian") .build(); assert_eq!(entry.url(), "https://github.com/example/tags"); assert_eq!( entry.matching_pattern().as_deref(), Some(".*/v?(\\d\\S+)\\.tar\\.gz") ); assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian))); } #[test] fn test_watchfile_add_entry_to_empty() { // Test adding an entry to an empty watchfile let mut wf = super::WatchFile::new(Some(4)); let entry = super::EntryBuilder::new("https://github.com/example/tags") .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz") .build(); wf.add_entry(entry); assert_eq!(wf.version(), 4); assert_eq!(wf.entries().count(), 1); let added_entry = wf.entries().next().unwrap(); assert_eq!(added_entry.url(), "https://github.com/example/tags"); assert_eq!( added_entry.matching_pattern().as_deref(), Some(".*/v?(\\d\\S+)\\.tar\\.gz") ); } #[test] fn test_watchfile_add_multiple_entries() { // Test adding multiple entries to a watchfile let mut wf = super::WatchFile::new(Some(4)); wf.add_entry( super::EntryBuilder::new("https://github.com/example1/tags") .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz") .build(), ); wf.add_entry( super::EntryBuilder::new("https://github.com/example2/releases") .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz") .opt("compression", "xz") .build(), ); assert_eq!(wf.entries().count(), 2); let entries: Vec<_> = wf.entries().collect(); assert_eq!(entries[0].url(), "https://github.com/example1/tags"); assert_eq!(entries[1].url(), "https://github.com/example2/releases"); assert_eq!(entries[1].get_option("compression"), Some("xz".to_string())); } #[test] fn test_watchfile_add_entry_to_existing() { // Test adding an entry to a watchfile that already has entries let mut wf: super::WatchFile = r#"version=4 https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz "# .parse() .unwrap(); assert_eq!(wf.entries().count(), 1); wf.add_entry( super::EntryBuilder::new("https://github.com/example/new") .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz") .opt("compression", "xz") .version_policy("debian") .build(), ); assert_eq!(wf.entries().count(), 2); let entries: Vec<_> = wf.entries().collect(); assert_eq!(entries[0].url(), "https://example.com/old"); assert_eq!(entries[1].url(), "https://github.com/example/new"); assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian))); } #[test] fn test_entry_builder_formatting() { // Test that the builder produces correctly formatted entries let entry = super::EntryBuilder::new("https://github.com/example/tags") .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz") .opt("compression", "xz") .flag("repack") .version_policy("debian") .script("uupdate") .build(); let entry_str = entry.to_string(); // Should start with opts= assert!(entry_str.starts_with("opts=")); // Should contain the URL assert!(entry_str.contains("https://github.com/example/tags")); // Should contain the pattern assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz")); // Should contain version policy assert!(entry_str.contains("debian")); // Should contain script assert!(entry_str.contains("uupdate")); // Should end with newline assert!(entry_str.ends_with('\n')); } #[test] fn test_watchfile_add_entry_preserves_format() { // Test that adding entries preserves the watchfile format let mut wf = super::WatchFile::new(Some(4)); wf.add_entry( super::EntryBuilder::new("https://github.com/example/tags") .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz") .build(), ); let wf_str = wf.to_string(); // Should have version line assert!(wf_str.starts_with("version=4\n")); // Should have the entry assert!(wf_str.contains("https://github.com/example/tags")); // Parse it back and ensure it's still valid let reparsed: super::WatchFile = wf_str.parse().unwrap(); assert_eq!(reparsed.version(), 4); assert_eq!(reparsed.entries().count(), 1); } #[test] fn test_line_col() { let text = r#"version=4 opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate "#; let wf = text.parse::().unwrap(); // Test version line position let version_node = wf.version_node().unwrap(); assert_eq!(version_node.line(), 0); assert_eq!(version_node.column(), 0); assert_eq!(version_node.line_col(), (0, 0)); // Test entry line numbers let entries: Vec<_> = wf.entries().collect(); assert_eq!(entries.len(), 1); // Entry starts at line 1 assert_eq!(entries[0].line(), 1); assert_eq!(entries[0].column(), 0); assert_eq!(entries[0].line_col(), (1, 0)); // Test node accessors let option_list = entries[0].option_list().unwrap(); assert_eq!(option_list.line(), 1); // Option list is on line 1 let url_node = entries[0].url_node().unwrap(); assert_eq!(url_node.line(), 1); // URL is on line 1 let pattern_node = entries[0].matching_pattern_node().unwrap(); assert_eq!(pattern_node.line(), 1); // Pattern is on line 1 let version_policy_node = entries[0].version_node().unwrap(); assert_eq!(version_policy_node.line(), 1); // Version policy is on line 1 let script_node = entries[0].script_node().unwrap(); assert_eq!(script_node.line(), 1); // Script is on line 1 // Test individual option nodes let options: Vec<_> = option_list.options().collect(); assert_eq!(options.len(), 1); assert_eq!(options[0].key(), Some("compression".to_string())); assert_eq!(options[0].value(), Some("xz".to_string())); assert_eq!(options[0].line(), 1); // Option is on line 1 // Test find_option let compression_opt = option_list.find_option("compression").unwrap(); assert_eq!(compression_opt.line(), 1); assert_eq!(compression_opt.column(), 5); // After "opts=" assert_eq!(compression_opt.line_col(), (1, 5)); } #[test] fn test_parse_str_relaxed() { let wf: super::WatchFile = super::WatchFile::from_str_relaxed( r#"version=4 ERRORS IN THIS LINE opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d "#, ); assert_eq!(wf.version(), 4); assert_eq!(wf.entries().count(), 2); let entries = wf.entries().collect::>(); let entry = &entries[0]; assert_eq!(entry.url(), "ERRORS"); let entry = &entries[1]; assert_eq!(entry.url(), "https://example.com/releases"); assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d")); assert_eq!(entry.get_option("compression"), Some("xz".to_string())); } } debian-watch-0.2.20/src/pgp.rs000064400000000000000000000213441046102023000141620ustar 00000000000000//! PGP signature verification support. use sequoia_openpgp as openpgp; use std::io::Read; /// Error type for PGP operations #[derive(Debug)] pub enum PgpError { /// Failed to parse signature SignatureParseError(String), /// Failed to verify signature VerificationError(String), /// IO error IoError(std::io::Error), /// Sequoia error SequoiaError(String), } impl std::fmt::Display for PgpError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { PgpError::SignatureParseError(s) => write!(f, "signature parse error: {}", s), PgpError::VerificationError(s) => write!(f, "verification error: {}", s), PgpError::IoError(e) => write!(f, "IO error: {}", e), PgpError::SequoiaError(e) => write!(f, "sequoia error: {}", e), } } } impl std::error::Error for PgpError {} impl From for PgpError { fn from(e: std::io::Error) -> Self { PgpError::IoError(e) } } impl From for PgpError { fn from(e: openpgp::Error) -> Self { PgpError::SequoiaError(e.to_string()) } } impl From for PgpError { fn from(e: anyhow::Error) -> Self { PgpError::SequoiaError(e.to_string()) } } /// Common signature file extensions to probe pub const SIGNATURE_EXTENSIONS: &[&str] = &[".asc", ".sig", ".sign", ".gpg"]; /// Result of signature verification #[derive(Debug, Clone, PartialEq, Eq)] pub struct SignatureVerification { /// Whether the signature is cryptographically valid pub valid: bool, /// The fingerprint of the signing key pub fingerprint: Option, /// Error message if verification failed pub error: Option, } impl SignatureVerification { /// Create a successful verification result pub fn valid(fingerprint: String) -> Self { Self { valid: true, fingerprint: Some(fingerprint), error: None, } } /// Create a failed verification result pub fn invalid(error: String) -> Self { Self { valid: false, fingerprint: None, error: Some(error), } } } /// Generate potential signature URLs from a tarball URL /// /// Returns a list of URLs that might contain the detached signature, /// based on common naming conventions. /// /// # Examples /// /// ``` /// use debian_watch::pgp::probe_signature_urls; /// /// let tarball_url = "https://example.com/project-1.0.tar.gz"; /// let sig_urls = probe_signature_urls(tarball_url); /// assert_eq!(sig_urls, vec![ /// "https://example.com/project-1.0.tar.gz.asc", /// "https://example.com/project-1.0.tar.gz.sig", /// "https://example.com/project-1.0.tar.gz.sign", /// "https://example.com/project-1.0.tar.gz.gpg", /// ]); /// ``` pub fn probe_signature_urls(url: &str) -> Vec { SIGNATURE_EXTENSIONS .iter() .map(|ext| format!("{}{}", url, ext)) .collect() } /// Verify a detached PGP signature and extract the key fingerprint /// /// Verifies that the signature correctly signs the data using the provided certificate. /// This performs cryptographic verification but does NOT verify certificate trust or validity. /// The caller is responsible for trust decisions. /// /// # Arguments /// /// * `signature` - The detached signature data (e.g., .asc file contents) /// * `data` - The data that was signed /// * `cert` - The PGP certificate containing the public key /// /// # Returns /// /// * `Ok(fingerprint)` with the signing key's fingerprint if the signature is cryptographically valid /// * `Err(PgpError)` if verification fails or parsing errors occur /// /// # Examples /// /// ```ignore /// use debian_watch::pgp::verify_detached; /// /// let data = b"Hello, world!"; /// let signature = std::fs::read("data.sig")?; /// let cert = std::fs::read("pubkey.asc")?; /// /// match verify_detached(&signature[..], &data[..], &cert[..]) { /// Ok(fingerprint) => println!("Signature valid, key fingerprint: {}", fingerprint), /// Err(e) => eprintln!("Signature verification failed: {}", e), /// } /// ``` pub fn verify_detached(signature: S, data: D, cert: C) -> Result where S: Read + Send + Sync, D: Read + Send + Sync, C: Read + Send + Sync, { use openpgp::parse::stream::*; use openpgp::parse::Parse; use openpgp::policy::StandardPolicy; let p = &StandardPolicy::new(); // Parse the certificate let cert = openpgp::Cert::from_reader(cert) .map_err(|e| PgpError::SignatureParseError(e.to_string()))?; // Create a helper that provides public keys for verification struct Helper<'a> { cert: &'a openpgp::Cert, fingerprint: Option, } impl<'a> VerificationHelper for Helper<'a> { fn get_certs( &mut self, _ids: &[openpgp::KeyHandle], ) -> openpgp::Result> { Ok(vec![self.cert.clone()]) } fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> { // Check that we have at least one valid signature let mut valid_signature = false; for layer in structure.iter() { match layer { MessageLayer::SignatureGroup { results } => { for result in results { match result { Ok(GoodChecksum { ka, .. }) => { valid_signature = true; // Extract the fingerprint from the key amalgamation self.fingerprint = Some(ka.key().fingerprint().to_hex()); } Err(e) => { eprintln!("Signature verification failed: {}", e); } } } } MessageLayer::Compression { .. } => {} MessageLayer::Encryption { .. } => {} } } if valid_signature { Ok(()) } else { Err(anyhow::anyhow!("No valid signature found")) } } } let helper = Helper { cert: &cert, fingerprint: None, }; // Create a verifier and verify the data let mut verifier = DetachedVerifierBuilder::from_reader(signature)?.with_policy(p, None, helper)?; // In sequoia v2, we verify by calling verify_reader with the data verifier.verify_reader(data)?; // Extract the fingerprint from the helper let fingerprint = verifier .into_helper() .fingerprint .ok_or_else(|| PgpError::VerificationError("No fingerprint found".to_string()))?; Ok(fingerprint) } /// Verify a detached signature from byte slices and extract the key fingerprint /// /// Convenience wrapper around `verify_detached` for in-memory data. /// /// # Examples /// /// ```ignore /// use debian_watch::pgp::verify_detached_bytes; /// /// let data = b"Hello, world!"; /// let signature = include_bytes!("test.sig"); /// let cert = include_bytes!("test_key.asc"); /// /// let fingerprint = verify_detached_bytes(signature, data, cert)?; /// println!("Signature valid, key fingerprint: {}", fingerprint); /// ``` pub fn verify_detached_bytes( signature: &[u8], data: &[u8], cert: &[u8], ) -> Result { verify_detached( std::io::Cursor::new(signature), std::io::Cursor::new(data), std::io::Cursor::new(cert), ) } #[cfg(test)] mod tests { use super::*; #[test] fn test_probe_signature_urls() { let url = "https://example.com/project-1.0.tar.gz"; let sig_urls = probe_signature_urls(url); assert_eq!( sig_urls, vec![ "https://example.com/project-1.0.tar.gz.asc", "https://example.com/project-1.0.tar.gz.sig", "https://example.com/project-1.0.tar.gz.sign", "https://example.com/project-1.0.tar.gz.gpg", ] ); } #[test] fn test_probe_signature_urls_tar_xz() { let url = "https://example.com/release.tar.xz"; let sig_urls = probe_signature_urls(url); assert_eq!( sig_urls, vec![ "https://example.com/release.tar.xz.asc", "https://example.com/release.tar.xz.sig", "https://example.com/release.tar.xz.sign", "https://example.com/release.tar.xz.gpg", ] ); } #[test] fn test_signature_extensions_constant() { assert_eq!(SIGNATURE_EXTENSIONS, &[".asc", ".sig", ".sign", ".gpg"]); } } debian-watch-0.2.20/src/release.rs000064400000000000000000000147771046102023000150300ustar 00000000000000//! Types for representing discovered releases. use debversion::Version; use std::cmp::Ordering; /// A discovered release from an upstream source #[derive(Debug, Clone, PartialEq, Eq)] pub struct Release { /// The version string of the release (after uversionmangle) pub version: String, /// The URL to download the release tarball (after downloadurlmangle) pub url: String, /// Optional URL to the PGP signature file pub pgpsigurl: Option, /// Optional target filename for the downloaded tarball (from filenamemangle) pub target_filename: Option, /// Optional Debian package version (from oversionmangle, e.g., "1.0+dfsg") pub package_version: Option, } impl Release { /// Create a new Release /// /// # Examples /// /// ``` /// use debian_watch::Release; /// /// let release = Release::new("1.0.0", "https://example.com/project-1.0.0.tar.gz", None); /// assert_eq!(release.version, "1.0.0"); /// assert_eq!(release.url, "https://example.com/project-1.0.0.tar.gz"); /// ``` pub fn new( version: impl Into, url: impl Into, pgpsigurl: Option, ) -> Self { Self { version: version.into(), url: url.into(), pgpsigurl, target_filename: None, package_version: None, } } /// Create a new Release with all fields /// /// # Examples /// /// ``` /// use debian_watch::Release; /// /// let release = Release::new_full( /// "1.0.0", /// "https://example.com/project-1.0.0.tar.gz", /// Some("https://example.com/project-1.0.0.tar.gz.asc".to_string()), /// Some("myproject_1.0.0.orig.tar.gz".to_string()), /// Some("1.0.0+dfsg".to_string()), /// ); /// assert_eq!(release.version, "1.0.0"); /// assert_eq!(release.target_filename, Some("myproject_1.0.0.orig.tar.gz".to_string())); /// ``` pub fn new_full( version: impl Into, url: impl Into, pgpsigurl: Option, target_filename: Option, package_version: Option, ) -> Self { Self { version: version.into(), url: url.into(), pgpsigurl, target_filename, package_version, } } /// Download the release tarball (async version) /// /// Downloads the tarball from the release URL. /// Requires the 'discover' feature. /// /// # Examples /// /// ```ignore /// use debian_watch::Release; /// /// # async fn example() -> Result<(), Box> { /// let release = Release::new("1.0.0", "https://example.com/project-1.0.tar.gz", None); /// let data = release.download().await?; /// println!("Downloaded {} bytes", data.len()); /// # Ok(()) /// # } /// ``` #[cfg(feature = "discover")] pub async fn download(&self) -> Result, Box> { let client = reqwest::Client::new(); let response = client.get(&self.url).send().await?; let bytes = response.bytes().await?; Ok(bytes.to_vec()) } /// Download the release tarball (blocking version) /// /// Downloads the tarball from the release URL. /// Requires both 'discover' and 'blocking' features. /// /// # Examples /// /// ```ignore /// use debian_watch::Release; /// /// let release = Release::new("1.0.0", "https://example.com/project-1.0.tar.gz", None); /// let data = release.download_blocking()?; /// println!("Downloaded {} bytes", data.len()); /// ``` #[cfg(all(feature = "discover", feature = "blocking"))] pub fn download_blocking(&self) -> Result, Box> { let client = reqwest::blocking::Client::new(); let response = client.get(&self.url).send()?; let bytes = response.bytes()?; Ok(bytes.to_vec()) } } impl PartialOrd for Release { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Release { fn cmp(&self, other: &Self) -> Ordering { // Parse versions and compare them match ( self.version.parse::(), other.version.parse::(), ) { (Ok(v1), Ok(v2)) => v1.cmp(&v2), // If parsing fails, fall back to string comparison _ => self.version.cmp(&other.version), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_release_new() { let release = Release::new("1.0.0", "https://example.com/foo.tar.gz", None); assert_eq!(release.version, "1.0.0"); assert_eq!(release.url, "https://example.com/foo.tar.gz"); assert_eq!(release.pgpsigurl, None); let release = Release::new( "2.0.0", "https://example.com/foo-2.0.0.tar.gz", Some("https://example.com/foo-2.0.0.tar.gz.asc".to_string()), ); assert_eq!(release.version, "2.0.0"); assert_eq!( release.pgpsigurl, Some("https://example.com/foo-2.0.0.tar.gz.asc".to_string()) ); } #[test] fn test_release_ordering() { let r1 = Release::new("1.0.0", "https://example.com/foo-1.0.0.tar.gz", None); let r2 = Release::new("2.0.0", "https://example.com/foo-2.0.0.tar.gz", None); let r3 = Release::new("1.5.0", "https://example.com/foo-1.5.0.tar.gz", None); assert!(r1 < r2); assert!(r2 > r1); assert!(r1 < r3); assert!(r3 < r2); } #[test] fn test_release_ordering_debian_versions() { // Test with Debian version strings let r1 = Release::new("1.0", "https://example.com/foo-1.0.tar.gz", None); let r2 = Release::new("1.0+dfsg", "https://example.com/foo-1.0+dfsg.tar.gz", None); let r3 = Release::new("1.0~rc1", "https://example.com/foo-1.0~rc1.tar.gz", None); // 1.0~rc1 < 1.0 < 1.0+dfsg in Debian version ordering assert!(r3 < r1); assert!(r1 < r2); } #[test] fn test_release_max() { let releases = vec![ Release::new("1.0.0", "https://example.com/foo-1.0.0.tar.gz", None), Release::new("2.0.0", "https://example.com/foo-2.0.0.tar.gz", None), Release::new("1.5.0", "https://example.com/foo-1.5.0.tar.gz", None), ]; let max = releases.iter().max().unwrap(); assert_eq!(max.version, "2.0.0"); } } debian-watch-0.2.20/src/search.rs000064400000000000000000000231561046102023000146440ustar 00000000000000//! Functions for searching web pages for upstream releases. use regex::Regex; use std::io::Read; /// Search for version matches in HTML content /// /// Parses the HTML and searches for links matching the given pattern. /// Returns an iterator of (version, url) tuples. /// /// # Arguments /// /// * `body` - The HTML content to search /// * `matching_pattern` - Regex pattern to match against URLs /// * `base_url` - Base URL for resolving relative links /// /// # Examples /// /// ```ignore /// use debian_watch::search::html_search; /// /// let html = b"Download"; /// let results: Vec<_> = html_search(html, r"project-(\d+\.\d+)\.tar\.gz", "https://example.com/") /// .collect(); /// assert_eq!(results.len(), 1); /// assert_eq!(results[0].0, "1.0"); /// ``` #[cfg(feature = "discover")] pub fn html_search( body: &[u8], matching_pattern: &str, base_url: &str, ) -> Box> { let html = String::from_utf8_lossy(body); let doc = scraper::Html::parse_document(&html); // Check for tag to use as base URL for resolving relative hrefs let base_selector = scraper::Selector::parse("base").unwrap(); let effective_base_url = doc .select(&base_selector) .filter_map(|element| element.value().attr("href")) .next() .unwrap_or(base_url); let base_url_parsed = match url::Url::parse(effective_base_url) { Ok(u) => u, Err(_) => return Box::new(std::iter::empty()), }; let selector = scraper::Selector::parse("a").unwrap(); let re = match Regex::new(matching_pattern) { Ok(r) => r, Err(_) => return Box::new(std::iter::empty()), }; let results: Vec<(String, String)> = doc .select(&selector) .filter_map(move |element| { let href = element.value().attr("href")?; // Match the pattern against the raw href value (as per uscan behavior) if let Some(captures) = re.captures(href) { // Extract the first capture group as the version if let Some(version_match) = captures.get(1) { let version = version_match.as_str().to_string(); // Convert href to absolute URL using proper URL joining // Use base tag href if present, otherwise use page URL let full_url = match base_url_parsed.join(href) { Ok(url) => url.to_string(), Err(_) => return None, }; Some((version, full_url)) } else { None } } else { None } }) .collect(); Box::new(results.into_iter()) } /// Search for version matches in plain text content /// /// Searches the plain text for matches of the given pattern. /// Returns an iterator of (version, url) tuples. /// /// # Arguments /// /// * `body` - The plain text content to search /// * `matching_pattern` - Regex pattern to match /// * `base_url` - Base URL for resolving relative links /// /// # Examples /// /// ``` /// use debian_watch::search::plain_search; /// /// let text = b"project-1.0.tar.gz\nproject-2.0.tar.gz"; /// let results: Vec<_> = plain_search(text, r"project-(\d+\.\d+)\.tar\.gz", "https://example.com/") /// .collect(); /// assert!(results.len() >= 1); /// ``` pub fn plain_search( body: &[u8], matching_pattern: &str, base_url: &str, ) -> Box> { let re = match Regex::new(matching_pattern) { Ok(r) => r, Err(_) => return Box::new(std::iter::empty()), }; let text = String::from_utf8_lossy(body); let base_url_parsed = match url::Url::parse(base_url) { Ok(u) => u, Err(_) => return Box::new(std::iter::empty()), }; let results: Vec<(String, String)> = re .captures_iter(&text) .filter_map(|captures| { // Extract the first capture group as the version if let Some(version_match) = captures.get(1) { let version = version_match.as_str().to_string(); // Use capture group 0 (full match) for constructing the URL let matched = captures.get(0).unwrap().as_str(); // Convert matched text to absolute URL using proper URL joining let full_url = if matched.starts_with("http://") || matched.starts_with("https://") { // Already absolute matched.to_string() } else { // Relative - use proper URL joining match base_url_parsed.join(matched) { Ok(url) => url.to_string(), Err(_) => return None, } }; Some((version, full_url)) } else { None } }) .collect(); Box::new(results.into_iter()) } /// Search for version matches in content /// /// Dispatches to either html_search or plain_search based on search mode. pub fn search( searchmode: &str, mut resp: R, matching_pattern: &str, _package: &str, url: &str, ) -> Result>, std::io::Error> { let mut body = Vec::new(); resp.read_to_end(&mut body)?; let iter: Box> = match searchmode { #[cfg(feature = "discover")] "html" => html_search(&body, matching_pattern, url), "plain" => plain_search(&body, matching_pattern, url), _ => Box::new(std::iter::empty()), }; Ok(iter) } #[cfg(test)] mod tests { use super::*; #[test] #[cfg(feature = "discover")] fn test_html_search() { let html = b"v1.0v2.0"; let results: Vec<_> = html_search(html, r"project-(\d+\.\d+)\.tar\.gz", "https://example.com/").collect(); assert_eq!(results.len(), 2); assert!(results.iter().any(|(v, _)| v == "1.0")); assert!(results.iter().any(|(v, _)| v == "2.0")); } #[test] #[cfg(feature = "discover")] fn test_html_search_absolute_urls() { // Test curl case with tag let html = b"curl"; let results: Vec<_> = html_search( html, r"download/curl-([\d.]+)\.tar\.gz", "https://curl.se/download/", ) .collect(); assert_eq!(results.len(), 1); assert_eq!(results[0].0, "8.14.0"); // With , the href resolves to https://curl.se/download/curl-8.14.0.tar.gz assert_eq!(results[0].1, "https://curl.se/download/curl-8.14.0.tar.gz"); } #[test] #[cfg(feature = "discover")] fn test_html_search_absolute_urls_with_slash_prefix() { // Test that returned URLs are absolute when href starts with '/' let html = b"curl"; let results: Vec<_> = html_search( html, r"download/curl-([\d.]+)\.tar\.gz", "https://curl.se/download/", ) .collect(); assert_eq!(results.len(), 1); assert_eq!(results[0].0, "8.14.0"); assert_eq!(results[0].1, "https://curl.se/download/curl-8.14.0.tar.gz"); } #[test] #[cfg(feature = "discover")] fn test_html_search_with_absolute_href() { // Test that absolute URLs in href are preserved correctly let html = b"v3.5.0"; let results: Vec<_> = html_search( html, r"https://example\.org/files/project-([\d.]+)\.tar\.gz", "https://example.com/", ) .collect(); assert_eq!(results.len(), 1); assert_eq!(results[0].0, "3.5.0"); assert_eq!( results[0].1, "https://example.org/files/project-3.5.0.tar.gz" ); } #[test] fn test_plain_search() { let text = b"Available: project-1.0.tar.gz project-2.0.tar.gz"; let results: Vec<_> = plain_search(text, r"project-(\d+\.\d+)\.tar\.gz", "https://example.com/").collect(); assert_eq!(results.len(), 2); assert!(results.iter().any(|(v, _)| v == "1.0")); assert!(results.iter().any(|(v, _)| v == "2.0")); } #[test] fn test_plain_search_absolute_urls() { // Test that returned URLs are absolute, not relative let text = b"Available: curl-8.14.0.tar.gz"; let results: Vec<_> = plain_search(text, r"curl-([\d.]+)\.tar\.gz", "https://curl.se/download/").collect(); assert_eq!(results.len(), 1); assert_eq!(results[0].0, "8.14.0"); assert_eq!(results[0].1, "https://curl.se/download/curl-8.14.0.tar.gz"); } #[test] fn test_plain_search_with_absolute_urls() { // Test that absolute URLs in text are preserved correctly let text = b"Available: https://example.org/files/project-3.5.0.tar.gz"; let results: Vec<_> = plain_search( text, r"https://example\.org/files/project-([\d.]+)\.tar\.gz", "https://example.com/", ) .collect(); assert_eq!(results.len(), 1); assert_eq!(results[0].0, "3.5.0"); assert_eq!( results[0].1, "https://example.org/files/project-3.5.0.tar.gz" ); } } debian-watch-0.2.20/src/types.rs000064400000000000000000000317001046102023000145350ustar 00000000000000use std::str::FromStr; /// The type of the component pub enum ComponentType { /// Perl component Perl, /// NodeJS component NodeJS, } impl std::fmt::Display for ComponentType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "{}", match self { ComponentType::Perl => "perl", ComponentType::NodeJS => "nodejs", } ) } } impl FromStr for ComponentType { type Err = (); fn from_str(s: &str) -> Result { match s { "perl" => Ok(ComponentType::Perl), "nodejs" => Ok(ComponentType::NodeJS), _ => Err(()), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] /// Compression type pub enum Compression { /// Gzip compression Gzip, /// Xz compression Xz, /// Bzip2 compression Bzip2, /// Lzma compression Lzma, #[default] /// Default compression Default, } impl std::fmt::Display for Compression { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "{}", match self { Compression::Gzip => "gzip", Compression::Xz => "xz", Compression::Bzip2 => "bzip2", Compression::Lzma => "lzma", Compression::Default => "default", } ) } } impl FromStr for Compression { type Err = (); fn from_str(s: &str) -> Result { match s { "gz" | "gzip" => Ok(Compression::Gzip), "xz" => Ok(Compression::Xz), "bz2" | "bzip2" => Ok(Compression::Bzip2), "lzma" => Ok(Compression::Lzma), "default" => Ok(Compression::Default), _ => Err(()), } } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] /// How to generate upstream version string from git tags pub enum Pretty { /// Use git describe to generate the version string Describe, /// Use a custom pattern to generate the version string Pattern(String), } impl Default for Pretty { fn default() -> Self { Pretty::Pattern("0.0~git%cd.h%".to_string()) } } impl std::fmt::Display for Pretty { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "{}", match self { Pretty::Describe => "describe", Pretty::Pattern(pattern) => pattern, } ) } } impl FromStr for Pretty { type Err = (); fn from_str(s: &str) -> Result { if s == "describe" { Ok(Pretty::Describe) } else { Ok(Pretty::Pattern(s.to_string())) } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] /// Git export mode pub enum GitExport { #[default] /// Export only files in the .orig.tar archive that are not ignored by the upstream. Default, /// Export all files in the .orig.tar archive, ignoring any export-ignore git attributes /// defined by the upstream. All, } impl std::fmt::Display for GitExport { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "{}", match self { GitExport::Default => "default".to_string(), GitExport::All => "all".to_string(), } ) } } impl FromStr for GitExport { type Err = (); fn from_str(s: &str) -> Result { match s { "default" => Ok(GitExport::Default), "all" => Ok(GitExport::All), _ => Err(()), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] /// Git clone operation mode pub enum GitMode { #[default] /// Clone the git repository in shallow mode Shallow, /// Clone the git repository in full mode Full, } impl std::fmt::Display for GitMode { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "{}", match self { GitMode::Shallow => "shallow".to_string(), GitMode::Full => "full".to_string(), } ) } } impl FromStr for GitMode { type Err = (); fn from_str(s: &str) -> Result { match s { "shallow" => Ok(GitMode::Shallow), "full" => Ok(GitMode::Full), _ => Err(()), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] /// PGP verification mode pub enum PgpMode { /// check possible URLs for the signature file and autogenerate a ``pgpsigurlmangle`` rule to /// use it Auto, #[default] /// Use pgpsigurlmangle=rules to generate the candidate upstream signature file URL string from /// the upstream tarball URL. /// /// If the specified pgpsigurlmangle is missing, uscan checks possible URLs for the signature /// file and suggests adding a pgpsigurlmangle rule. /// Default, /// Use pgpsigurlmangle=rules to generate the candidate upstream signature file URL string from the upstream tarball URL. Mangle, /// Verify this downloaded tarball file with the signature file specified in the next watch /// line. The next watch line must be pgpmode=previous. Otherwise, no verification occurs. Next, /// Verify the downloaded tarball file specified in the previous watch line with this signature /// file. The previous watch line must be pgpmode=next. Previous, /// Verify the downloaded file foo.ext with its self signature and extract its content tarball /// file as foo. SelfSignature, /// Verify tag signature if mode=git. GitTag, } impl std::fmt::Display for PgpMode { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "{}", match self { PgpMode::Auto => "auto", PgpMode::Default => "default", PgpMode::Mangle => "mangle", PgpMode::Next => "next", PgpMode::Previous => "previous", PgpMode::SelfSignature => "self", PgpMode::GitTag => "gittag", } ) } } impl FromStr for PgpMode { type Err = (); fn from_str(s: &str) -> Result { match s { "auto" => Ok(PgpMode::Auto), "default" => Ok(PgpMode::Default), "mangle" => Ok(PgpMode::Mangle), "next" => Ok(PgpMode::Next), "previous" => Ok(PgpMode::Previous), "self" => Ok(PgpMode::SelfSignature), "gittag" => Ok(PgpMode::GitTag), _ => Err(()), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] /// How to search for the upstream tarball pub enum SearchMode { #[default] /// Search for the upstream tarball in the HTML page Html, /// Search for the upstream tarball in the plain text page Plain, } impl std::fmt::Display for SearchMode { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "{}", match self { SearchMode::Html => "html", SearchMode::Plain => "plain", } ) } } impl FromStr for SearchMode { type Err = (); fn from_str(s: &str) -> Result { match s { "html" => Ok(SearchMode::Html), "plain" => Ok(SearchMode::Plain), _ => Err(()), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] /// Archive download mode pub enum Mode { #[default] /// downloads the specified tarball from the archive URL on the web. Automatically internal /// mode value is updated to either http or ftp by URL. LWP, /// Access the upstream git archive directly with the git command and packs the source tree /// with the specified tag via matching-pattern into spkg-version.tar.xz. Git, /// Access the upstream Subversion archive directly with the svn command and packs the source /// tree. Svn, } impl std::fmt::Display for Mode { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "{}", match self { Mode::LWP => "lwp", Mode::Git => "git", Mode::Svn => "svn", } ) } } impl FromStr for Mode { type Err = (); fn from_str(s: &str) -> Result { match s { "lwp" => Ok(Mode::LWP), "git" => Ok(Mode::Git), "svn" => Ok(Mode::Svn), _ => Err(()), } } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] /// The version policy to use when downloading upstream tarballs pub enum VersionPolicy { #[default] /// Requires the downloading upstream tarball to be newer than the version obtained from debian/changelog Debian, /// Requires the upstream tarball to be newer than specified version Version(debversion::Version), /// Requires the downloaded version of the secondary tarballs to be exactly the same as the one for the first upstream tarball downloaded Same, /// Restricts the version of the seignature file (used with pgpmode=previous) Previous, /// Does not restrict the version of the secondary tarballs Ignore, /// Requires the downloading upstream tarball to be newer than the version obtained from /// debian/changelog. Package version is the concatenation of all "group" upstream version. Group, /// Requires the downloading upstream tarball to be newer than the version obtained from /// debian/changelog. Package version is the concatenation of the version of the main tarball, /// followed by a checksum of all the tarballs using the checksum version system. At least the /// main upstream source has to be declared as group. Checksum, } impl std::fmt::Display for VersionPolicy { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { VersionPolicy::Debian => write!(f, "debian"), VersionPolicy::Version(v) => write!(f, "version-{}", v), VersionPolicy::Same => write!(f, "same"), VersionPolicy::Previous => write!(f, "previous"), VersionPolicy::Ignore => write!(f, "ignore"), VersionPolicy::Group => write!(f, "group"), VersionPolicy::Checksum => write!(f, "checksum"), } } } impl std::str::FromStr for VersionPolicy { type Err = String; fn from_str(s: &str) -> Result { match s { "debian" => Ok(VersionPolicy::Debian), "same" => Ok(VersionPolicy::Same), "previous" => Ok(VersionPolicy::Previous), "ignore" => Ok(VersionPolicy::Ignore), "group" => Ok(VersionPolicy::Group), "checksum" => Ok(VersionPolicy::Checksum), s if s.starts_with("version-") => { let v = s.trim_start_matches("version-"); Ok(VersionPolicy::Version( v.parse::() .map_err(|e| e.to_string())?, )) } _ => Err(format!("Unknown version policy: {}", s)), } } } #[cfg(test)] mod version_policy_tests { use super::VersionPolicy; use std::str::FromStr; #[test] fn test_version_policy_to_string() { assert_eq!("debian", VersionPolicy::Debian.to_string()); assert_eq!("same", VersionPolicy::Same.to_string()); assert_eq!("previous", VersionPolicy::Previous.to_string()); assert_eq!("ignore", VersionPolicy::Ignore.to_string()); assert_eq!("group", VersionPolicy::Group.to_string()); assert_eq!("checksum", VersionPolicy::Checksum.to_string()); assert_eq!( "version-1.2.3", VersionPolicy::Version("1.2.3".parse().unwrap()).to_string() ); } #[test] fn test_version_policy_from_str() { assert_eq!( VersionPolicy::Debian, VersionPolicy::from_str("debian").unwrap() ); assert_eq!( VersionPolicy::Same, VersionPolicy::from_str("same").unwrap() ); assert_eq!( VersionPolicy::Previous, VersionPolicy::from_str("previous").unwrap() ); assert_eq!( VersionPolicy::Ignore, VersionPolicy::from_str("ignore").unwrap() ); assert_eq!( VersionPolicy::Group, VersionPolicy::from_str("group").unwrap() ); assert_eq!( VersionPolicy::Checksum, VersionPolicy::from_str("checksum").unwrap() ); assert_eq!( VersionPolicy::Version("1.2.3".parse().unwrap()), VersionPolicy::from_str("version-1.2.3").unwrap() ); } }