diff --git a/.cargo-husky/hooks/pre-commit b/.cargo-husky/hooks/pre-commit deleted file mode 100755 index f735ef4..0000000 --- a/.cargo-husky/hooks/pre-commit +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git commit" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message if -# it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-commit". - -cargo build -cargo test diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d4b3cf1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "monthly" + day: "sunday" + commit-message: + prefix: "chore(dep): " + groups: + deps: + patterns: + - "*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + day: "sunday" + commit-message: + prefix: "chore(dep): " + groups: + deps: + patterns: + - "*" diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 004dc18..563233f 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -13,7 +13,7 @@ jobs: matrix: os: [macOS-latest, ubuntu-latest] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - uses: actions-rs/toolchain@v1 with: components: clippy diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1b86e39 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,21 @@ +name: release + +on: + push: + tags: + - "v*" +jobs: + publish: + name: Publish + # Specify OS + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - uses: katyo/publish-crates@v2 + with: + registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3f22989..ee47a77 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,9 +2,9 @@ name: leetcode-cli on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build: @@ -14,7 +14,7 @@ jobs: os: [macOS-latest, ubuntu-latest] steps: - name: Checkout the source code - uses: actions/checkout@v1 + uses: actions/checkout@v4 - name: Set nightly toolchain uses: actions-rs/toolchain@v1 with: @@ -22,13 +22,12 @@ jobs: - name: Environment run: | if [[ "$(uname)" == 'Darwin' ]]; then - brew update brew install sqlite3 else sudo apt-get update -y sudo apt-get install -y libsqlite3-dev libdbus-1-dev fi - name: Build - run: cargo build + run: cargo build --release --all-features - name: Run tests - run: cargo test + run: cargo test --release --all-features diff --git a/.gitignore b/.gitignore index 93cf624..67e0e9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ **/*target **/*.rs.bk -Cargo.lock .DS_Store .idea .direnv/ +/result diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b1a223..cb532f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,136 +1,171 @@ +## v0.4.1 + +- Search problems by name +- Re-enable chrome plugin + ## v0.3.3 -* allow more flexible categories by @frrad -* change params type to `Option` @frrad +- allow more flexible categories by @frrad +- change params type to `Option` @frrad ## v0.3.2 -* adds additional tracing by @shmuga -* removes test_mode parameter by @shmuga +- adds additional tracing by @shmuga +- removes test_mode parameter by @shmuga ## v0.3.1 -* pipe handling by @aymanbagabas -* Improve README by @xiaoxiae +- pipe handling by @aymanbagabas +- Improve README by @xiaoxiae ## v0.3.0 -* Upgrade reqwest to async mode -* Format code using clippy +- Upgrade reqwest to async mode +- Format code using clippy ## v0.2.23 -* support color display +- support color display ## v0.2.22 -* Fixed the cache can't update with new added problems -* Display user friendly errors when pick/edit new added problem. +- Fixed the cache can't update with new added problems -* upgrade pyo3 +- Display user friendly errors when pick/edit new added problem. -* fix leetcode list with empty cache +- upgrade pyo3 + +- fix leetcode list with empty cache ## v0.2.21 -* Make programmable support to be an advanced feature +- Make programmable support to be an advanced feature ## v0.2.20 -* Support sup/sub style for numbers +- Support sup/sub style for numbers ## v0.2.19 -* Better HTML! + +- Better HTML! ## v0.2.18 -* Display stdout for test and execute commands, fix minor spacing in results displayed -* Fix panic on `pick` command without cache +- Display stdout for test and execute commands, fix minor spacing in results displayed + +- Fix panic on `pick` command without cache ## v0.2.17 -Fix panic on stat command with zero numbers + +Fix panic on stat command with zero numbers ## v0.2.16 + Update versions of diesel and reqwest ## v0.2.15 + Allow for custom testcases with the `leetcode test` command, and some minor edits ## v0.2.14 -Corrects file suffixes for c** and c# files + +Corrects file suffixes for c\*\* and c# files ## v0.2.13 + fix percent length panic ## v0.2.12 + fix gt || ge || lt || le ## v0.2.11 + added code 14 and transfered `&#ge;`、`&#le` and `'`. ## v0.2.10 + add code 15 ## v0.2.9 + update ac status after submit successfully ## v0.2.8 + show last testcases ## v0.2.7 + fixed float bug in result ## v0.2.6 + sync config while change current lang ## v0.2.5 + update local cache when submission status changes ## v0.2.4 + auto fetch question while exec `edit` directly. ## v0.2.3 + Programmable leetcode-cli ## v0.2.2 + 1. optimize logs 2. add tag filter 3. sync configs ## v0.2.1 + 1. fix cookies error handling 2. dismiss all `unwrap`, `expect` and `panic` 3. add cookie configs ## v0.2.0 + 1. Add Linux Support ## v0.1.9 + 1. release submit command 2. deserialize json using outter funcs ## v0.1.8 + 1. pack mod exports 2. add edit command 3. add test command ## v0.1.7 + render html in command-line, and `pick` command ## v0.1.6 + complete `stat` command ## v0.1.5 + complete `cache` command ## v0.1.3 + complete `list` command ## v0.1.2 + abstract data cache ## v0.1.1 + add list command ## v0.1.0 + chrome cookie diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1f006e7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2559 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "async-compression" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.4", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad5b1b4de04fead402672b48897030eec1f3bfe1550776322f59f6d6e6a5677" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[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 = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[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 = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel" +version = "2.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a917a9209950404d5be011c81d081a2692a822f73c3d6af586f0cab5ff50f614" +dependencies = [ + "diesel_derives", + "libsqlite3-sys", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +dependencies = [ + "syn", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] + +[[package]] +name = "dsl_auto_type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" +dependencies = [ + "darling", + "either", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +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.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width 0.1.14", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +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 = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "html5ever" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e15626aaf9c351bc696217cbe29cb9b5e86c43f8a46b5e2f5c6c5cf7cb904ce" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keyring" +version = "3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1961983669d57bdfe6c0f3ef8e4c229b5ef751afcc7d87e4271d2f71f6ccfa8b" +dependencies = [ + "log", +] + +[[package]] +name = "leetcode-cli" +version = "0.4.7" +dependencies = [ + "anyhow", + "async-trait", + "clap", + "clap_complete", + "colored", + "diesel", + "dirs", + "env_logger", + "keyring", + "log", + "nix", + "openssl", + "pyo3", + "rand 0.9.1", + "regex", + "reqwest", + "scraper", + "serde", + "serde_json", + "thiserror", + "tokio", + "toml", + "unicode-width 0.2.1", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c88c6129bd24319e62a0359cb6b958fa7e8be6e19bb1663bc396b90883aca5" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "object" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[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.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "portable-atomic" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.35", +] + +[[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.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +dependencies = [ + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.14", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "async-compression", + "base64", + "bytes", + "encoding_rs", + "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", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e65d9d888567588db4c12da1087598d0f6f8b346cc2c5abc91f05fc2dffe2" +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.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +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 = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae65c4249478a2647db249fb43e23cec56a2c8974a427e7bd8cb5a1d0964921a" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[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 = "target-lexicon" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[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 = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +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.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +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 = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[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.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +dependencies = [ + "zerocopy-derive 0.8.14", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 5da9c82..a29aa42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,10 @@ path = "src/bin/lc.rs" [package] name = "leetcode-cli" -version = "0.3.12" -authors = ["clearloop "] +version = "0.4.7" +authors = ["clearloop "] edition = "2021" -description = "Leet your code in command-line." +description = "Leetcode command-line interface in rust." repository = "https://github.com/clearloop/leetcode-cli" license = "MIT" documentation = "https://docs.rs/leetcode_cli" @@ -16,38 +16,37 @@ keywords = ["cli", "games", "leetcode"] readme = './README.md' [dependencies] -async-trait = "0.1.56" -tokio = { version = "1.19.2", features = ["full"] } -clap = { version = "4", features = ["cargo"] } -colored = "2.0.0" -dirs = "4.0.0" -env_logger = "0.9.0" -keyring = "1.2.0" -log = "0.4.17" -openssl = "0.10.41" -pyo3 = { version = "0.16.5", optional = true } -rand = "0.8.5" -serde = { version = "1.0.139", features = ["derive"] } -serde_json = "1.0.82" -toml = "0.5.9" -regex = "1.6.0" -scraper = "0.13.0" +async-trait = "0.1.88" +tokio = { version = "1.45.1", features = ["full"] } +clap = { version = "4.5.40", features = ["cargo"] } +colored = "3.0.0" +dirs = "6.0.0" +env_logger = "0.11.6" +keyring = "3.6.2" +log = "0.4.27" +openssl = "0.10.73" +pyo3 = { version = "0.25.1", optional = true } +rand = "0.9.1" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +toml = "0.8.23" +regex = "1.11.1" +scraper = "0.23.1" +anyhow = "1.0.98" +clap_complete = "4.5.54" +thiserror = "2.0.12" +unicode-width = "0.2" [dependencies.diesel] -version = "1.4.8" +version = "2.2.11" features = ["sqlite"] [dependencies.reqwest] -version = "0.11.11" +version = "0.12.22" features = ["gzip", "json"] -[dev-dependencies.cargo-husky] -version = "1.5.0" -default-features = false -features = ["precommit-hook", "user-hooks"] - [features] pym = ["pyo3"] [target.'cfg(target_family = "unix")'.dependencies] -nix = "0.24.1" +nix = { version = "0.30.1", features = [ "signal" ] } diff --git a/README.md b/README.md index d7a3517..315587a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ # leetcode-cli -![Rust](https://github.com/clearloop/leetcode-cli/workflows/Rust/badge.svg) + +![Rust](https://github.com/clearloop/leetcode-cli/workflows/leetcode-cli/badge.svg) [![crate](https://img.shields.io/crates/v/leetcode-cli.svg)](https://crates.io/crates/leetcode-cli) [![doc](https://img.shields.io/badge/current-docs-brightgreen.svg)](https://docs.rs/leetcode-cli/) [![downloads](https://img.shields.io/crates/d/leetcode-cli.svg)](https://crates.io/crates/leetcode-cli) -[![gitter](https://img.shields.io/gitter/room/odditypark/leetcode-cli)](https://gitter.im/Odditypark/leetcode-cli) +[![telegram](https://img.shields.io/badge/telegram-blue?logo=telegram)](https://t.me/+U_5si6PhWykxZTI1) [![LICENSE](https://img.shields.io/crates/l/leetcode-cli.svg)](https://choosealicense.com/licenses/mit/) ## Installing ```sh # Required dependencies: -# +# # gcc # libssl-dev # libdbus-1-dev @@ -19,12 +20,33 @@ cargo install leetcode-cli ``` +
+Shell completions + +For Bash and Zsh (by default picks up `$SHELL` from environment) + +```sh +eval "$(leetcode completions)" +``` + +Copy the line above to `.bash_profile` or `.zshrc` + +You may also obtain specific shell configuration using. + +```sh +leetcode completions fish +``` + +If no argument is provided, the shell is inferred from the `SHELL` environment variable. + +
+ ## Usage -**Make sure you have logged in to `leetcode.com` with `Chrome`**. See [Cookies](#cookies) for why you need to do this first. +**Make sure you have logged in to `leetcode.com` with `Firefox`**. See [Cookies](#cookies) for why you need to do this first. ```sh -leetcode 0.3.10 +leetcode 0.4.0 May the Code be with You 👻 USAGE: @@ -42,18 +64,142 @@ SUBCOMMANDS: list List problems [aliases: l] pick Pick a problem [aliases: p] stat Show simple chart about submissions [aliases: s] - test Edit question by id [aliases: t] + test Test question by id [aliases: t] help Prints this message or the help of the given subcommand(s) ``` ## Example -For example, given this config (can be found in `~/.leetcode/leetcode.toml`, it can be generated automatically with command: `leetcode list` if you are a new user): +To configure leetcode-cli, create a file at `~/.leetcode/leetcode.toml`): ```toml [code] -lang = "rust" -editor = "emacs" +editor = 'emacs' +# Optional parameter +editor_args = ['-nw'] +# Optional environment variables (ex. [ "XDG_DATA_HOME=...", "XDG_CONFIG_HOME=...", "XDG_STATE_HOME=..." ]) +editor_envs = [] +lang = 'rust' +edit_code_marker = false +start_marker = "" +end_marker = "" +# if include problem description +comment_problem_desc = false +# comment syntax +comment_leading = "" +test = true + +[cookies] +csrf = '' +session = '' +# leetcode.com or leetcode.cn +site = "leetcode.com" + +[storage] +cache = 'Problems' +code = 'code' +root = '~/.leetcode' +scripts = 'scripts' +``` + +
+ Configuration Explanation + +```toml +[code] +editor = 'emacs' +# Optional parameter +editor_args = ['-nw'] +# Optional environment variables (ex. [ "XDG_DATA_HOME=...", "XDG_CONFIG_HOME=...", "XDG_STATE_HOME=..." ]) +editor_envs = [] +lang = 'rust' +edit_code_marker = true +start_marker = "start_marker" +end_marker = "end_marker" +# if include problem description +comment_problem_desc = true +# comment syntax +comment_leading = "//" +test = true + +[cookies] +csrf = '' +session = '' + +[storage] +cache = 'Problems' +code = 'code' +root = '~/.leetcode' +scripts = 'scripts' +``` + +If we change the configuration as shown previously, we will get the following code after `leetcode edit 15`. + +```rust +// Category: algorithms +// Level: Medium +// Percent: 32.90331% + +// Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0. +// +// Notice that the solution set must not contain duplicate triplets. +// +//   +// Example 1: +// +// Input: nums = [-1,0,1,2,-1,-4] +// Output: [[-1,-1,2],[-1,0,1]] +// Explanation: +// nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0. +// nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0. +// nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0. +// The distinct triplets are [-1,0,1] and [-1,-1,2]. +// Notice that the order of the output and the order of the triplets does not matter. +// +// +// Example 2: +// +// Input: nums = [0,1,1] +// Output: [] +// Explanation: The only possible triplet does not sum up to 0. +// +// +// Example 3: +// +// Input: nums = [0,0,0] +// Output: [[0,0,0]] +// Explanation: The only possible triplet sums up to 0. +// +// +//   +// Constraints: +// +// +// 3 <= nums.length <= 3000 +// -10⁵ <= nums[i] <= 10⁵ +// + +// start_marker +impl Solution { +pub fn three_sum(nums: Vec) -> Vec> { + + } + +} +// end_marker + +``` + +
+ +
+ +Some linting tools/lsps will throw errors unless the necessary libraries are imported. leetcode-cli can generate this boilerplate automatically if the `inject_before` key is set. Similarly, if you want to test out your code locally, you can automate that with `inject_after`. For c++ this might look something like: + +```toml +[code] +inject_before = ["#include", "using namespace std;"] +inject_after = ["int main() {\n Solution solution;\n\n}"] ``` #### 1. pick @@ -62,6 +208,10 @@ editor = "emacs" leetcode pick 1 ``` +```sh +leetcode pick --name "Two Sum" +``` + ```sh [1] Two Sum is on the run... @@ -142,7 +292,8 @@ leetcode exec 1 ## Cookies -The cookie plugin of leetcode-cli can work on OSX and [Linux][#1]. **If you are on a different platform, there are problems with caching the cookies**, you can manually input your LeetCode Cookies to the configuration file. +The cookie plugin of leetcode-cli can work on OSX and [Linux][#1]. **If you are on a different platform, there are problems with caching the cookies**, +you can manually input your LeetCode Cookies to the configuration file. ```toml [cookies] @@ -150,26 +301,47 @@ csrf = "..." session = "..." ``` -For Example, using Chrome (after logging in to LeetCode): - +For Example, using Firefox (after logging in to LeetCode): #### Step 1 -Open Chrome and navigate to the link below: +Open Firefox, press F12, and click `Storage` tab. -```sh -chrome://settings/cookies/detail?site=leetcode.com -``` +#### Step 2 + +Expand `Cookies` tab on the left and select https://leetcode.com. #### Step 2 -Copy `Content` from `LEETCODE_SESSION` and `csrftoken` to `session` and `csrf` in your configuration file, respectively: +Copy `Value` from `LEETCODE_SESSION` and `csrftoken` to `session` and `csrf` in your configuration file, respectively: + +```toml +[cookies] +csrf = '' +session = '' +``` + +#### Environment variables + +The cookies can also be overridden by environment variables, which might be useful to exclude the sensitive information from the configuration file `leetcode.toml`. To do this, you can leave the `csrf` and `session` fields empty in the configuration file and override cookies settings via the environment variables `LEETCODE_CSRF`, `LEETCODE_SESSION`, and `LEETCODE_SITE`: + ```toml [cookies] -csrf = "${csrftoken}" -session = "${LEETCODE_SESSION}" +csrf = '' +session = '' +site = 'leetcode.com' +``` + +Then set the environment variables: + +```bash +export LEETCODE_CSRF='' +export LEETCODE_SESSION='' +export LEETCODE_SITE='leetcode.cn' # or 'leetcode.com' ``` +Note that `cookies.site` in still required in the `leetcode.toml` to avoid exception during configuration file parsing, but can be overridden using environment variables. + ## Programmable If you want to filter LeetCode questions using custom Python scripts, add the following to your the configuration file: @@ -187,13 +359,13 @@ import json; def plan(sps, stags): ## - # `print` in python is supported, - # if you want to know the data structures of these two args, + # `print` in python is supported, + # if you want to know the data structures of these two args, # just print them ## problems = json.loads(sps) tags = json.loads(stags) - + ret = [] tm = {} for tag in tags: @@ -213,17 +385,15 @@ Then run `list` with the filter that you just wrote: leetcode list -p plan1 ``` -And that's it! Enjoy! +That's it! Enjoy! +## Contributions -## PR - -[PRs][pr] are more than welcome! +Feel free to add your names and emails in the `authors` field of `Cargo.toml` ! ## LICENSE MIT - [pr]: https://github.com/clearloop/leetcode-cli/pulls [#1]: https://github.com/clearloop/leetcode-cli/issues/1 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/flake.lock b/flake.lock index 02323b6..8d2d0df 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,32 @@ { "nodes": { + "naersk": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1721727458, + "narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=", + "owner": "nix-community", + "repo": "naersk", + "rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "naersk", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1665634984, - "narHash": "sha256-zwXeMc96BD9iFxSB/SLr3dI8iYpqM+seX9qy6bGV+cw=", + "lastModified": 1728538411, + "narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cfea568da97a2668ef3cb3fc42eaacfb0e706807", + "rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221", "type": "github" }, "original": { @@ -18,17 +38,57 @@ }, "root": { "inputs": { + "naersk": "naersk", "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", "utils": "utils" } }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1728700003, + "narHash": "sha256-Ox1pvEHxLK6lAdaKQW21Zvk65SPDag+cD8YA444R/og=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "fc1e58ebabe0cef4442eedea07556ff0c9eafcfe", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index d9ce06a..936e777 100644 --- a/flake.nix +++ b/flake.nix @@ -1,41 +1,72 @@ { description = "Leet your code in command-line."; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - inputs.utils.url = "github:numtide/flake-utils"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + utils.url = "github:numtide/flake-utils"; - outputs = { self, nixpkgs, utils, ... }: + naersk = { + url = "github:nix-community/naersk"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + nixpkgs, + utils, + naersk, + rust-overlay, + ... + }: utils.lib.eachDefaultSystem (system: let - pkgs = import nixpkgs { inherit system; }; + overlays = [ (import rust-overlay) ]; + + pkgs = (import nixpkgs) { + inherit system overlays; + }; + + toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + + naersk' = pkgs.callPackage naersk { + cargo = toolchain; + rustc = toolchain; + clippy = toolchain; + }; nativeBuildInputs = with pkgs; [ pkg-config ]; + darwinBuildInputs = pkgs.lib.optionals pkgs.stdenv.isDarwin [ + pkgs.darwin.apple_sdk.frameworks.Security + pkgs.darwin.apple_sdk.frameworks.SystemConfiguration + ]; + buildInputs = with pkgs; [ openssl dbus sqlite - ] ++ lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.Security ]; + ] ++ darwinBuildInputs; - - package = with pkgs; rustPlatform.buildRustPackage rec { + package = naersk'.buildPackage rec { pname = "leetcode-cli"; - version = "0.3.11"; - src = fetchCrate { - inherit pname version; - sha256 = "sha256-DHtIhiRPRGuO6Rf1d9f8r0bMOHqAaJleUvYNyPiX6mc="; - }; - cargoSha256 = "sha256-Suk/nQ+JcoD9HO9x1lYp+p4qx0DZ9dt0p5jPz0ZQB+k="; + version = "git"; + + src = ./.; + doCheck = true; # run `cargo test` on build inherit buildInputs nativeBuildInputs; - # a nightly compiler is required unless we use this cheat code. - RUSTC_BOOTSTRAP = 0; + buildNoDefaultFeatures = true; - # CFG_RELEASE = "${rustPlatform.rust.rustc.version}-stable"; - CFG_RELEASE_CHANNEL = "stable"; + buildFeatures = "git"; meta = with pkgs.lib; { description = "Leet your code in command-line."; @@ -44,9 +75,16 @@ maintainers = with maintainers; [ congee ]; mainProgram = "leetcode"; }; + + # Env vars + # a nightly compiler is required unless we use this cheat code. + RUSTC_BOOTSTRAP = 0; + + # CFG_RELEASE = "${rustPlatform.rust.rustc.version}-stable"; + CFG_RELEASE_CHANNEL = "stable"; }; in - { + { defaultPackage = package; overlay = final: prev: { leetcode-cli = package; }; @@ -55,11 +93,7 @@ inherit nativeBuildInputs; buildInputs = buildInputs ++ [ - rustc - cargo - rustfmt - clippy - rust-analyzer + toolchain cargo-edit cargo-bloat cargo-audit diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..2a19081 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,10 @@ +[toolchain] +channel = "stable" +components = [ + "rustc", + "cargo", + "rustfmt", + "clippy", + "rust-analyzer", +] +profile = "minimal" diff --git a/rustfmt.toml b/rustfmt.toml index dcbebe6..79f8a99 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,2 @@ -tab_spaces = 4 \ No newline at end of file +edition = "2021" +tab_spaces = 4 diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 6ec86cb..3250bf6 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -7,7 +7,8 @@ use self::models::*; use self::schemas::{problems::dsl::*, tags::dsl::*}; use self::sql::*; use crate::helper::test_cases_path; -use crate::{cfg, err::Error, plugins::LeetCode}; +use crate::{config::Config, err::Error, plugins::LeetCode}; +use anyhow::anyhow; use colored::Colorize; use diesel::prelude::*; use reqwest::Response; @@ -22,16 +23,13 @@ pub fn conn(p: String) -> SqliteConnection { /// Condition submit or test #[derive(Clone, Debug)] +#[derive(Default)] pub enum Run { Test, + #[default] Submit, } -impl Default for Run { - fn default() -> Self { - Run::Submit - } -} /// Requests if data not download #[derive(Clone)] @@ -45,19 +43,21 @@ impl Cache { /// Clean cache pub fn clean(&self) -> Result<(), Error> { - Ok(std::fs::remove_file(&self.0.conf.storage.cache()?)?) + Ok(std::fs::remove_file(self.0.conf.storage.cache()?)?) } - /// ref to download probems + /// ref to download problems pub async fn update(self) -> Result<(), Error> { self.download_problems().await?; Ok(()) } pub fn update_after_ac(self, rid: i32) -> Result<(), Error> { - let c = conn(self.0.conf.storage.cache()?); + let mut c = conn(self.0.conf.storage.cache()?); let target = problems.filter(id.eq(rid)); - diesel::update(target).set(status.eq("ac")).execute(&c)?; + diesel::update(target) + .set(status.eq("ac")) + .execute(&mut c)?; Ok(()) } @@ -102,23 +102,32 @@ impl Cache { diesel::replace_into(problems) .values(&ps) - .execute(&self.conn()?)?; + .execute(&mut self.conn()?)?; Ok(ps) } /// Get problem pub fn get_problem(&self, rfid: i32) -> Result { - let p: Problem = problems.filter(fid.eq(rfid)).first(&self.conn()?)?; + let p: Problem = problems.filter(fid.eq(rfid)).first(&mut self.conn()?)?; if p.category != "algorithms" { - return Err(Error::FeatureError( - "Not support database and shell questions for now".to_string(), - )); + return Err(anyhow!("No support for database and shell questions yet").into()); } Ok(p) } + /// Get problem from name + pub fn get_problem_id_from_name(&self, problem_name: &String) -> Result { + let p: Problem = problems + .filter(name.eq(problem_name)) + .first(&mut self.conn()?)?; + if p.category != "algorithms" { + return Err(anyhow!("No support for database and shell questions yet").into()); + } + Ok(p.fid) + } + /// Get daily problem pub async fn get_daily_problem_id(&self) -> Result { parser::daily( @@ -134,13 +143,13 @@ impl Cache { /// Get problems from cache pub fn get_problems(&self) -> Result, Error> { - Ok(problems.load::(&self.conn()?)?) + Ok(problems.load::(&mut self.conn()?)?) } /// Get question #[allow(clippy::useless_let_if_seq)] pub async fn get_question(&self, rfid: i32) -> Result { - let target: Problem = problems.filter(fid.eq(rfid)).first(&self.conn()?)?; + let target: Problem = problems.filter(fid.eq(rfid)).first(&mut self.conn()?)?; let ids = match target.level { 1 => target.fid.to_string().green(), @@ -157,9 +166,7 @@ impl Cache { ); if target.category != "algorithms" { - return Err(Error::FeatureError( - "Not support database and shell questions for now".to_string(), - )); + return Err(anyhow!("No support for database and shell questions yet").into()); } let mut rdesc = Question::default(); @@ -190,7 +197,7 @@ impl Cache { let sdesc = serde_json::to_string(&rdesc)?; diesel::update(&target) .set(desc.eq(sdesc)) - .execute(&self.conn()?)?; + .execute(&mut self.conn()?)?; } Ok(rdesc) @@ -201,7 +208,7 @@ impl Cache { let ids: Vec; let rtag = tags .filter(tag.eq(rslug.to_string())) - .first::(&self.conn()?); + .first::(&mut self.conn()?); if let Ok(t) = rtag { trace!("Got {} questions from local cache...", &rslug); ids = serde_json::from_str(&t.refs)?; @@ -222,14 +229,14 @@ impl Cache { diesel::insert_into(tags) .values(&t) - .execute(&self.conn()?)?; + .execute(&mut self.conn()?)?; } Ok(ids) } pub fn get_tags(&self) -> Result, Error> { - Ok(tags.load::(&self.conn()?)?) + Ok(tags.load::(&mut self.conn()?)?) } /// run_code data @@ -239,7 +246,7 @@ impl Cache { rfid: i32, test_case: Option, ) -> Result<(HashMap<&'static str, String>, [String; 2]), Error> { - trace!("pre run code..."); + trace!("pre-run code..."); use crate::helper::code_path; use std::fs::File; use std::io::Read; @@ -307,33 +314,14 @@ impl Cache { json.insert("data_input", test_case); let url = match run { - Run::Test => conf - .sys - .urls - .get("test") - .ok_or(Error::NoneError)? - .replace("$slug", &p.slug), + Run::Test => conf.sys.urls.test(&p.slug), Run::Submit => { json.insert("judge_type", "large".to_string()); - conf.sys - .urls - .get("submit") - .ok_or(Error::NoneError)? - .replace("$slug", &p.slug) + conf.sys.urls.submit(&p.slug) } }; - Ok(( - json, - [ - url, - conf.sys - .urls - .get("problems") - .ok_or(Error::NoneError)? - .replace("$slug", &p.slug), - ], - )) + Ok((json, [url, conf.sys.urls.problem(&p.slug)])) } /// TODO: The real delay @@ -359,15 +347,20 @@ impl Cache { ) -> Result { trace!("Exec problem filter —— Test or Submit"); let (json, [url, refer]) = self.pre_run_code(run.clone(), rfid, test_case).await?; - trace!("Pre run code result {:?}, {:?}, {:?}", json, url, refer); + trace!("Pre-run code result {:#?}, {}, {}", json, url, refer); - let run_res: RunCode = self + let text = self .0 .clone() .run_code(json.clone(), url.clone(), refer.clone()) .await? - .json() // does not require LEETCODE_SESSION (very oddly) + .text() .await?; + + let run_res: RunCode = serde_json::from_str(&text).map_err(|e| { + anyhow!("JSON error: {e}, please double check your session and csrf config.") + })?; + trace!("Run code result {:#?}", run_res); // Check if leetcode accepted the Run request @@ -395,10 +388,10 @@ impl Cache { /// New cache pub fn new() -> Result { - let conf = cfg::locate()?; - let c = conn(conf.storage.cache()?); - diesel::sql_query(CREATE_PROBLEMS_IF_NOT_EXISTS).execute(&c)?; - diesel::sql_query(CREATE_TAGS_IF_NOT_EXISTS).execute(&c)?; + let conf = Config::locate()?; + let mut c = conn(conf.storage.cache()?); + diesel::sql_query(CREATE_PROBLEMS_IF_NOT_EXISTS).execute(&mut c)?; + diesel::sql_query(CREATE_TAGS_IF_NOT_EXISTS).execute(&mut c)?; Ok(Cache(LeetCode::new()?)) } diff --git a/src/cache/models.rs b/src/cache/models.rs index f74cb28..9b28482 100644 --- a/src/cache/models.rs +++ b/src/cache/models.rs @@ -1,4 +1,6 @@ //! Leetcode data models +use unicode_width::UnicodeWidthStr; +use unicode_width::UnicodeWidthChar; use super::schemas::{problems, tags}; use crate::helper::HTML; use colored::Colorize; @@ -7,7 +9,7 @@ use serde_json::Number; /// Tag model #[derive(Clone, Insertable, Queryable, Serialize, Debug)] -#[table_name = "tags"] +#[diesel(table_name = tags)] pub struct Tag { pub tag: String, pub refs: String, @@ -15,7 +17,7 @@ pub struct Tag { /// Problem model #[derive(AsChangeset, Clone, Identifiable, Insertable, Queryable, Serialize, Debug)] -#[table_name = "problems"] +#[diesel(table_name = problems)] pub struct Problem { pub category: String, pub fid: i32, @@ -39,6 +41,7 @@ impl Problem { _ => "Unknown", } } + pub fn desc_comment(&self, conf: &Config) -> String { let mut res = String::new(); let comment_leading = &conf.code.comment_leading; @@ -53,7 +56,7 @@ impl Problem { static DONE: &str = " ✔"; static ETC: &str = "..."; static LOCK: &str = "🔒"; -static NDONE: &str = "✘"; +static NDONE: &str = " ✘"; static SPACE: &str = " "; impl std::fmt::Display for Problem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -97,14 +100,27 @@ impl std::fmt::Display for Problem { } } - if self.name.len() < 60_usize { + let name_width = UnicodeWidthStr::width(self.name.as_str()); + let target_width = 60; + if name_width <= target_width { name.push_str(&self.name); - name.push_str(&SPACE.repeat(60 - &self.name.len())); + name.push_str(&SPACE.repeat(target_width - name_width)); } else { - name.push_str(&self.name[..49]); - name = name.trim_end().to_string(); - name.push_str(ETC); - name.push_str(&SPACE.repeat(60 - name.len())); + // truncate carefully to target width - 3 (because "..." will take some width) + let mut truncated = String::new(); + let mut current_width = 0; + for c in self.name.chars() { + let char_width = UnicodeWidthChar::width(c).unwrap_or(0); + if current_width + char_width > target_width - 3 { + break; + } + truncated.push(c); + current_width += char_width; + } + truncated.push_str(ETC); // add "..." + let truncated_width = UnicodeWidthStr::width(truncated.as_str()); + truncated.push_str(&SPACE.repeat(target_width - truncated_width)); + name = truncated; } level = match self.level { @@ -153,7 +169,7 @@ impl Question { let desc = self.content.render(); let mut res = desc.lines().fold("\n".to_string(), |acc, e| { - acc + " " + conf.code.comment_leading.as_str() + " " + e + "\n" + acc + "" + conf.code.comment_leading.as_str() + " " + e + "\n" }); res += " \n"; @@ -323,22 +339,26 @@ impl std::fmt::Display for VerifyResult { .expect("update ac to cache failed"); // prints - let (mut rp, mut mp) = (0, 0); - if let Some(n) = &self.analyse.runtime_percentile { + let rp = if let Some(n) = &self.analyse.runtime_percentile { if n.is_f64() { - rp = n.as_f64().unwrap_or(0.0) as i64; + n.as_f64().unwrap_or(0.0) as i64 } else { - rp = n.as_i64().unwrap_or(0); + n.as_i64().unwrap_or(0) } - } + } else { + 0 + }; - if let Some(n) = &self.analyse.memory_percentile { + let mp = if let Some(n) = &self.analyse.memory_percentile { if n.is_f64() { - mp = n.as_f64().unwrap_or(0.0) as i64; + n.as_f64().unwrap_or(0.0) as i64 } else { - mp = n.as_i64().unwrap_or(0); + n.as_i64().unwrap_or(0) } - } + } else { + 0 + }; + write!( f, "\n{}{}{}\ diff --git a/src/cache/parser.rs b/src/cache/parser.rs index dbbc87e..329455d 100644 --- a/src/cache/parser.rs +++ b/src/cache/parser.rs @@ -10,9 +10,17 @@ pub fn problem(problems: &mut Vec, v: Value) -> Option<()> { let total_acs = stat.get("total_acs")?.as_f64()? as f32; let total_submitted = stat.get("total_submitted")?.as_f64()? as f32; + let fid_obj = stat.get("frontend_question_id")?; + let fid = match fid_obj.as_i64() { + // Handle on leetcode-com + Some(s) => s as i32, + // Handle on leetcode-cn + None => fid_obj.as_str()?.split(' ').last()?.parse::().ok()?, + }; + problems.push(Problem { category: v.get("category_slug")?.as_str()?.to_string(), - fid: stat.get("frontend_question_id")?.as_i64()? as i32, + fid, id: stat.get("question_id")?.as_i64()? as i32, level: p.get("difficulty")?.as_object()?.get("level")?.as_i64()? as i32, locked: p.get("paid_only")?.as_bool()?, @@ -89,17 +97,20 @@ pub fn tags(v: Value) -> Option> { /// daily parser pub fn daily(v: Value) -> Option { trace!("Parse daily..."); - v.as_object()? - .get("data")? - .as_object()? - .get("activeDailyCodingChallengeQuestion")? - .as_object()? - .get("question")? - .as_object()? - .get("questionFrontendId")? - .as_str()? - .parse() - .ok() + let v_obj = v.as_object()?.get("data")?.as_object()?; + match v_obj.get("activeDailyCodingChallengeQuestion") { + // Handle on leetcode-com + Some(v) => v, + // Handle on leetcode-cn + None => v_obj.get("todayRecord")?.as_array()?.first()?, + } + .as_object()? + .get("question")? + .as_object()? + .get("questionFrontendId")? + .as_str()? + .parse() + .ok() } /// user parser diff --git a/src/cfg.rs b/src/cfg.rs deleted file mode 100644 index 184e3e0..0000000 --- a/src/cfg.rs +++ /dev/null @@ -1,239 +0,0 @@ -//! Soft-link with `config.toml` -//! -//! leetcode-cli will generate a `leetcode.toml` by default, -//! if you wanna change to it, you can: -//! -//! + Edit leetcode.toml at `~/.leetcode/leetcode.toml` directly -//! + Use `leetcode config` to update it -use crate::Error; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs, path::PathBuf}; - -pub const DEFAULT_CONFIG: &str = r##" -# usually you don't wanna change those -[sys] -categories = [ - "algorithms", - "concurrency", - "database", - "shell" -] - -langs = [ - "bash", - "c", - "cpp", - "csharp", - "golang", - "java", - "javascript", - "kotlin", - "mysql", - "php", - "python", - "python3", - "ruby", - "rust", - "scala", - "swift" -] - -[sys.urls] -base = "https://leetcode.com" -graphql = "https://leetcode.com/graphql" -login = "https://leetcode.com/accounts/login/" -problems = "https://leetcode.com/api/problems/$category/" -problem = "https://leetcode.com/problems/$slug/description/" -tag = "https://leetcode.com/tag/$slug/" -test = "https://leetcode.com/problems/$slug/interpret_solution/" -session = "https://leetcode.com/session/" -submit = "https://leetcode.com/problems/$slug/submit/" -submissions = "https://leetcode.com/api/submissions/$slug" -submission = "https://leetcode.com/submissions/detail/$id/" -verify = "https://leetcode.com/submissions/detail/$id/check/" -favorites = "https://leetcode.com/list/api/questions" -favorite_delete = "https://leetcode.com/list/api/questions/$hash/$id" - -[code] -editor = "vim" -lang = "rust" -edit_code_marker = false -comment_problem_desc = false -comment_leading = "///" -start_marker = "@lc code=start" -end_marker = "@lc code=start" -test = true -pick = "${fid}.${slug}" -submission = "${fid}.${slug}.${sid}.${ac}" - -[cookies] -csrf = "" -session = "" - -[storage] -root = "~/.leetcode" -scripts = "scripts" -code = "code" -# absolutely path for the cache, other use root as parent dir -cache = "~/.cache/leetcode" -"##; - -/// Sync with `~/.leetcode/config.toml` -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Config { - pub sys: Sys, - pub code: Code, - pub cookies: Cookies, - pub storage: Storage, -} - -impl Config { - /// Sync new config to config.toml - pub fn sync(&self) -> Result<(), Error> { - let home = dirs::home_dir().ok_or(Error::NoneError)?; - let conf = home.join(".leetcode/leetcode.toml"); - fs::write(conf, toml::ser::to_string_pretty(&self)?)?; - - Ok(()) - } -} - -/// Cookie settings -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Cookies { - pub csrf: String, - pub session: String, -} - -/// System settings, for leetcode api mainly -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Sys { - pub categories: Vec, - pub langs: Vec, - pub urls: HashMap, -} - -/// Leetcode API -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Urls { - pub base: String, - pub graphql: String, - pub login: String, - pub problems: String, - pub problem: String, - pub test: String, - pub session: String, - pub submit: String, - pub submissions: String, - pub submission: String, - pub verify: String, - pub favorites: String, - pub favorite_delete: String, -} - -/// default editor and langs -/// -/// + support editor: [emacs, vim] -/// + support langs: all in config -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Code { - pub editor: String, - #[serde(rename(serialize = "editor-args", deserialize = "editor-args"))] - pub editor_args: Option>, - pub edit_code_marker: bool, - pub start_marker: String, - pub end_marker: String, - pub comment_problem_desc: bool, - pub comment_leading: String, - pub test: bool, - pub lang: String, - pub pick: String, - pub submission: String, -} - -/// Locate code files -/// -/// + cache -> the path to cache -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Storage { - cache: String, - code: String, - root: String, - scripts: Option, -} - -impl Storage { - /// convert root path - pub fn root(&self) -> Result { - let home = dirs::home_dir() - .ok_or(Error::NoneError)? - .to_string_lossy() - .to_string(); - let path = self.root.replace('~', &home); - Ok(path) - } - - /// get cache path - pub fn cache(&self) -> Result { - let home = dirs::home_dir() - .ok_or(Error::NoneError)? - .to_string_lossy() - .to_string(); - let path = PathBuf::from(self.cache.replace('~', &home)); - if !path.is_dir() { - info!("Generate cache dir at {:?}.", &path); - fs::DirBuilder::new().recursive(true).create(&path)?; - } - - Ok(path.join("Problems").to_string_lossy().to_string()) - } - - /// get code path - pub fn code(&self) -> Result { - let root = &self.root()?; - let p = PathBuf::from(root).join(&self.code); - if !PathBuf::from(&p).exists() { - fs::create_dir(&p)? - } - - Ok(p.to_string_lossy().to_string()) - } - - /// get scripts path - pub fn scripts(mut self) -> Result { - let root = &self.root()?; - if self.scripts.is_none() { - let tmp = toml::from_str::(DEFAULT_CONFIG)?; - self.scripts = Some(tmp.storage.scripts.ok_or(Error::NoneError)?); - } - - let p = PathBuf::from(root).join(&self.scripts.ok_or(Error::NoneError)?); - if !PathBuf::from(&p).exists() { - std::fs::create_dir(&p)? - } - - Ok(p.to_string_lossy().to_string()) - } -} - -/// Locate lc's config file -pub fn locate() -> Result { - let conf = root()?.join("leetcode.toml"); - if !conf.is_file() { - fs::write(&conf, &DEFAULT_CONFIG[1..])?; - } - - let s = fs::read_to_string(&conf)?; - Ok(toml::from_str::(&s)?) -} - -/// Get root path of leetcode-cli -pub fn root() -> Result { - let dir = dirs::home_dir().ok_or(Error::NoneError)?.join(".leetcode"); - if !dir.is_dir() { - info!("Generate root dir at {:?}.", &dir); - fs::DirBuilder::new().recursive(true).create(&dir)?; - } - - Ok(dir) -} diff --git a/src/cli.rs b/src/cli.rs index 90d1756..63107a2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,13 +1,13 @@ //! Clap Commanders use crate::{ cmds::{ - Command, DataCommand, EditCommand, ExecCommand, ListCommand, PickCommand, StatCommand, - TestCommand, + completion_handler, Command, CompletionCommand, DataCommand, EditCommand, ExecCommand, + ListCommand, PickCommand, StatCommand, TestCommand, }, err::Error, flag::{Debug, Flag}, }; -use clap::{crate_name, crate_version}; +use clap::crate_version; use log::LevelFilter; /// This should be called before calling any cli method or printing any output. @@ -26,7 +26,8 @@ pub fn reset_signal_pipe_handler() { /// Get matches pub async fn main() -> Result<(), Error> { reset_signal_pipe_handler(); - let m = clap::Command::new(crate_name!()) + + let mut cmd = clap::Command::new("leetcode") .version(crate_version!()) .about("May the Code be with You 👻") .subcommands(vec![ @@ -37,10 +38,12 @@ pub async fn main() -> Result<(), Error> { PickCommand::usage().display_order(5), StatCommand::usage().display_order(6), TestCommand::usage().display_order(7), + CompletionCommand::usage().display_order(8), ]) .arg(Debug::usage()) - .arg_required_else_help(true) - .get_matches(); + .arg_required_else_help(true); + + let m = cmd.clone().get_matches(); if m.get_flag("debug") { Debug::handler()?; @@ -59,6 +62,7 @@ pub async fn main() -> Result<(), Error> { Some(("pick", sub_m)) => Ok(PickCommand::handler(sub_m).await?), Some(("stat", sub_m)) => Ok(StatCommand::handler(sub_m).await?), Some(("test", sub_m)) => Ok(TestCommand::handler(sub_m).await?), + Some(("completions", sub_m)) => Ok(completion_handler(sub_m, &mut cmd)?), _ => Err(Error::MatchError), } } diff --git a/src/cmds/completions.rs b/src/cmds/completions.rs new file mode 100644 index 0000000..bc675de --- /dev/null +++ b/src/cmds/completions.rs @@ -0,0 +1,59 @@ +//! Completions command + +use super::Command; +use crate::err::Error; +use async_trait::async_trait; +use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; +use clap_complete::{generate, Generator, Shell}; + +/// Abstract shell completions command +/// +/// ```sh +/// Generate shell Completions + +/// USAGE: +/// leetcode completions + +/// ARGUMENTS: +/// [possible values: bash, elvish, fish, powershell, zsh] +/// ``` +pub struct CompletionCommand; + +#[async_trait] +impl Command for CompletionCommand { + /// `pick` usage + fn usage() -> ClapCommand { + ClapCommand::new("completions") + .about("Generate shell Completions") + .visible_alias("c") + .arg( + Arg::new("shell") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(Shell)), + ) + } + + async fn handler(_m: &ArgMatches) -> Result<(), Error> { + // defining custom handler to print the completions. Handler method signature limits taking + // other params. We need &ArgMatches and &mut ClapCommand to generate completions. + println!("Don't use this handler. Does not implement the functionality to print completions. Use completions_handler() below."); + Ok(()) + } +} + +fn get_completions_string(gen: G, cmd: &mut ClapCommand) -> Result { + let mut v: Vec = Vec::new(); + let name = cmd.get_name().to_string(); + generate(gen, cmd, name, &mut v); + Ok(String::from_utf8(v)?) +} + +pub fn completion_handler(m: &ArgMatches, cmd: &mut ClapCommand) -> Result<(), Error> { + let shell = *m.get_one::("shell").unwrap_or( + // if shell value is not provided try to get from the environment + &Shell::from_env().ok_or(Error::MatchError)?, + ); + let completions = get_completions_string(shell, cmd)?; + println!("{}", completions); + Ok(()) +} diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index f7e490b..f611d6c 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -1,8 +1,10 @@ //! Edit command use super::Command; -use crate::Error; +use crate::{Error, Result}; +use anyhow::anyhow; use async_trait::async_trait; -use clap::{Arg, ArgMatches, Command as ClapCommand}; +use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command as ClapCommand}; +use std::collections::HashMap; /// Abstract `edit` command /// @@ -27,7 +29,7 @@ impl Command for EditCommand { /// `edit` usage fn usage() -> ClapCommand { ClapCommand::new("edit") - .about("Edit question by id") + .about("Edit question") .visible_alias("e") .arg( Arg::new("lang") @@ -39,21 +41,46 @@ impl Command for EditCommand { .arg( Arg::new("id") .num_args(1) - .required(true) .value_parser(clap::value_parser!(i32)) .help("question id"), ) + .arg( + Arg::new("daily") + .short('d') + .long("daily") + .help("Edit today's daily challenge") + .action(ArgAction::SetTrue), + ) + .group( + ArgGroup::new("question-id") + .args(["id", "daily"]) + .multiple(false) + .required(true), + ) } /// `edit` handler - async fn handler(m: &ArgMatches) -> Result<(), crate::Error> { + async fn handler(m: &ArgMatches) -> Result<()> { use crate::{cache::models::Question, Cache}; use std::fs::File; use std::io::Write; use std::path::Path; - let id = *m.get_one::("id").ok_or(Error::NoneError)?; let cache = Cache::new()?; + + let daily = m.get_one::("daily").unwrap_or(&false); + let daily_id = if *daily { + Some(cache.get_daily_problem_id().await?) + } else { + None + }; + + let id = m + .get_one::("id") + .copied() + .or(daily_id) + .ok_or(Error::NoneError)?; + let problem = cache.get_problem(id)?; let mut conf = cache.to_owned().0.conf; @@ -93,6 +120,11 @@ impl Command for EditCommand { file_code.write_all(p_desc_comment.as_bytes())?; file_code.write_all(question_desc.as_bytes())?; } + if let Some(inject_before) = &conf.code.inject_before { + for line in inject_before { + file_code.write_all((line.to_string() + "\n").as_bytes())?; + } + } if conf.code.edit_code_marker { file_code.write_all( (conf.code.comment_leading.clone() @@ -112,6 +144,11 @@ impl Command for EditCommand { .as_bytes(), )?; } + if let Some(inject_after) = &conf.code.inject_after { + for line in inject_after { + file_code.write_all((line.to_string() + "\n").as_bytes())?; + } + } if test_flag { let mut file_tests = File::create(&test_path)?; @@ -123,13 +160,14 @@ impl Command for EditCommand { // if language is not found in the list of supported languges clean up files if !flag { std::fs::remove_file(&path)?; + if test_flag { std::fs::remove_file(&test_path)?; } - return Err(crate::Error::FeatureError(format!( - "This question doesn't support {}, please try another", - &lang - ))); + + return Err( + anyhow!("This question doesn't support {lang}, please try another").into(), + ); } } @@ -140,7 +178,7 @@ impl Command for EditCommand { // ```toml // [code] // editor = "emacsclient" - // editor-args = [ "-n", "-s", "doom" ] + // editor_args = [ "-n", "-s", "doom" ] // ``` // // ```rust @@ -151,8 +189,36 @@ impl Command for EditCommand { args.extend_from_slice(&editor_args); } + // Set environment variables for editor + // + // for example: + // + // ```toml + // [code] + // editor = "nvim" + // editor_envs = [ "XDG_DATA_HOME=...", "XDG_CONFIG_HOME=...", "XDG_STATE_HOME=..." ] + // ``` + // + // ```rust + // Command::new("nvim").envs(&[ ("XDG_DATA_HOME", "..."), ("XDG_CONFIG_HOME", "..."), ("XDG_STATE_HOME", "..."), ]); + // ``` + let mut envs: HashMap = Default::default(); + if let Some(editor_envs) = &conf.code.editor_envs { + for env in editor_envs.iter() { + let parts: Vec<&str> = env.split('=').collect(); + if parts.len() == 2 { + let name = parts[0].trim(); + let value = parts[1].trim(); + envs.insert(name.to_string(), value.to_string()); + } else { + return Err(anyhow!("Invalid editor environment variable: {env}").into()); + } + } + } + args.push(path); std::process::Command::new(conf.code.editor) + .envs(envs) .args(args) .status()?; Ok(()) diff --git a/src/cmds/exec.rs b/src/cmds/exec.rs index f25f9b4..d6538f8 100644 --- a/src/cmds/exec.rs +++ b/src/cmds/exec.rs @@ -1,8 +1,8 @@ //! Exec command use super::Command; -use crate::Error; +use crate::{Error, Result}; use async_trait::async_trait; -use clap::{Arg, ArgMatches, Command as ClapCommand}; +use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command as ClapCommand}; /// Abstract Exec Command /// @@ -36,14 +36,40 @@ impl Command for ExecCommand { .value_parser(clap::value_parser!(i32)) .help("question id"), ) + .arg( + Arg::new("daily") + .short('d') + .long("daily") + .help("Exec today's daily challenge") + .action(ArgAction::SetTrue), + ) + .group( + ArgGroup::new("question-id") + .args(["id", "daily"]) + .multiple(false) + .required(true), + ) } /// `exec` handler - async fn handler(m: &ArgMatches) -> Result<(), crate::Error> { + async fn handler(m: &ArgMatches) -> Result<()> { use crate::cache::{Cache, Run}; - let id: i32 = *m.get_one::("id").ok_or(Error::NoneError)?; let cache = Cache::new()?; + + let daily = m.get_one::("daily").unwrap_or(&false); + let daily_id = if *daily { + Some(cache.get_daily_problem_id().await?) + } else { + None + }; + + let id = m + .get_one::("id") + .copied() + .or(daily_id) + .ok_or(Error::NoneError)?; + let res = cache.exec_problem(id, Run::Submit, None).await?; println!("{}", res); diff --git a/src/cmds/list.rs b/src/cmds/list.rs index f18a953..ae6df47 100644 --- a/src/cmds/list.rs +++ b/src/cmds/list.rs @@ -13,9 +13,9 @@ //! -V, --version Prints version information //! //! OPTIONS: -//! -c, --category Fliter problems by category name +//! -c, --category Filter problems by category name //! [algorithms, database, shell, concurrency] -//! -q, --query Fliter questions by conditions: +//! -q, --query Filter questions by conditions: //! Uppercase means negative //! e = easy E = m+h //! m = medium M = e+h @@ -42,15 +42,15 @@ use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; /// ## handler /// + try to request cache /// + prints the list -/// + if chache doesn't exist, download problems list +/// + if cache doesn't exist, download problems list /// + ... pub struct ListCommand; -static CATEGORY_HELP: &str = r#"Fliter problems by category name +static CATEGORY_HELP: &str = r#"Filter problems by category name [algorithms, database, shell, concurrency] "#; -static QUERY_HELP: &str = r#"Fliter questions by conditions: +static QUERY_HELP: &str = r#"Filter questions by conditions: Uppercase means negative e = easy E = m+h m = medium M = e+h @@ -61,7 +61,7 @@ s = starred S = not starred"#; static LIST_AFTER_HELP: &str = r#"EXAMPLES: leetcode list List all questions - leetcode list array List questions that has "array" in name + leetcode list array List questions that has "array" in name, and this is letter non-sensitive leetcode list -c database List questions that in database category leetcode list -q eD List questions that with easy level and not done leetcode list -t linked-list List questions that under tag "linked-list" @@ -183,8 +183,7 @@ impl Command for ListCommand { let num_range: Vec = m .get_many::("range") .ok_or(Error::NoneError)? - .map(|id| *id) - .into_iter() + .copied() .collect(); ps.retain(|x| num_range[0] <= x.fid && x.fid <= num_range[1]); } diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index 7bf957f..1124532 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -24,6 +24,7 @@ pub trait Command { async fn handler(m: &ArgMatches) -> Result<(), Error>; } +mod completions; mod data; mod edit; mod exec; @@ -31,6 +32,7 @@ mod list; mod pick; mod stat; mod test; +pub use completions::{completion_handler, CompletionCommand}; pub use data::DataCommand; pub use edit::EditCommand; pub use exec::ExecCommand; diff --git a/src/cmds/pick.rs b/src/cmds/pick.rs index 9fc136f..aba8166 100644 --- a/src/cmds/pick.rs +++ b/src/cmds/pick.rs @@ -1,5 +1,6 @@ //! Pick command use super::Command; +use crate::cache::models::Problem; use crate::err::Error; use async_trait::async_trait; use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; @@ -17,7 +18,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; /// -V, --version Prints version information /// /// OPTIONS: -/// -q, --query Fliter questions by conditions: +/// -q, --query Filter questions by conditions: /// Uppercase means negative /// e = easy E = m+h /// m = medium M = e+h @@ -31,7 +32,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand}; /// ``` pub struct PickCommand; -static QUERY_HELP: &str = r#"Fliter questions by conditions: +static QUERY_HELP: &str = r#"Filter questions by conditions: Uppercase means negative e = easy E = m+h m = medium M = e+h @@ -47,6 +48,14 @@ impl Command for PickCommand { ClapCommand::new("pick") .about("Pick a problem") .visible_alias("p") + .arg( + Arg::new("name") + .short('n') + .long("name") + .value_parser(clap::value_parser!(String)) + .help("Problem name") + .num_args(1), + ) .arg( Arg::new("id") .value_parser(clap::value_parser!(i32)) @@ -121,21 +130,36 @@ impl Command for PickCommand { crate::helper::filter(&mut problems, query.to_string()); } - let daily_id = if m.contains_id("daily") { + let daily = m.get_one::("daily").unwrap_or(&false); + let daily_id = if *daily { Some(cache.get_daily_problem_id().await?) } else { None }; - let fid = m - .get_one::("id") - .map(|id| *id) - .or(daily_id) - .unwrap_or_else(|| { - // Pick random without specify id - let problem = &problems[rand::thread_rng().gen_range(0..problems.len())]; - problem.fid - }); + let fid = match m.contains_id("name") { + // check for name specified, or closest name + true => { + match m.get_one::("name") { + Some(quesname) => closest_named_problem(&problems, quesname).unwrap_or(1), + None => { + // Pick random without specify id + let problem = &problems[rand::rng().random_range(0..problems.len())]; + problem.fid + } + } + } + false => { + m.get_one::("id") + .copied() + .or(daily_id) + .unwrap_or_else(|| { + // Pick random without specify id + let problem = &problems[rand::rng().random_range(0..problems.len())]; + problem.fid + }) + } + }; let r = cache.get_question(fid).await; @@ -143,7 +167,7 @@ impl Command for PickCommand { Ok(q) => println!("{}", q.desc()), Err(e) => { eprintln!("{:?}", e); - if let Error::NetworkError(_) = e { + if let Error::Reqwest(_) = e { Self::handler(m).await?; } } @@ -152,3 +176,69 @@ impl Command for PickCommand { Ok(()) } } + +// Returns the closest problem according to a scoring algorithm +// taking into account both the longest common subsequence and the size +// problem string (to compensate for smaller strings having smaller lcs). +// Returns None if there are no problems in the problem list +fn closest_named_problem(problems: &Vec, lookup_name: &str) -> Option { + let max_name_size: usize = problems.iter().map(|p| p.name.len()).max()?; + // Init table to the max name length of all the problems to share + // the same table allocation + let mut table: Vec = vec![0; (max_name_size + 1) * (lookup_name.len() + 1)]; + + // this is guaranteed because of the earlier max None propegation + assert!(!problems.is_empty()); + let mut max_score = 0; + let mut current_problem = &problems[0]; + for problem in problems { + // In case bug becomes bugged, always return the matching string + if problem.name == lookup_name { + return Some(problem.fid); + } + + let this_lcs = longest_common_subsequence(&mut table, &problem.name, lookup_name); + let this_score = this_lcs * (max_name_size - problem.name.len()); + + if this_score > max_score { + max_score = this_score; + current_problem = problem; + } + } + + Some(current_problem.fid) +} + +// Longest commong subsequence DP approach O(nm) space and time. Table must be at least +// (text1.len() + 1) * (text2.len() + 1) length or greater and is mutated every call +fn longest_common_subsequence(table: &mut [usize], text1: &str, text2: &str) -> usize { + assert!(table.len() >= (text1.len() + 1) * (text2.len() + 1)); + let height: usize = text1.len() + 1; + let width: usize = text2.len() + 1; + + // initialize base cases to 0 + for i in 0..height { + table[i * width + (width - 1)] = 0; + } + for j in 0..width { + table[((height - 1) * width) + j] = 0; + } + + let mut i: usize = height - 1; + let mut j: usize; + for c0 in text1.chars().rev() { + i -= 1; + j = width - 1; + for c1 in text2.chars().rev() { + j -= 1; + if c0.to_lowercase().next() == c1.to_lowercase().next() { + table[i * width + j] = 1 + table[(i + 1) * width + j + 1]; + } else { + let a = table[(i + 1) * width + j]; + let b = table[i * width + j + 1]; + table[i * width + j] = std::cmp::max(a, b); + } + } + } + table[0] +} diff --git a/src/cmds/stat.rs b/src/cmds/stat.rs index 913064a..4b463da 100644 --- a/src/cmds/stat.rs +++ b/src/cmds/stat.rs @@ -81,7 +81,7 @@ impl Command for StatCommand { ); // lines - for (i, l) in vec![(easy, easy_ac), (medium, medium_ac), (hard, hard_ac)] + for (i, l) in [(easy, easy_ac), (medium, medium_ac), (hard, hard_ac)] .iter() .enumerate() { diff --git a/src/cmds/test.rs b/src/cmds/test.rs index af72014..6d36920 100644 --- a/src/cmds/test.rs +++ b/src/cmds/test.rs @@ -1,8 +1,8 @@ //! Test command use super::Command; -use crate::Error; +use crate::{Error, Result}; use async_trait::async_trait; -use clap::{Arg, ArgMatches, Command as ClapCommand}; +use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command as ClapCommand}; /// Abstract Test Command /// @@ -27,12 +27,11 @@ impl Command for TestCommand { /// `test` usage fn usage() -> ClapCommand { ClapCommand::new("test") - .about("Test question by id") + .about("Test a question") .visible_alias("t") .arg( Arg::new("id") .num_args(1) - .required(true) .value_parser(clap::value_parser!(i32)) .help("question id"), ) @@ -42,18 +41,45 @@ impl Command for TestCommand { .required(false) .help("custom testcase"), ) + .arg( + Arg::new("daily") + .short('d') + .long("daily") + .help("Test today's daily challenge") + .action(ArgAction::SetTrue), + ) + .group( + ArgGroup::new("question-id") + .args(["id", "daily"]) + .multiple(false) + .required(true), + ) } /// `test` handler - async fn handler(m: &ArgMatches) -> Result<(), Error> { + async fn handler(m: &ArgMatches) -> Result<()> { use crate::cache::{Cache, Run}; - let id: i32 = *m.get_one::("id").ok_or(Error::NoneError)?; + + let cache = Cache::new()?; + + let daily = m.get_one::("daily").unwrap_or(&false); + let daily_id = if *daily { + Some(cache.get_daily_problem_id().await?) + } else { + None + }; + + let id = m + .get_one::("id") + .copied() + .or(daily_id) + .ok_or(Error::NoneError)?; + let testcase = m.get_one::("testcase"); let case_str: Option = match testcase { Some(case) => Option::from(case.replace("\\n", "\n")), _ => None, }; - let cache = Cache::new()?; let res = cache.exec_problem(id, Run::Test, case_str).await?; println!("{}", res); diff --git a/src/config/code.rs b/src/config/code.rs new file mode 100644 index 0000000..b842657 --- /dev/null +++ b/src/config/code.rs @@ -0,0 +1,63 @@ +//! Code in config +use serde::{Deserialize, Serialize}; + +fn default_pick() -> String { + "${fid}.${slug}".into() +} + +fn default_submission() -> String { + "${fid}.${slug}.${sid}.${ac}".into() +} + +/// Code config +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Code { + #[serde(default)] + pub editor: String, + #[serde(rename(serialize = "editor-args"), alias = "editor-args", default)] + pub editor_args: Option>, + #[serde(rename(serialize = "editor-envs"), alias = "editor-envs", default)] + pub editor_envs: Option>, + #[serde(default, skip_serializing)] + pub edit_code_marker: bool, + #[serde(default, skip_serializing)] + pub start_marker: String, + #[serde(default, skip_serializing)] + pub end_marker: String, + #[serde(rename(serialize = "inject_before"), alias = "inject_before", default)] + pub inject_before: Option>, + #[serde(rename(serialize = "inject_after"), alias = "inject_after", default)] + pub inject_after: Option>, + #[serde(default, skip_serializing)] + pub comment_problem_desc: bool, + #[serde(default, skip_serializing)] + pub comment_leading: String, + #[serde(default, skip_serializing)] + pub test: bool, + pub lang: String, + #[serde(default = "default_pick", skip_serializing)] + pub pick: String, + #[serde(default = "default_submission", skip_serializing)] + pub submission: String, +} + +impl Default for Code { + fn default() -> Self { + Self { + editor: "vim".into(), + editor_args: None, + editor_envs: None, + edit_code_marker: false, + start_marker: "".into(), + end_marker: "".into(), + inject_before: None, + inject_after: None, + comment_problem_desc: false, + comment_leading: "".into(), + test: true, + lang: "rust".into(), + pick: "${fid}.${slug}".into(), + submission: "${fid}.${slug}.${sid}.${ac}".into(), + } + } +} diff --git a/src/config/cookies.rs b/src/config/cookies.rs new file mode 100644 index 0000000..8492780 --- /dev/null +++ b/src/config/cookies.rs @@ -0,0 +1,88 @@ +//! Cookies in config +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum LeetcodeSite { + #[serde(rename = "leetcode.com")] + LeetcodeCom, + #[serde(rename = "leetcode.cn")] + LeetcodeCn, +} + +impl FromStr for LeetcodeSite { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "leetcode.com" => Ok(LeetcodeSite::LeetcodeCom), + "leetcode.cn" => Ok(LeetcodeSite::LeetcodeCn), + _ => Err("Invalid site key".to_string()), + } + } +} + +impl Display for LeetcodeSite { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + LeetcodeSite::LeetcodeCom => "leetcode.com", + LeetcodeSite::LeetcodeCn => "leetcode.cn", + }; + + write!(f, "{s}") + } +} + +/// Cookies settings +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Cookies { + pub csrf: String, + pub session: String, + pub site: LeetcodeSite, +} + +impl Default for Cookies { + fn default() -> Self { + Self { + csrf: "".to_string(), + session: "".to_string(), + site: LeetcodeSite::LeetcodeCom, + } + } +} + +impl Display for Cookies { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "LEETCODE_SESSION={};csrftoken={};", + self.session, self.csrf + ) + } +} + +/// Override cookies from environment variables +pub const LEETCODE_CSRF_ENV: &str = "LEETCODE_CSRF"; +pub const LEETCODE_SESSION_ENV: &str = "LEETCODE_SESSION"; +pub const LEETCODE_SITE_ENV: &str = "LEETCODE_SITE"; + +impl Cookies { + /// Load cookies from environment variables, overriding any existing values + /// if the environment variables are set. + pub fn with_env_override(mut self) -> Self { + if let Ok(csrf) = std::env::var(LEETCODE_CSRF_ENV) { + self.csrf = csrf; + } + if let Ok(session) = std::env::var(LEETCODE_SESSION_ENV) { + self.session = session; + } + if let Ok(site) = std::env::var(LEETCODE_SITE_ENV) { + if let Ok(leetcode_site) = LeetcodeSite::from_str(&site) { + self.site = leetcode_site; + } + } + self + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..f3f5e40 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,89 @@ +//! Soft-link with `config.toml` +//! +//! leetcode-cli will generate a `leetcode.toml` by default, +//! if you wanna change to it, you can: +//! +//! + Edit leetcode.toml at `~/.leetcode/leetcode.toml` directly +//! + Use `leetcode config` to update it +use crate::{ + config::{code::Code, cookies::Cookies, storage::Storage, sys::Sys}, + Error, Result, +}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::Path}; + +mod code; +mod cookies; +mod storage; +mod sys; + +pub use cookies::LeetcodeSite; + +/// Sync with `~/.leetcode/leetcode.toml` +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Config { + #[serde(default, skip_serializing)] + pub sys: Sys, + pub code: Code, + pub cookies: Cookies, + pub storage: Storage, +} + +impl Config { + fn write_default(p: impl AsRef) -> Result<()> { + fs::write(p.as_ref(), toml::ser::to_string_pretty(&Self::default())?)?; + + Ok(()) + } + + /// Locate lc's config file + pub fn locate() -> Result { + let conf = Self::root()?.join("leetcode.toml"); + + if !conf.is_file() { + Self::write_default(&conf)?; + } + + let s = fs::read_to_string(&conf)?; + match toml::from_str::(&s) { + Ok(mut config) => { + // Override config.cookies with environment variables + config.cookies = config.cookies.with_env_override(); + + match config.cookies.site { + cookies::LeetcodeSite::LeetcodeCom => Ok(config), + cookies::LeetcodeSite::LeetcodeCn => { + let mut config = config; + config.sys.urls = sys::Urls::new_with_leetcode_cn(); + Ok(config) + } + } + } + Err(e) => { + let tmp = Self::root()?.join("leetcode.tmp.toml"); + Self::write_default(tmp)?; + Err(e.into()) + } + } + } + + /// Get root path of leetcode-cli + pub fn root() -> Result { + let dir = dirs::home_dir().ok_or(Error::NoneError)?.join(".leetcode"); + if !dir.is_dir() { + info!("Generate root dir at {:?}.", &dir); + fs::DirBuilder::new().recursive(true).create(&dir)?; + } + + Ok(dir) + } + + /// Sync new config to config.toml + pub fn sync(&self) -> Result<()> { + let home = dirs::home_dir().ok_or(Error::NoneError)?; + let conf = home.join(".leetcode/leetcode.toml"); + fs::write(conf, toml::ser::to_string_pretty(&self)?)?; + + Ok(()) + } +} diff --git a/src/config/storage.rs b/src/config/storage.rs new file mode 100644 index 0000000..0395b1f --- /dev/null +++ b/src/config/storage.rs @@ -0,0 +1,75 @@ +//! Storage in config. +use crate::{Error, Result}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::PathBuf}; + +/// Locate code files +/// +/// + cache -> the path to cache +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Storage { + cache: String, + code: String, + root: String, + scripts: Option, +} + +impl Default for Storage { + fn default() -> Self { + Self { + cache: "Problems".into(), + code: "code".into(), + scripts: Some("scripts".into()), + root: "~/.leetcode".into(), + } + } +} + +impl Storage { + /// convert root path + pub fn root(&self) -> Result { + let home = dirs::home_dir() + .ok_or(Error::NoneError)? + .to_string_lossy() + .to_string(); + let path = self.root.replace('~', &home); + Ok(path) + } + + /// get cache path + pub fn cache(&self) -> Result { + let root = PathBuf::from(self.root()?); + if !root.exists() { + info!("Generate cache dir at {:?}.", &root); + fs::DirBuilder::new().recursive(true).create(&root)?; + } + + Ok(root.join("Problems").to_string_lossy().to_string()) + } + + /// get code path + pub fn code(&self) -> Result { + let root = &self.root()?; + let p = PathBuf::from(root).join(&self.code); + if !PathBuf::from(&p).exists() { + fs::create_dir(&p)? + } + + Ok(p.to_string_lossy().to_string()) + } + + /// get scripts path + pub fn scripts(mut self) -> Result { + let root = &self.root()?; + if self.scripts.is_none() { + self.scripts = Some("scripts".into()); + } + + let p = PathBuf::from(root).join(self.scripts.ok_or(Error::NoneError)?); + if !PathBuf::from(&p).exists() { + std::fs::create_dir(&p)? + } + + Ok(p.to_string_lossy().to_string()) + } +} diff --git a/src/config/sys.rs b/src/config/sys.rs new file mode 100644 index 0000000..02a8458 --- /dev/null +++ b/src/config/sys.rs @@ -0,0 +1,121 @@ +//! System section +//! +//! This section is a set of constants after #88 + +use serde::{Deserialize, Serialize}; + +const CATEGORIES: [&str; 4] = ["algorithms", "concurrency", "database", "shell"]; + +// TODO: find a better solution. +fn categories() -> Vec { + CATEGORIES.into_iter().map(|s| s.into()).collect() +} + +/// Leetcode API +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Urls { + pub base: String, + pub graphql: String, + pub login: String, + pub problems: String, + pub problem: String, + pub tag: String, + pub test: String, + pub session: String, + pub submit: String, + pub submissions: String, + pub submission: String, + pub verify: String, + pub favorites: String, + pub favorite_delete: String, +} + +impl Default for Urls { + fn default() -> Self { + Self { + base: "https://leetcode.com".into(), + graphql: "https://leetcode.com/graphql".into(), + login: "https://leetcode.com/accounts/login/".into(), + problems: "https://leetcode.com/api/problems/$category/".into(), + problem: "https://leetcode.com/problems/$slug/description/".into(), + tag: "https://leetcode.com/tag/$slug/".into(), + test: "https://leetcode.com/problems/$slug/interpret_solution/".into(), + session: "https://leetcode.com/session/".into(), + submit: "https://leetcode.com/problems/$slug/submit/".into(), + submissions: "https://leetcode.com/submissions/detail/$id/".into(), + submission: "https://leetcode.com/submissions/detail/$id/".into(), + verify: "https://leetcode.com/submissions/detail/$id/check/".into(), + favorites: "https://leetcode.com/list/api/questions".into(), + favorite_delete: "https://leetcode.com/list/api/questions/$hash/$id".into(), + } + } +} + +impl Urls { + pub fn new_with_leetcode_cn() -> Self { + Self { + base: "https://leetcode.cn".into(), + graphql: "https://leetcode.cn/graphql".into(), + login: "https://leetcode.cn/accounts/login/".into(), + problems: "https://leetcode.cn/api/problems/$category/".into(), + problem: "https://leetcode.cn/problems/$slug/description/".into(), + tag: "https://leetcode.cn/tag/$slug/".into(), + test: "https://leetcode.cn/problems/$slug/interpret_solution/".into(), + session: "https://leetcode.cn/session/".into(), + submit: "https://leetcode.cn/problems/$slug/submit/".into(), + submissions: "https://leetcode.cn/submissions/detail/$id/".into(), + submission: "https://leetcode.cn/submissions/detail/$id/".into(), + verify: "https://leetcode.cn/submissions/detail/$id/check/".into(), + favorites: "https://leetcode.cn/list/api/questions".into(), + favorite_delete: "https://leetcode.cn/list/api/questions/$hash/$id".into(), + } + } + + /// problem url with specific `$slug` + pub fn problem(&self, slug: &str) -> String { + self.problem.replace("$slug", slug) + } + + /// problems url with specific `$category` + pub fn problems(&self, category: &str) -> String { + self.problems.replace("$category", category) + } + + /// submit url with specific `$slug` + pub fn submit(&self, slug: &str) -> String { + self.submit.replace("$slug", slug) + } + + /// tag url with specific `$slug` + pub fn tag(&self, slug: &str) -> String { + self.tag.replace("$slug", slug) + } + + /// test url with specific `$slug` + pub fn test(&self, slug: &str) -> String { + self.test.replace("$slug", slug) + } + + /// verify url with specific `$id` + pub fn verify(&self, id: &str) -> String { + self.verify.replace("$id", id) + } +} + +/// System settings, for leetcode api mainly +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Sys { + #[serde(default = "categories")] + pub categories: Vec, + #[serde(default)] + pub urls: Urls, +} + +impl Default for Sys { + fn default() -> Self { + Self { + categories: CATEGORIES.into_iter().map(|s| s.into()).collect(), + urls: Default::default(), + } + } +} diff --git a/src/err.rs b/src/err.rs index 344cf1b..cf256cf 100644 --- a/src/err.rs +++ b/src/err.rs @@ -1,158 +1,91 @@ //! Errors in leetcode-cli -use crate::cfg::{root, DEFAULT_CONFIG}; use crate::cmds::{Command, DataCommand}; +use anyhow::anyhow; use colored::Colorize; -use std::fmt; -/// Error enum -#[derive(Clone)] +#[cfg(debug_assertions)] +const CONFIG: &str = "~/.leetcode/leetcode.tmp.toml"; +#[cfg(not(debug_assertions))] +const CONFIG: &str = "~/.leetcode/leetcode_tmp.toml"; + +/// Leetcode result. +pub type Result = std::result::Result; + +/// Leetcode cli errors +#[derive(thiserror::Error, Debug)] pub enum Error { + #[error("Nothing matched")] MatchError, + #[error("Download {0} failed, please try again")] DownloadError(String), - NetworkError(String), - ParseError(String), - CacheError(String), - FeatureError(String), - ScriptError(String), + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + #[error(transparent)] + HeaderName(#[from] reqwest::header::InvalidHeaderName), + #[error(transparent)] + HeaderValue(#[from] reqwest::header::InvalidHeaderValue), + #[error( + "Your leetcode cookies seems expired, \ + {} \ + Either you can handwrite your `LEETCODE_SESSION` and `csrf` into `leetcode.toml`, \ + more info please checkout this: \ + https://github.com/clearloop/leetcode-cli/blob/master/README.md#cookies", + "please make sure you have logined in leetcode.com with chrome. ".yellow().bold() + )] CookieError, + #[error( + "Your leetcode account lacks a premium subscription, which the given problem requires.\n \ + If this looks like a mistake, please open a new issue at: {}", + "https://github.com/clearloop/leetcode-cli/".underline() + )] PremiumError, - DecryptError, - SilentError, + #[error(transparent)] + Utf8(#[from] std::string::FromUtf8Error), + #[error( + "json from response parse failed, please open a new issue at: {}.", + "https://github.com/clearloop/leetcode-cli/".underline() + )] NoneError, + #[error( + "Parse config file failed, \ + leetcode-cli has just generated a new leetcode.toml at {}, \ + the current one at {} seems missing some keys, Please compare \ + the new file and add the missing keys.\n", + CONFIG, + "~/.leetcode/leetcode.toml".yellow().bold().underline(), + )] + Config(#[from] toml::de::Error), + #[error("Maybe you not login on the Chrome, you can login and retry")] ChromeNotLogin, + #[error(transparent)] + ParseInt(#[from] std::num::ParseIntError), + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + Toml(#[from] toml::ser::Error), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Anyhow(#[from] anyhow::Error), + #[error(transparent)] + Keyring(#[from] keyring::Error), + #[error(transparent)] + OpenSSL(#[from] openssl::error::ErrorStack), + #[cfg(feature = "pym")] + #[error(transparent)] + Pyo3(#[from] pyo3::PyErr), } -impl std::fmt::Debug for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let e = "error:".bold().red(); - match self { - Error::CacheError(s) => write!(f, "{} {}, please try again", e, s), - Error::CookieError => write!( - f, - "{} \ - Your leetcode cookies seems expired, \ - {} \ - Either you can handwrite your `LEETCODE_SESSION` and `csrf` into `leetcode.toml`, \ - more info please checkout this: \ - https://github.com/clearloop/leetcode-cli/blob/master/README.md#cookies", - e, - "please make sure you have logined in leetcode.com with chrome. " - .yellow() - .bold(), - ), - Error::PremiumError => write!( - f, - "{} \ - Your leetcode account lacks a premium subscription, which the given problem requires.\n \ - If this looks like a mistake, please open a new issue at: {}", - e, - "https://github.com/clearloop/leetcode-cli/".underline()), - Error::DownloadError(s) => write!(f, "{} Download {} failed, please try again", e, s), - Error::NetworkError(s) => write!(f, "{} {}, please try again", e, s), - Error::ParseError(s) => write!(f, "{} {}", e, s), - Error::FeatureError(s) => write!(f, "{} {}", e, s), - Error::MatchError => write!(f, "{} Nothing matches", e), - Error::DecryptError => write!(f, "{} openssl decrypt failed", e), - Error::ScriptError(s) => write!(f, "{} {}", e, s), - Error::SilentError => write!(f, ""), - Error::NoneError => write!(f, - "json from response parse failed, please open a new issue at: {}.", - "https://github.com/clearloop/leetcode-cli/".underline(), - ), - Error::ChromeNotLogin => write!(f, "maybe you not login on the Chrome, you can login and retry.") - } - } -} - -// network -impl std::convert::From for Error { - fn from(err: reqwest::Error) -> Self { - Error::NetworkError(err.to_string()) - } -} - -// nums -impl std::convert::From for Error { - fn from(err: std::num::ParseIntError) -> Self { - Error::ParseError(err.to_string()) - } -} - -// sql impl std::convert::From for Error { fn from(err: diesel::result::Error) -> Self { match err { diesel::result::Error::NotFound => { - println!("NotFound, you may update cache, and try it again\r\n"); DataCommand::usage().print_help().unwrap_or(()); - Error::SilentError + Error::Anyhow(anyhow!( + "NotFound, you may update cache, and try it again\r\n" + )) } - _ => Error::CacheError(err.to_string()), + _ => Error::Anyhow(anyhow!("{err}")), } } } - -// serde -impl std::convert::From for Error { - fn from(err: serde_json::Error) -> Self { - Error::ParseError(err.to_string()) - } -} - -// toml -impl std::convert::From for Error { - fn from(_err: toml::de::Error) -> Self { - let conf = root().unwrap().join("leetcode_tmp.toml"); - std::fs::write(&conf, &DEFAULT_CONFIG[1..]).unwrap(); - #[cfg(debug_assertions)] - let err_msg = format!( - "{}, {}{}{}{}{}{}", - _err, - "Parse config file failed, ", - "leetcode-cli has just generated a new leetcode.toml at ", - "~/.leetcode/leetcode_tmp.toml,".green().bold().underline(), - " the current one at ", - "~/.leetcode/leetcode.toml".yellow().bold().underline(), - " seems missing some keys, Please compare the new file and add the missing keys.\n", - ); - #[cfg(not(debug_assertions))] - let err_msg = format!( - "{}{}{}{}{}{}", - "Parse config file failed, ", - "leetcode-cli has just generated a new leetcode.toml at ", - "~/.leetcode/leetcode_tmp.toml,".green().bold().underline(), - " the current one at ", - "~/.leetcode/leetcode.toml".yellow().bold().underline(), - " seems missing some keys, Please compare the new file and add the missing keys.\n", - ); - Error::ParseError(err_msg) - } -} - -impl std::convert::From for Error { - fn from(err: toml::ser::Error) -> Self { - Error::ParseError(err.to_string()) - } -} - -// io -impl std::convert::From for Error { - fn from(err: std::io::Error) -> Self { - Error::CacheError(err.to_string()) - } -} - -// openssl -impl std::convert::From for Error { - fn from(_: openssl::error::ErrorStack) -> Self { - Error::DecryptError - } -} - -// pyo3 -#[cfg(feature = "pym")] -impl std::convert::From for Error { - fn from(_: pyo3::PyErr) -> Self { - Error::ScriptError("Python script went Error".to_string()) - } -} diff --git a/src/helper.rs b/src/helper.rs index 60efcd8..321828c 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,8 +1,10 @@ //! A set of helper traits -pub use self::digit::Digit; -pub use self::file::{code_path, load_script, test_cases_path}; -pub use self::filter::{filter, squash}; -pub use self::html::HTML; +pub use self::{ + digit::Digit, + file::{code_path, load_script, test_cases_path}, + filter::{filter, squash}, + html::HTML, +}; /// Convert i32 to specific digits string. mod digit { @@ -48,7 +50,7 @@ mod filter { /// Abstract query filter /// /// ```sh - /// -q, --query Fliter questions by conditions: + /// -q, --query Filter questions by conditions: /// Uppercase means negative /// e = easy E = m+h /// m = medium M = e+h @@ -78,14 +80,15 @@ mod filter { } /// Squash questions and ids - pub fn squash(ps: &mut Vec, ids: Vec) -> Result<(), crate::Error> { + pub fn squash(ps: &mut Vec, ids: Vec) -> crate::Result<()> { use std::collections::HashMap; let mut map: HashMap = HashMap::new(); ids.iter().for_each(|x| { map.insert(x.to_string(), true).unwrap_or_default(); }); - ps.retain(|x| map.get(&x.id.to_string()).is_some()); + + ps.retain(|x| map.contains_key(&x.id.to_string())); Ok(()) } } @@ -164,12 +167,13 @@ mod html { mod file { /// Convert file suffix from language type - pub fn suffix(l: &str) -> Result<&'static str, crate::Error> { + pub fn suffix(l: &str) -> crate::Result<&'static str> { match l { "bash" => Ok("sh"), "c" => Ok("c"), "cpp" => Ok("cpp"), "csharp" => Ok("cs"), + "elixir" => Ok("ex"), "golang" => Ok("go"), "java" => Ok("java"), "javascript" => Ok("js"), @@ -182,6 +186,7 @@ mod file { "rust" => Ok("rs"), "scala" => Ok("scala"), "swift" => Ok("swift"), + "typescript" => Ok("ts"), _ => Ok("c"), } } @@ -189,8 +194,8 @@ mod file { use crate::{cache::models::Problem, Error}; /// Generate test cases path by fid - pub fn test_cases_path(problem: &Problem) -> Result { - let conf = crate::cfg::locate()?; + pub fn test_cases_path(problem: &Problem) -> crate::Result { + let conf = crate::config::Config::locate()?; let mut path = format!("{}/{}.tests.dat", conf.storage.code()?, conf.code.pick); path = path.replace("${fid}", &problem.fid.to_string()); @@ -199,8 +204,8 @@ mod file { } /// Generate code path by fid - pub fn code_path(problem: &Problem, l: Option) -> Result { - let conf = crate::cfg::locate()?; + pub fn code_path(problem: &Problem, l: Option) -> crate::Result { + let conf = crate::config::Config::locate()?; let mut lang = conf.code.lang; if l.is_some() { lang = l.ok_or(Error::NoneError)?; @@ -220,10 +225,10 @@ mod file { } /// Load python scripts - pub fn load_script(module: &str) -> Result { + pub fn load_script(module: &str) -> crate::Result { use std::fs::File; use std::io::Read; - let conf = crate::cfg::locate()?; + let conf = crate::config::Config::locate()?; let mut script = "".to_string(); File::open(format!("{}/{}.py", conf.storage.scripts()?, module))? .read_to_string(&mut script)?; diff --git a/src/lib.rs b/src/lib.rs index e97fb6e..39a89e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -235,9 +235,9 @@ extern crate diesel; // show docs pub mod cache; -pub mod cfg; pub mod cli; pub mod cmds; +pub mod config; pub mod err; pub mod flag; pub mod helper; @@ -247,5 +247,5 @@ pub mod pym; // re-exports pub use cache::Cache; -pub use cfg::Config; -pub use err::Error; +pub use config::Config; +pub use err::{Error, Result}; diff --git a/src/plugins/chrome.rs b/src/plugins/chrome.rs index 8893999..0825300 100644 --- a/src/plugins/chrome.rs +++ b/src/plugins/chrome.rs @@ -1,8 +1,9 @@ -use crate::{cache, Error}; +use crate::{cache, Error, Result}; +use anyhow::anyhow; use diesel::prelude::*; use keyring::Entry; use openssl::{hash, pkcs5, symm}; -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Display}; /// LeetCode Cookies Schema mod schema { @@ -33,15 +34,19 @@ pub struct Ident { session: String, } -impl std::string::ToString for Ident { - fn to_string(&self) -> String { - format!("LEETCODE_SESSION={};csrftoken={};", self.session, self.csrf) +impl Display for Ident { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "LEETCODE_SESSION={};csrftoken={};", + self.session, self.csrf + ) } } /// Get cookies from chrome storage -pub fn cookies() -> Result { - let ccfg = crate::cfg::locate()?.cookies; +pub fn cookies() -> Result { + let ccfg = crate::config::Config::locate()?.cookies; if !ccfg.csrf.is_empty() && !ccfg.session.is_empty() { return Ok(Ident { csrf: ccfg.csrf, @@ -61,19 +66,19 @@ pub fn cookies() -> Result { }; debug!("Chrome Cookies path is {:?}", &p); - let conn = cache::conn(p.to_string_lossy().to_string()); + let mut conn = cache::conn(p.to_string_lossy().to_string()); let res = cookies - .filter(host_key.like("%leetcode.com")) - .load::(&conn) + .filter(host_key.like(format!("#{}", ccfg.site))) + .load::(&mut conn) .expect("Loading cookies from google chrome failed."); debug!("res {:?}", &res); if res.is_empty() { - return Err(crate::Error::CookieError); + return Err(Error::CookieError); } // Get system password - let ring = Entry::new("Chrome Safe Storage", "Chrome"); + let ring = Entry::new("Chrome Safe Storage", "Chrome")?; let pass = ring.get_password().expect("Get Password failed"); // Decode cookies @@ -94,7 +99,7 @@ pub fn cookies() -> Result { } /// Decode cookies from chrome -fn decode_cookies(pass: &str, v: Vec) -> Result { +fn decode_cookies(pass: &str, v: Vec) -> Result { let mut key = [0_u8; 16]; match std::env::consts::OS { "macos" => { @@ -117,18 +122,14 @@ fn decode_cookies(pass: &str, v: Vec) -> Result { ) .expect("pbkdf2 hmac went error."); } - _ => { - return Err(crate::Error::FeatureError( - "only supports OSX or Linux for now".to_string(), - )) - } + _ => return Err(anyhow!("only supports OSX or Linux for now").into()), } chrome_decrypt(v, key) } /// Decrypt chrome cookie value with aes-128-cbc -fn chrome_decrypt(v: Vec, key: [u8; 16]) -> Result { +fn chrome_decrypt(v: Vec, key: [u8; 16]) -> Result { // : \u16 let iv = vec![32_u8; 16]; let mut decrypter = symm::Crypter::new( diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 141c765..ec8a924 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -1,8 +1,7 @@ use self::req::{Json, Mode, Req}; use crate::{ - cfg::{self, Config}, - err::Error, - plugins::chrome, + config::{self, Config}, + Result, }; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, @@ -20,31 +19,32 @@ pub struct LeetCode { impl LeetCode { /// Parse reqwest headers - fn headers(mut headers: HeaderMap, ts: Vec<(&str, &str)>) -> Result { + fn headers(mut headers: HeaderMap, ts: Vec<(&str, &str)>) -> Result { for (k, v) in ts.into_iter() { - let name = HeaderName::from_str(k); - let value = HeaderValue::from_str(v); - if name.is_err() || value.is_err() { - return Err(Error::ParseError("http header parse failed".to_string())); - } - - headers.insert(name.unwrap(), value.unwrap()); + let name = HeaderName::from_str(k)?; + let value = HeaderValue::from_str(v)?; + headers.insert(name, value); } Ok(headers) } /// New LeetCode client - pub fn new() -> Result { - let conf = cfg::locate()?; - let cookies = chrome::cookies()?; + pub fn new() -> Result { + let conf = config::Config::locate()?; + let (cookie, csrf) = if conf.cookies.csrf.is_empty() || conf.cookies.session.is_empty() { + let cookies = super::chrome::cookies()?; + (cookies.to_string(), cookies.csrf) + } else { + (conf.cookies.clone().to_string(), conf.cookies.clone().csrf) + }; let default_headers = LeetCode::headers( HeaderMap::new(), vec![ - ("Cookie", cookies.to_string().as_str()), - ("x-csrftoken", &cookies.csrf), + ("Cookie", &cookie), + ("x-csrftoken", &csrf), ("x-requested-with", "XMLHttpRequest"), - ("Origin", &conf.sys.urls["base"]), + ("Origin", &conf.sys.urls.base), ], )?; @@ -53,11 +53,6 @@ impl LeetCode { .connect_timeout(Duration::from_secs(30)) .build()?; - // Sync conf - if conf.cookies.csrf != cookies.csrf { - conf.sync()?; - } - Ok(LeetCode { conf, client, @@ -66,15 +61,9 @@ impl LeetCode { } /// Get category problems - pub async fn get_category_problems(self, category: &str) -> Result { + pub async fn get_category_problems(self, category: &str) -> Result { trace!("Requesting {} problems...", &category); - let url = &self - .conf - .sys - .urls - .get("problems") - .ok_or(Error::NoneError)? - .replace("$category", category); + let url = &self.conf.sys.urls.problems(category); Req { default_headers: self.default_headers, @@ -89,31 +78,27 @@ impl LeetCode { .await } - pub async fn get_question_ids_by_tag(self, slug: &str) -> Result { + pub async fn get_question_ids_by_tag(self, slug: &str) -> Result { trace!("Requesting {} ref problems...", &slug); - let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; + let url = &self.conf.sys.urls.graphql; let mut json: Json = HashMap::new(); json.insert("operationName", "getTopicTag".to_string()); json.insert("variables", r#"{"slug": "$slug"}"#.replace("$slug", slug)); json.insert( "query", - vec![ - "query getTopicTag($slug: String!) {", + ["query getTopicTag($slug: String!) {", " topicTag(slug: $slug) {", " questions {", " questionId", " }", " }", - "}", - ] + "}"] .join("\n"), ); Req { default_headers: self.default_headers, - refer: Some( - (self.conf.sys.urls.get("tag").ok_or(Error::NoneError)?).replace("$slug", slug), - ), + refer: Some(self.conf.sys.urls.tag(slug)), info: false, json: Some(json), mode: Mode::Post, @@ -124,9 +109,9 @@ impl LeetCode { .await } - pub async fn get_user_info(self) -> Result { + pub async fn get_user_info(self) -> Result { trace!("Requesting user info..."); - let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; + let url = &self.conf.sys.urls.graphql; let mut json: Json = HashMap::new(); json.insert("operationName", "a".to_string()); json.insert( @@ -154,24 +139,41 @@ impl LeetCode { } /// Get daily problem - pub async fn get_question_daily(self) -> Result { + pub async fn get_question_daily(self) -> Result { trace!("Requesting daily problem..."); - let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; + let url = &self.conf.sys.urls.graphql; let mut json: Json = HashMap::new(); - json.insert("operationName", "daily".to_string()); - json.insert( - "query", - vec![ - "query daily {", - " activeDailyCodingChallengeQuestion {", - " question {", - " questionFrontendId", - " }", - " }", - "}", - ] - .join("\n"), - ); + + match self.conf.cookies.site { + config::LeetcodeSite::LeetcodeCom => { + json.insert("operationName", "daily".to_string()); + json.insert( + "query", + ["query daily {", + " activeDailyCodingChallengeQuestion {", + " question {", + " questionFrontendId", + " }", + " }", + "}"] + .join("\n"), + ); + } + config::LeetcodeSite::LeetcodeCn => { + json.insert("operationName", "questionOfToday".to_string()); + json.insert( + "query", + ["query questionOfToday {", + " todayRecord {", + " question {", + " questionFrontendId", + " }", + " }", + "}"] + .join("\n"), + ); + } + } Req { default_headers: self.default_headers, @@ -187,20 +189,13 @@ impl LeetCode { } /// Get specific problem detail - pub async fn get_question_detail(self, slug: &str) -> Result { + pub async fn get_question_detail(self, slug: &str) -> Result { trace!("Requesting {} detail...", &slug); - let refer = self - .conf - .sys - .urls - .get("problems") - .ok_or(Error::NoneError)? - .replace("$slug", slug); + let refer = self.conf.sys.urls.problem(slug); let mut json: Json = HashMap::new(); json.insert( "query", - vec![ - "query getQuestionDetail($titleSlug: String!) {", + ["query getQuestionDetail($titleSlug: String!) {", " question(titleSlug: $titleSlug) {", " content", " stats", @@ -211,8 +206,7 @@ impl LeetCode { " metaData", " translatedContent", " }", - "}", - ] + "}"] .join("\n"), ); @@ -230,14 +224,14 @@ impl LeetCode { json: Some(json), mode: Mode::Post, name: "get_problem_detail", - url: self.conf.sys.urls["graphql"].to_string(), + url: self.conf.sys.urls.graphql, } .send(&self.client) .await } /// Send code to judge - pub async fn run_code(self, j: Json, url: String, refer: String) -> Result { + pub async fn run_code(self, j: Json, url: String, refer: String) -> Result { info!("Sending code to judge..."); Req { default_headers: self.default_headers, @@ -253,15 +247,10 @@ impl LeetCode { } /// Get the result of submission / testing - pub async fn verify_result(self, id: String) -> Result { + pub async fn verify_result(self, id: String) -> Result { trace!("Verifying result..."); - let url = self - .conf - .sys - .urls - .get("verify") - .ok_or(Error::NoneError)? - .replace("$id", &id); + let url = self.conf.sys.urls.verify(&id); + Req { default_headers: self.default_headers, refer: None, diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 5a84870..0102ca0 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -6,6 +6,8 @@ //! ## login to `leetcode.com` //! Leetcode-cli use chrome cookie directly, do not need to login, please make sure you have loggined in `leetcode.com` before usnig `leetcode-cli` //! + +// FIXME: Read cookies from local storage. (issue #122) mod chrome; mod leetcode; pub use leetcode::LeetCode; diff --git a/src/pym.rs b/src/pym.rs index dfb409b..43dd81b 100644 --- a/src/pym.rs +++ b/src/pym.rs @@ -1,26 +1,27 @@ //! This module is for python scripts. //! //! Seems like some error exists now, welocome pr to fix this : ) -use crate::cache::Cache; -use crate::helper::load_script; +use crate::{cache::Cache, helper::load_script, Result}; use pyo3::prelude::*; +use std::ffi::CString; /// Exec python scripts as filter -pub fn exec(module: &str) -> Result, crate::Error> { +pub fn exec(module: &str) -> Result> { + pyo3::prepare_freethreaded_python(); let script = load_script(&module)?; let cache = Cache::new()?; - // pygil - let gil = Python::acquire_gil(); - let py = gil.python(); - let pym = PyModule::from_code(py, &script, "plan.py", "plan")?; - // args let sps = serde_json::to_string(&cache.get_problems()?)?; let stags = serde_json::to_string(&cache.get_tags()?)?; - // ret - let res: Vec = pym.call1("plan", (sps, stags))?.extract()?; - - Ok(res) + // pygil + Python::with_gil(|py| { + let script_cstr = CString::new(script.as_str())?; + let filename_cstr = CString::new("plan.py")?; + let module_name_cstr = CString::new("plan")?; + let pym = PyModule::from_code(py, &script_cstr, &filename_cstr, &module_name_cstr)?; + pym.getattr("plan")?.call1((sps, stags))?.extract() + }) + .map_err(Into::into) }