├── .cirrus.yml ├── .github └── FUNDING.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── feed-icon.svg ├── src ├── cache.rs ├── cli.rs ├── config.rs ├── dirs.rs ├── feed.rs ├── main.rs └── xdg.rs ├── tests └── local.html └── visit-website.png /.cirrus.yml: -------------------------------------------------------------------------------- 1 | env: 2 | PATH: "$HOME/.cargo/bin:$PATH" 3 | RUST_VERSION: '1.70.0' 4 | AWS_ACCESS_KEY_ID: ENCRYPTED[d195e8c503f9711bb02e1cf5c64a90cdd63cdbaec7618e57ec7efb0194acf251db6b3056bf17437703025410dab0ef5a] 5 | AWS_SECRET_ACCESS_KEY: ENCRYPTED[0930789e1bd5f1277a5e01f43750cc6de7e2be51ee6d26a60e34b3f47b6b124c54fab9cbcb498f8f26b0f5df48b622c6] 6 | 7 | task: 8 | name: Build (Debian x86_64) 9 | container: 10 | image: debian:12-slim 11 | cpu: 4 12 | cargo_cache: 13 | folder: $HOME/.cargo/registry 14 | fingerprint_script: cat Cargo.lock 15 | install_script: 16 | - apt-get update && apt-get install -y --no-install-recommends git ca-certificates curl gcc libc6-dev musl-tools 17 | - curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain ${RUST_VERSION} 18 | - rustup target add x86_64-unknown-linux-musl 19 | - mkdir ~/bin 20 | - curl -L https://releases.wezm.net/upload-to-s3/0.3.0/upload-to-s3-0.3.0-x86_64-unknown-linux-musl.tar.gz | tar xzf - -C ~/bin 21 | test_script: 22 | - cargo test 23 | publish_script: | 24 | tag=$(git describe --exact-match HEAD 2>/dev/null || true) 25 | if [ -n "$tag" ]; then 26 | cargo build --release --locked --target x86_64-unknown-linux-musl 27 | tarball="rsspls-${tag}-x86_64-unknown-linux-musl.tar.gz" 28 | strip target/x86_64-unknown-linux-musl/release/rsspls 29 | tar zcf "$tarball" -C target/x86_64-unknown-linux-musl/release rsspls 30 | ~/bin/upload-to-s3 -b releases.wezm.net "$tarball" "rsspls/$tag/$tarball" 31 | fi 32 | 33 | task: 34 | name: Build (Debian aarch64) 35 | arm_container: 36 | image: debian:12-slim 37 | cpu: 4 38 | cargo_cache: 39 | folder: $HOME/.cargo/registry 40 | fingerprint_script: cat Cargo.lock 41 | install_script: 42 | - apt-get update && apt-get install -y --no-install-recommends git ca-certificates curl gcc libc6-dev musl-tools 43 | - curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain ${RUST_VERSION} 44 | - rustup target add aarch64-unknown-linux-musl 45 | - mkdir ~/bin 46 | - curl -L https://releases.wezm.net/upload-to-s3/0.3.0/upload-to-s3-0.3.0-aarch64-unknown-linux-musl.tar.gz | tar xzf - -C ~/bin 47 | test_script: 48 | - cargo test 49 | publish_script: | 50 | tag=$(git describe --exact-match HEAD 2>/dev/null || true) 51 | if [ -n "$tag" ]; then 52 | cargo build --release --locked --target aarch64-unknown-linux-musl 53 | tarball="rsspls-${tag}-aarch64-unknown-linux-musl.tar.gz" 54 | strip target/aarch64-unknown-linux-musl/release/rsspls 55 | tar zcf "$tarball" -C target/aarch64-unknown-linux-musl/release rsspls 56 | ~/bin/upload-to-s3 -b releases.wezm.net "$tarball" "rsspls/$tag/$tarball" 57 | fi 58 | 59 | task: 60 | name: Build (FreeBSD) 61 | freebsd_instance: 62 | image_family: freebsd-13-3 63 | cpu: 4 64 | cargo_cache: 65 | folder: $HOME/.cargo/registry 66 | fingerprint_script: cat Cargo.lock 67 | install_script: 68 | - pkg install -y git-lite ca_root_nss 69 | - fetch -o - https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain ${RUST_VERSION} 70 | - fetch -o - https://releases.wezm.net/upload-to-s3/0.3.0/upload-to-s3-0.3.0-amd64-unknown-freebsd.tar.gz | tar xzf - -C /usr/local/bin 71 | test_script: 72 | - cargo test 73 | publish_script: | 74 | tag=$(git describe --exact-match HEAD 2>/dev/null || true) 75 | if [ -n "$tag" ]; then 76 | cargo build --release --locked 77 | tarball="rsspls-${tag}-amd64-unknown-freebsd.tar.gz" 78 | strip target/release/rsspls 79 | tar zcf "$tarball" -C target/release rsspls 80 | upload-to-s3 -b releases.wezm.net "$tarball" "rsspls/$tag/$tarball" 81 | fi 82 | 83 | task: 84 | name: Build (Mac OS) 85 | macos_instance: 86 | image: ghcr.io/cirruslabs/macos-runner:sonoma 87 | env: 88 | PATH: "$HOME/.cargo/bin:$HOME/bin:$PATH" 89 | cargo_cache: 90 | folder: $HOME/.cargo/registry 91 | fingerprint_script: cat Cargo.lock 92 | install_script: 93 | - curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain ${RUST_VERSION} 94 | - mkdir ~/bin 95 | - curl -L https://releases.wezm.net/upload-to-s3/0.3.0/upload-to-s3-0.3.0-universal-apple-darwin.tar.gz | tar xzf - -C ~/bin 96 | - rustup target add x86_64-apple-darwin 97 | test_script: 98 | - cargo test 99 | publish_script: | 100 | tag=$(git describe --exact-match HEAD 2>/dev/null || true) 101 | if [ -n "$tag" ]; then 102 | cargo build --release --locked 103 | cargo build --release --locked --target x86_64-apple-darwin 104 | mv target/release/rsspls target/release/rsspls.$CIRRUS_ARCH 105 | lipo target/release/rsspls.$CIRRUS_ARCH target/x86_64-apple-darwin/release/rsspls -create -output target/release/rsspls 106 | lipo -info target/release/rsspls 107 | tarball="rsspls-${tag}-universal-apple-darwin.tar.gz" 108 | strip target/release/rsspls 109 | tar zcf "$tarball" -C target/release rsspls 110 | upload-to-s3 -b releases.wezm.net "$tarball" "rsspls/$tag/$tarball" 111 | fi 112 | 113 | task: 114 | name: Build (Windows) 115 | windows_container: 116 | image: cirrusci/windowsservercore:cmake 117 | cpu: 4 118 | environment: 119 | CIRRUS_SHELL: powershell 120 | cargo_cache: 121 | folder: $HOME\.cargo\registry 122 | fingerprint_script: cat Cargo.lock 123 | install_script: 124 | - Invoke-WebRequest -Uri https://win.rustup.rs/x86_64 -OutFile rustup-init.exe 125 | - .\rustup-init -y --profile minimal --default-toolchain $env:RUST_VERSION 126 | - Invoke-WebRequest https://releases.wezm.net/upload-to-s3/0.3.0/upload-to-s3-0.3.0-x86_64-pc-windows-msvc.zip -OutFile upload-to-s3.zip 127 | - Expand-Archive upload-to-s3.zip -DestinationPath . 128 | - git fetch --tags 129 | # PowerShell it truly horrific and lacks a way to exit on external command failure so we have to check after every command 130 | # https://stackoverflow.com/questions/48864988/powershell-with-git-command-error-handling-automatically-abort-on-non-zero-exi/48877892#48877892 131 | test_script: | 132 | ~\.cargo\bin\cargo test 133 | if ($LASTEXITCODE) { Throw } 134 | publish_script: | 135 | try { 136 | $tag=$(git describe --exact-match HEAD 2>$null) 137 | if ($LASTEXITCODE) { Throw } 138 | } catch { 139 | $tag="" 140 | } 141 | if ( $tag.Length -gt 0 ) { 142 | ~\.cargo\bin\cargo build --release --locked 143 | if ($LASTEXITCODE) { Throw } 144 | $tarball="rsspls-$tag-x86_64-pc-windows-msvc.zip" 145 | cd target\release 146 | strip rsspls.exe 147 | if ($LASTEXITCODE) { Throw } 148 | Compress-Archive .\rsspls.exe "$tarball" 149 | cd ..\.. 150 | .\upload-to-s3 -b releases.wezm.net "target\release\$tarball" "rsspls/$tag/$tarball" 151 | if ($LASTEXITCODE) { Throw } 152 | } 153 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: wezm 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /TODO.md 3 | *.rss 4 | .vscode 5 | feeds.toml -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anydate" 31 | version = "0.4.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "ab5a92998480462b6ae88d54b3d720a6b43427a45443f42dd191a184dd4dbca3" 34 | dependencies = [ 35 | "chrono", 36 | "thiserror", 37 | ] 38 | 39 | [[package]] 40 | name = "async-compression" 41 | version = "0.4.12" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" 44 | dependencies = [ 45 | "flate2", 46 | "futures-core", 47 | "memchr", 48 | "pin-project-lite", 49 | "tokio", 50 | ] 51 | 52 | [[package]] 53 | name = "atom_syndication" 54 | version = "0.12.3" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "f2f34613907f31c9dbef0240156db3c9263f34842b6e1a8999d2304ea62c8a30" 57 | dependencies = [ 58 | "chrono", 59 | "derive_builder", 60 | "diligent-date-parser", 61 | "never", 62 | "quick-xml", 63 | ] 64 | 65 | [[package]] 66 | name = "atomicwrites" 67 | version = "0.4.3" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "fc7b2dbe9169059af0f821e811180fddc971fc210c776c133c7819ccd6e478db" 70 | dependencies = [ 71 | "rustix", 72 | "tempfile", 73 | "windows-sys 0.52.0", 74 | ] 75 | 76 | [[package]] 77 | name = "autocfg" 78 | version = "1.3.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 81 | 82 | [[package]] 83 | name = "backtrace" 84 | version = "0.3.73" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 87 | dependencies = [ 88 | "addr2line", 89 | "cc", 90 | "cfg-if", 91 | "libc", 92 | "miniz_oxide", 93 | "object", 94 | "rustc-demangle", 95 | ] 96 | 97 | [[package]] 98 | name = "base64" 99 | version = "0.22.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 102 | 103 | [[package]] 104 | name = "basic-toml" 105 | version = "0.1.9" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" 108 | dependencies = [ 109 | "serde", 110 | ] 111 | 112 | [[package]] 113 | name = "bitflags" 114 | version = "1.3.2" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 117 | 118 | [[package]] 119 | name = "bitflags" 120 | version = "2.6.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 123 | 124 | [[package]] 125 | name = "bumpalo" 126 | version = "3.16.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 129 | 130 | [[package]] 131 | name = "byteorder" 132 | version = "1.5.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 135 | 136 | [[package]] 137 | name = "bytes" 138 | version = "1.7.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" 141 | 142 | [[package]] 143 | name = "cc" 144 | version = "1.1.8" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" 147 | 148 | [[package]] 149 | name = "cfg-if" 150 | version = "1.0.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 153 | 154 | [[package]] 155 | name = "chrono" 156 | version = "0.4.38" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 159 | dependencies = [ 160 | "num-traits", 161 | ] 162 | 163 | [[package]] 164 | name = "convert_case" 165 | version = "0.4.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 168 | 169 | [[package]] 170 | name = "core-foundation" 171 | version = "0.9.4" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 174 | dependencies = [ 175 | "core-foundation-sys", 176 | "libc", 177 | ] 178 | 179 | [[package]] 180 | name = "core-foundation-sys" 181 | version = "0.8.6" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 184 | 185 | [[package]] 186 | name = "crc32fast" 187 | version = "1.4.2" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 190 | dependencies = [ 191 | "cfg-if", 192 | ] 193 | 194 | [[package]] 195 | name = "cryptoxide" 196 | version = "0.4.4" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "382ce8820a5bb815055d3553a610e8cb542b2d767bbacea99038afda96cd760d" 199 | 200 | [[package]] 201 | name = "cssparser" 202 | version = "0.27.2" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" 205 | dependencies = [ 206 | "cssparser-macros", 207 | "dtoa-short", 208 | "itoa 0.4.8", 209 | "matches", 210 | "phf", 211 | "proc-macro2", 212 | "quote", 213 | "smallvec", 214 | "syn 1.0.109", 215 | ] 216 | 217 | [[package]] 218 | name = "cssparser-macros" 219 | version = "0.6.1" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" 222 | dependencies = [ 223 | "quote", 224 | "syn 2.0.72", 225 | ] 226 | 227 | [[package]] 228 | name = "darling" 229 | version = "0.20.10" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 232 | dependencies = [ 233 | "darling_core", 234 | "darling_macro", 235 | ] 236 | 237 | [[package]] 238 | name = "darling_core" 239 | version = "0.20.10" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 242 | dependencies = [ 243 | "fnv", 244 | "ident_case", 245 | "proc-macro2", 246 | "quote", 247 | "strsim", 248 | "syn 2.0.72", 249 | ] 250 | 251 | [[package]] 252 | name = "darling_macro" 253 | version = "0.20.10" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 256 | dependencies = [ 257 | "darling_core", 258 | "quote", 259 | "syn 2.0.72", 260 | ] 261 | 262 | [[package]] 263 | name = "deranged" 264 | version = "0.3.11" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 267 | dependencies = [ 268 | "powerfmt", 269 | ] 270 | 271 | [[package]] 272 | name = "derive_builder" 273 | version = "0.20.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" 276 | dependencies = [ 277 | "derive_builder_macro", 278 | ] 279 | 280 | [[package]] 281 | name = "derive_builder_core" 282 | version = "0.20.0" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" 285 | dependencies = [ 286 | "darling", 287 | "proc-macro2", 288 | "quote", 289 | "syn 2.0.72", 290 | ] 291 | 292 | [[package]] 293 | name = "derive_builder_macro" 294 | version = "0.20.0" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" 297 | dependencies = [ 298 | "derive_builder_core", 299 | "syn 2.0.72", 300 | ] 301 | 302 | [[package]] 303 | name = "derive_more" 304 | version = "0.99.18" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" 307 | dependencies = [ 308 | "convert_case", 309 | "proc-macro2", 310 | "quote", 311 | "rustc_version", 312 | "syn 2.0.72", 313 | ] 314 | 315 | [[package]] 316 | name = "diligent-date-parser" 317 | version = "0.1.4" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "f6cf7fe294274a222363f84bcb63cdea762979a0443b4cf1f4f8fd17c86b1182" 320 | dependencies = [ 321 | "chrono", 322 | ] 323 | 324 | [[package]] 325 | name = "dirs" 326 | version = "5.0.1" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 329 | dependencies = [ 330 | "dirs-sys", 331 | ] 332 | 333 | [[package]] 334 | name = "dirs-sys" 335 | version = "0.4.1" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 338 | dependencies = [ 339 | "libc", 340 | "option-ext", 341 | "redox_users", 342 | "windows-sys 0.48.0", 343 | ] 344 | 345 | [[package]] 346 | name = "dtoa" 347 | version = "1.0.9" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" 350 | 351 | [[package]] 352 | name = "dtoa-short" 353 | version = "0.3.5" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" 356 | dependencies = [ 357 | "dtoa", 358 | ] 359 | 360 | [[package]] 361 | name = "either" 362 | version = "1.13.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 365 | 366 | [[package]] 367 | name = "encoding_rs" 368 | version = "0.8.34" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" 371 | dependencies = [ 372 | "cfg-if", 373 | ] 374 | 375 | [[package]] 376 | name = "env_logger" 377 | version = "0.10.2" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 380 | dependencies = [ 381 | "humantime", 382 | "is-terminal", 383 | "log", 384 | "regex", 385 | "termcolor", 386 | ] 387 | 388 | [[package]] 389 | name = "errno" 390 | version = "0.3.9" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 393 | dependencies = [ 394 | "libc", 395 | "windows-sys 0.52.0", 396 | ] 397 | 398 | [[package]] 399 | name = "eyre" 400 | version = "0.6.12" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 403 | dependencies = [ 404 | "indenter", 405 | "once_cell", 406 | ] 407 | 408 | [[package]] 409 | name = "fastrand" 410 | version = "2.1.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 413 | 414 | [[package]] 415 | name = "flate2" 416 | version = "1.0.31" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" 419 | dependencies = [ 420 | "crc32fast", 421 | "miniz_oxide", 422 | ] 423 | 424 | [[package]] 425 | name = "fnv" 426 | version = "1.0.7" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 429 | 430 | [[package]] 431 | name = "foreign-types" 432 | version = "0.3.2" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 435 | dependencies = [ 436 | "foreign-types-shared", 437 | ] 438 | 439 | [[package]] 440 | name = "foreign-types-shared" 441 | version = "0.1.1" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 444 | 445 | [[package]] 446 | name = "form_urlencoded" 447 | version = "1.2.1" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 450 | dependencies = [ 451 | "percent-encoding", 452 | ] 453 | 454 | [[package]] 455 | name = "futf" 456 | version = "0.1.5" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" 459 | dependencies = [ 460 | "mac", 461 | "new_debug_unreachable", 462 | ] 463 | 464 | [[package]] 465 | name = "futures" 466 | version = "0.3.30" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 469 | dependencies = [ 470 | "futures-channel", 471 | "futures-core", 472 | "futures-io", 473 | "futures-sink", 474 | "futures-task", 475 | "futures-util", 476 | ] 477 | 478 | [[package]] 479 | name = "futures-channel" 480 | version = "0.3.30" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 483 | dependencies = [ 484 | "futures-core", 485 | "futures-sink", 486 | ] 487 | 488 | [[package]] 489 | name = "futures-core" 490 | version = "0.3.30" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 493 | 494 | [[package]] 495 | name = "futures-io" 496 | version = "0.3.30" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 499 | 500 | [[package]] 501 | name = "futures-sink" 502 | version = "0.3.30" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 505 | 506 | [[package]] 507 | name = "futures-task" 508 | version = "0.3.30" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 511 | 512 | [[package]] 513 | name = "futures-util" 514 | version = "0.3.30" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 517 | dependencies = [ 518 | "futures-channel", 519 | "futures-core", 520 | "futures-io", 521 | "futures-sink", 522 | "futures-task", 523 | "memchr", 524 | "pin-project-lite", 525 | "pin-utils", 526 | "slab", 527 | ] 528 | 529 | [[package]] 530 | name = "fxhash" 531 | version = "0.2.1" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 534 | dependencies = [ 535 | "byteorder", 536 | ] 537 | 538 | [[package]] 539 | name = "getrandom" 540 | version = "0.1.16" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 543 | dependencies = [ 544 | "cfg-if", 545 | "libc", 546 | "wasi 0.9.0+wasi-snapshot-preview1", 547 | ] 548 | 549 | [[package]] 550 | name = "getrandom" 551 | version = "0.2.15" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 554 | dependencies = [ 555 | "cfg-if", 556 | "libc", 557 | "wasi 0.11.0+wasi-snapshot-preview1", 558 | ] 559 | 560 | [[package]] 561 | name = "gimli" 562 | version = "0.29.0" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 565 | 566 | [[package]] 567 | name = "hermit-abi" 568 | version = "0.3.9" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 571 | 572 | [[package]] 573 | name = "html5ever" 574 | version = "0.25.2" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" 577 | dependencies = [ 578 | "log", 579 | "mac", 580 | "markup5ever", 581 | "proc-macro2", 582 | "quote", 583 | "syn 1.0.109", 584 | ] 585 | 586 | [[package]] 587 | name = "http" 588 | version = "1.1.0" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 591 | dependencies = [ 592 | "bytes", 593 | "fnv", 594 | "itoa 1.0.11", 595 | ] 596 | 597 | [[package]] 598 | name = "http-body" 599 | version = "1.0.1" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 602 | dependencies = [ 603 | "bytes", 604 | "http", 605 | ] 606 | 607 | [[package]] 608 | name = "http-body-util" 609 | version = "0.1.2" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 612 | dependencies = [ 613 | "bytes", 614 | "futures-util", 615 | "http", 616 | "http-body", 617 | "pin-project-lite", 618 | ] 619 | 620 | [[package]] 621 | name = "httparse" 622 | version = "1.9.4" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" 625 | 626 | [[package]] 627 | name = "humantime" 628 | version = "2.1.0" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 631 | 632 | [[package]] 633 | name = "hyper" 634 | version = "1.4.1" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" 637 | dependencies = [ 638 | "bytes", 639 | "futures-channel", 640 | "futures-util", 641 | "http", 642 | "http-body", 643 | "httparse", 644 | "itoa 1.0.11", 645 | "pin-project-lite", 646 | "smallvec", 647 | "tokio", 648 | "want", 649 | ] 650 | 651 | [[package]] 652 | name = "hyper-rustls" 653 | version = "0.27.2" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" 656 | dependencies = [ 657 | "futures-util", 658 | "http", 659 | "hyper", 660 | "hyper-util", 661 | "rustls", 662 | "rustls-pki-types", 663 | "tokio", 664 | "tokio-rustls", 665 | "tower-service", 666 | "webpki-roots", 667 | ] 668 | 669 | [[package]] 670 | name = "hyper-tls" 671 | version = "0.6.0" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 674 | dependencies = [ 675 | "bytes", 676 | "http-body-util", 677 | "hyper", 678 | "hyper-util", 679 | "native-tls", 680 | "tokio", 681 | "tokio-native-tls", 682 | "tower-service", 683 | ] 684 | 685 | [[package]] 686 | name = "hyper-util" 687 | version = "0.1.7" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" 690 | dependencies = [ 691 | "bytes", 692 | "futures-channel", 693 | "futures-util", 694 | "http", 695 | "http-body", 696 | "hyper", 697 | "pin-project-lite", 698 | "socket2", 699 | "tokio", 700 | "tower", 701 | "tower-service", 702 | "tracing", 703 | ] 704 | 705 | [[package]] 706 | name = "ident_case" 707 | version = "1.0.1" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 710 | 711 | [[package]] 712 | name = "idna" 713 | version = "0.5.0" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 716 | dependencies = [ 717 | "unicode-bidi", 718 | "unicode-normalization", 719 | ] 720 | 721 | [[package]] 722 | name = "indenter" 723 | version = "0.3.3" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 726 | 727 | [[package]] 728 | name = "ipnet" 729 | version = "2.9.0" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 732 | 733 | [[package]] 734 | name = "is-terminal" 735 | version = "0.4.12" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" 738 | dependencies = [ 739 | "hermit-abi", 740 | "libc", 741 | "windows-sys 0.52.0", 742 | ] 743 | 744 | [[package]] 745 | name = "itoa" 746 | version = "0.4.8" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 749 | 750 | [[package]] 751 | name = "itoa" 752 | version = "1.0.11" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 755 | 756 | [[package]] 757 | name = "js-sys" 758 | version = "0.3.69" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 761 | dependencies = [ 762 | "wasm-bindgen", 763 | ] 764 | 765 | [[package]] 766 | name = "kuchiki" 767 | version = "0.8.1" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" 770 | dependencies = [ 771 | "cssparser", 772 | "html5ever", 773 | "matches", 774 | "selectors", 775 | ] 776 | 777 | [[package]] 778 | name = "libc" 779 | version = "0.2.155" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 782 | 783 | [[package]] 784 | name = "libredox" 785 | version = "0.1.3" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 788 | dependencies = [ 789 | "bitflags 2.6.0", 790 | "libc", 791 | ] 792 | 793 | [[package]] 794 | name = "linux-raw-sys" 795 | version = "0.4.14" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 798 | 799 | [[package]] 800 | name = "lock_api" 801 | version = "0.4.12" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 804 | dependencies = [ 805 | "autocfg", 806 | "scopeguard", 807 | ] 808 | 809 | [[package]] 810 | name = "log" 811 | version = "0.4.22" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 814 | 815 | [[package]] 816 | name = "mac" 817 | version = "0.1.1" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 820 | 821 | [[package]] 822 | name = "markup5ever" 823 | version = "0.10.1" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" 826 | dependencies = [ 827 | "log", 828 | "phf", 829 | "phf_codegen", 830 | "string_cache", 831 | "string_cache_codegen", 832 | "tendril", 833 | ] 834 | 835 | [[package]] 836 | name = "matches" 837 | version = "0.1.10" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" 840 | 841 | [[package]] 842 | name = "memchr" 843 | version = "2.7.4" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 846 | 847 | [[package]] 848 | name = "mime" 849 | version = "0.3.17" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 852 | 853 | [[package]] 854 | name = "mime_guess" 855 | version = "2.0.5" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 858 | dependencies = [ 859 | "mime", 860 | "unicase", 861 | ] 862 | 863 | [[package]] 864 | name = "miniz_oxide" 865 | version = "0.7.4" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 868 | dependencies = [ 869 | "adler", 870 | ] 871 | 872 | [[package]] 873 | name = "mio" 874 | version = "1.0.1" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" 877 | dependencies = [ 878 | "hermit-abi", 879 | "libc", 880 | "wasi 0.11.0+wasi-snapshot-preview1", 881 | "windows-sys 0.52.0", 882 | ] 883 | 884 | [[package]] 885 | name = "native-tls" 886 | version = "0.2.12" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" 889 | dependencies = [ 890 | "libc", 891 | "log", 892 | "openssl", 893 | "openssl-probe", 894 | "openssl-sys", 895 | "schannel", 896 | "security-framework", 897 | "security-framework-sys", 898 | "tempfile", 899 | ] 900 | 901 | [[package]] 902 | name = "never" 903 | version = "0.1.0" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" 906 | 907 | [[package]] 908 | name = "new_debug_unreachable" 909 | version = "1.0.6" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 912 | 913 | [[package]] 914 | name = "nodrop" 915 | version = "0.1.14" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" 918 | 919 | [[package]] 920 | name = "num-conv" 921 | version = "0.1.0" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 924 | 925 | [[package]] 926 | name = "num-traits" 927 | version = "0.2.19" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 930 | dependencies = [ 931 | "autocfg", 932 | ] 933 | 934 | [[package]] 935 | name = "object" 936 | version = "0.36.3" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" 939 | dependencies = [ 940 | "memchr", 941 | ] 942 | 943 | [[package]] 944 | name = "once_cell" 945 | version = "1.19.0" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 948 | 949 | [[package]] 950 | name = "openssl" 951 | version = "0.10.66" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" 954 | dependencies = [ 955 | "bitflags 2.6.0", 956 | "cfg-if", 957 | "foreign-types", 958 | "libc", 959 | "once_cell", 960 | "openssl-macros", 961 | "openssl-sys", 962 | ] 963 | 964 | [[package]] 965 | name = "openssl-macros" 966 | version = "0.1.1" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 969 | dependencies = [ 970 | "proc-macro2", 971 | "quote", 972 | "syn 2.0.72", 973 | ] 974 | 975 | [[package]] 976 | name = "openssl-probe" 977 | version = "0.1.5" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 980 | 981 | [[package]] 982 | name = "openssl-sys" 983 | version = "0.9.103" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" 986 | dependencies = [ 987 | "cc", 988 | "libc", 989 | "pkg-config", 990 | "vcpkg", 991 | ] 992 | 993 | [[package]] 994 | name = "option-ext" 995 | version = "0.2.0" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 998 | 999 | [[package]] 1000 | name = "parking_lot" 1001 | version = "0.12.3" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1004 | dependencies = [ 1005 | "lock_api", 1006 | "parking_lot_core", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "parking_lot_core" 1011 | version = "0.9.10" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1014 | dependencies = [ 1015 | "cfg-if", 1016 | "libc", 1017 | "redox_syscall", 1018 | "smallvec", 1019 | "windows-targets 0.52.6", 1020 | ] 1021 | 1022 | [[package]] 1023 | name = "percent-encoding" 1024 | version = "2.3.1" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1027 | 1028 | [[package]] 1029 | name = "phf" 1030 | version = "0.8.0" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" 1033 | dependencies = [ 1034 | "phf_macros", 1035 | "phf_shared 0.8.0", 1036 | "proc-macro-hack", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "phf_codegen" 1041 | version = "0.8.0" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" 1044 | dependencies = [ 1045 | "phf_generator 0.8.0", 1046 | "phf_shared 0.8.0", 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "phf_generator" 1051 | version = "0.8.0" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" 1054 | dependencies = [ 1055 | "phf_shared 0.8.0", 1056 | "rand 0.7.3", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "phf_generator" 1061 | version = "0.10.0" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" 1064 | dependencies = [ 1065 | "phf_shared 0.10.0", 1066 | "rand 0.8.5", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "phf_macros" 1071 | version = "0.8.0" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" 1074 | dependencies = [ 1075 | "phf_generator 0.8.0", 1076 | "phf_shared 0.8.0", 1077 | "proc-macro-hack", 1078 | "proc-macro2", 1079 | "quote", 1080 | "syn 1.0.109", 1081 | ] 1082 | 1083 | [[package]] 1084 | name = "phf_shared" 1085 | version = "0.8.0" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" 1088 | dependencies = [ 1089 | "siphasher", 1090 | ] 1091 | 1092 | [[package]] 1093 | name = "phf_shared" 1094 | version = "0.10.0" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" 1097 | dependencies = [ 1098 | "siphasher", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "pico-args" 1103 | version = "0.5.0" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" 1106 | 1107 | [[package]] 1108 | name = "pin-project" 1109 | version = "1.1.5" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 1112 | dependencies = [ 1113 | "pin-project-internal", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "pin-project-internal" 1118 | version = "1.1.5" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 1121 | dependencies = [ 1122 | "proc-macro2", 1123 | "quote", 1124 | "syn 2.0.72", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "pin-project-lite" 1129 | version = "0.2.14" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 1132 | 1133 | [[package]] 1134 | name = "pin-utils" 1135 | version = "0.1.0" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1138 | 1139 | [[package]] 1140 | name = "pkg-config" 1141 | version = "0.3.30" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 1144 | 1145 | [[package]] 1146 | name = "powerfmt" 1147 | version = "0.2.0" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1150 | 1151 | [[package]] 1152 | name = "ppv-lite86" 1153 | version = "0.2.20" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 1156 | dependencies = [ 1157 | "zerocopy", 1158 | ] 1159 | 1160 | [[package]] 1161 | name = "precomputed-hash" 1162 | version = "0.1.1" 1163 | source = "registry+https://github.com/rust-lang/crates.io-index" 1164 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 1165 | 1166 | [[package]] 1167 | name = "pretty_env_logger" 1168 | version = "0.5.0" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" 1171 | dependencies = [ 1172 | "env_logger", 1173 | "log", 1174 | ] 1175 | 1176 | [[package]] 1177 | name = "proc-macro-hack" 1178 | version = "0.5.20+deprecated" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" 1181 | 1182 | [[package]] 1183 | name = "proc-macro2" 1184 | version = "1.0.86" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 1187 | dependencies = [ 1188 | "unicode-ident", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "quick-xml" 1193 | version = "0.31.0" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" 1196 | dependencies = [ 1197 | "encoding_rs", 1198 | "memchr", 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "quinn" 1203 | version = "0.11.3" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" 1206 | dependencies = [ 1207 | "bytes", 1208 | "pin-project-lite", 1209 | "quinn-proto", 1210 | "quinn-udp", 1211 | "rustc-hash", 1212 | "rustls", 1213 | "socket2", 1214 | "thiserror", 1215 | "tokio", 1216 | "tracing", 1217 | ] 1218 | 1219 | [[package]] 1220 | name = "quinn-proto" 1221 | version = "0.11.6" 1222 | source = "registry+https://github.com/rust-lang/crates.io-index" 1223 | checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" 1224 | dependencies = [ 1225 | "bytes", 1226 | "rand 0.8.5", 1227 | "ring", 1228 | "rustc-hash", 1229 | "rustls", 1230 | "slab", 1231 | "thiserror", 1232 | "tinyvec", 1233 | "tracing", 1234 | ] 1235 | 1236 | [[package]] 1237 | name = "quinn-udp" 1238 | version = "0.5.4" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" 1241 | dependencies = [ 1242 | "libc", 1243 | "once_cell", 1244 | "socket2", 1245 | "tracing", 1246 | "windows-sys 0.52.0", 1247 | ] 1248 | 1249 | [[package]] 1250 | name = "quote" 1251 | version = "1.0.36" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 1254 | dependencies = [ 1255 | "proc-macro2", 1256 | ] 1257 | 1258 | [[package]] 1259 | name = "rand" 1260 | version = "0.7.3" 1261 | source = "registry+https://github.com/rust-lang/crates.io-index" 1262 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 1263 | dependencies = [ 1264 | "getrandom 0.1.16", 1265 | "libc", 1266 | "rand_chacha 0.2.2", 1267 | "rand_core 0.5.1", 1268 | "rand_hc", 1269 | "rand_pcg", 1270 | ] 1271 | 1272 | [[package]] 1273 | name = "rand" 1274 | version = "0.8.5" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1277 | dependencies = [ 1278 | "libc", 1279 | "rand_chacha 0.3.1", 1280 | "rand_core 0.6.4", 1281 | ] 1282 | 1283 | [[package]] 1284 | name = "rand_chacha" 1285 | version = "0.2.2" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 1288 | dependencies = [ 1289 | "ppv-lite86", 1290 | "rand_core 0.5.1", 1291 | ] 1292 | 1293 | [[package]] 1294 | name = "rand_chacha" 1295 | version = "0.3.1" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1298 | dependencies = [ 1299 | "ppv-lite86", 1300 | "rand_core 0.6.4", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "rand_core" 1305 | version = "0.5.1" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 1308 | dependencies = [ 1309 | "getrandom 0.1.16", 1310 | ] 1311 | 1312 | [[package]] 1313 | name = "rand_core" 1314 | version = "0.6.4" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1317 | dependencies = [ 1318 | "getrandom 0.2.15", 1319 | ] 1320 | 1321 | [[package]] 1322 | name = "rand_hc" 1323 | version = "0.2.0" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 1326 | dependencies = [ 1327 | "rand_core 0.5.1", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "rand_pcg" 1332 | version = "0.2.1" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" 1335 | dependencies = [ 1336 | "rand_core 0.5.1", 1337 | ] 1338 | 1339 | [[package]] 1340 | name = "redox_syscall" 1341 | version = "0.5.3" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 1344 | dependencies = [ 1345 | "bitflags 2.6.0", 1346 | ] 1347 | 1348 | [[package]] 1349 | name = "redox_users" 1350 | version = "0.4.5" 1351 | source = "registry+https://github.com/rust-lang/crates.io-index" 1352 | checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" 1353 | dependencies = [ 1354 | "getrandom 0.2.15", 1355 | "libredox", 1356 | "thiserror", 1357 | ] 1358 | 1359 | [[package]] 1360 | name = "regex" 1361 | version = "1.10.6" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 1364 | dependencies = [ 1365 | "aho-corasick", 1366 | "memchr", 1367 | "regex-automata", 1368 | "regex-syntax", 1369 | ] 1370 | 1371 | [[package]] 1372 | name = "regex-automata" 1373 | version = "0.4.7" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 1376 | dependencies = [ 1377 | "aho-corasick", 1378 | "memchr", 1379 | "regex-syntax", 1380 | ] 1381 | 1382 | [[package]] 1383 | name = "regex-syntax" 1384 | version = "0.8.4" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 1387 | 1388 | [[package]] 1389 | name = "reqwest" 1390 | version = "0.12.5" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" 1393 | dependencies = [ 1394 | "async-compression", 1395 | "base64", 1396 | "bytes", 1397 | "futures-core", 1398 | "futures-util", 1399 | "http", 1400 | "http-body", 1401 | "http-body-util", 1402 | "hyper", 1403 | "hyper-rustls", 1404 | "hyper-tls", 1405 | "hyper-util", 1406 | "ipnet", 1407 | "js-sys", 1408 | "log", 1409 | "mime", 1410 | "native-tls", 1411 | "once_cell", 1412 | "percent-encoding", 1413 | "pin-project-lite", 1414 | "quinn", 1415 | "rustls", 1416 | "rustls-pemfile", 1417 | "rustls-pki-types", 1418 | "serde", 1419 | "serde_json", 1420 | "serde_urlencoded", 1421 | "sync_wrapper", 1422 | "tokio", 1423 | "tokio-native-tls", 1424 | "tokio-rustls", 1425 | "tokio-socks", 1426 | "tokio-util", 1427 | "tower-service", 1428 | "url", 1429 | "wasm-bindgen", 1430 | "wasm-bindgen-futures", 1431 | "web-sys", 1432 | "webpki-roots", 1433 | "winreg", 1434 | ] 1435 | 1436 | [[package]] 1437 | name = "ring" 1438 | version = "0.17.8" 1439 | source = "registry+https://github.com/rust-lang/crates.io-index" 1440 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 1441 | dependencies = [ 1442 | "cc", 1443 | "cfg-if", 1444 | "getrandom 0.2.15", 1445 | "libc", 1446 | "spin", 1447 | "untrusted", 1448 | "windows-sys 0.52.0", 1449 | ] 1450 | 1451 | [[package]] 1452 | name = "rss" 1453 | version = "2.0.8" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "2f374fd66bb795938b78c021db1662d43a8ffbc42ec1ac25429fc4833b732751" 1456 | dependencies = [ 1457 | "atom_syndication", 1458 | "derive_builder", 1459 | "never", 1460 | "quick-xml", 1461 | ] 1462 | 1463 | [[package]] 1464 | name = "rsspls" 1465 | version = "0.10.0" 1466 | dependencies = [ 1467 | "anydate", 1468 | "atomicwrites", 1469 | "basic-toml", 1470 | "chrono", 1471 | "cryptoxide", 1472 | "dirs", 1473 | "futures", 1474 | "kuchiki", 1475 | "log", 1476 | "mime_guess", 1477 | "pico-args", 1478 | "pretty_env_logger", 1479 | "reqwest", 1480 | "rss", 1481 | "serde", 1482 | "simple-eyre", 1483 | "time", 1484 | "tokio", 1485 | "url", 1486 | "xdg", 1487 | ] 1488 | 1489 | [[package]] 1490 | name = "rustc-demangle" 1491 | version = "0.1.24" 1492 | source = "registry+https://github.com/rust-lang/crates.io-index" 1493 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1494 | 1495 | [[package]] 1496 | name = "rustc-hash" 1497 | version = "2.0.0" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" 1500 | 1501 | [[package]] 1502 | name = "rustc_version" 1503 | version = "0.4.0" 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" 1505 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 1506 | dependencies = [ 1507 | "semver", 1508 | ] 1509 | 1510 | [[package]] 1511 | name = "rustix" 1512 | version = "0.38.34" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 1515 | dependencies = [ 1516 | "bitflags 2.6.0", 1517 | "errno", 1518 | "libc", 1519 | "linux-raw-sys", 1520 | "windows-sys 0.52.0", 1521 | ] 1522 | 1523 | [[package]] 1524 | name = "rustls" 1525 | version = "0.23.12" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" 1528 | dependencies = [ 1529 | "once_cell", 1530 | "ring", 1531 | "rustls-pki-types", 1532 | "rustls-webpki", 1533 | "subtle", 1534 | "zeroize", 1535 | ] 1536 | 1537 | [[package]] 1538 | name = "rustls-pemfile" 1539 | version = "2.1.3" 1540 | source = "registry+https://github.com/rust-lang/crates.io-index" 1541 | checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" 1542 | dependencies = [ 1543 | "base64", 1544 | "rustls-pki-types", 1545 | ] 1546 | 1547 | [[package]] 1548 | name = "rustls-pki-types" 1549 | version = "1.8.0" 1550 | source = "registry+https://github.com/rust-lang/crates.io-index" 1551 | checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" 1552 | 1553 | [[package]] 1554 | name = "rustls-webpki" 1555 | version = "0.102.6" 1556 | source = "registry+https://github.com/rust-lang/crates.io-index" 1557 | checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" 1558 | dependencies = [ 1559 | "ring", 1560 | "rustls-pki-types", 1561 | "untrusted", 1562 | ] 1563 | 1564 | [[package]] 1565 | name = "ryu" 1566 | version = "1.0.18" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1569 | 1570 | [[package]] 1571 | name = "schannel" 1572 | version = "0.1.23" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" 1575 | dependencies = [ 1576 | "windows-sys 0.52.0", 1577 | ] 1578 | 1579 | [[package]] 1580 | name = "scopeguard" 1581 | version = "1.2.0" 1582 | source = "registry+https://github.com/rust-lang/crates.io-index" 1583 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1584 | 1585 | [[package]] 1586 | name = "security-framework" 1587 | version = "2.11.1" 1588 | source = "registry+https://github.com/rust-lang/crates.io-index" 1589 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1590 | dependencies = [ 1591 | "bitflags 2.6.0", 1592 | "core-foundation", 1593 | "core-foundation-sys", 1594 | "libc", 1595 | "security-framework-sys", 1596 | ] 1597 | 1598 | [[package]] 1599 | name = "security-framework-sys" 1600 | version = "2.11.1" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" 1603 | dependencies = [ 1604 | "core-foundation-sys", 1605 | "libc", 1606 | ] 1607 | 1608 | [[package]] 1609 | name = "selectors" 1610 | version = "0.22.0" 1611 | source = "registry+https://github.com/rust-lang/crates.io-index" 1612 | checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" 1613 | dependencies = [ 1614 | "bitflags 1.3.2", 1615 | "cssparser", 1616 | "derive_more", 1617 | "fxhash", 1618 | "log", 1619 | "matches", 1620 | "phf", 1621 | "phf_codegen", 1622 | "precomputed-hash", 1623 | "servo_arc", 1624 | "smallvec", 1625 | "thin-slice", 1626 | ] 1627 | 1628 | [[package]] 1629 | name = "semver" 1630 | version = "1.0.23" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 1633 | 1634 | [[package]] 1635 | name = "serde" 1636 | version = "1.0.205" 1637 | source = "registry+https://github.com/rust-lang/crates.io-index" 1638 | checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" 1639 | dependencies = [ 1640 | "serde_derive", 1641 | ] 1642 | 1643 | [[package]] 1644 | name = "serde_derive" 1645 | version = "1.0.205" 1646 | source = "registry+https://github.com/rust-lang/crates.io-index" 1647 | checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" 1648 | dependencies = [ 1649 | "proc-macro2", 1650 | "quote", 1651 | "syn 2.0.72", 1652 | ] 1653 | 1654 | [[package]] 1655 | name = "serde_json" 1656 | version = "1.0.122" 1657 | source = "registry+https://github.com/rust-lang/crates.io-index" 1658 | checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" 1659 | dependencies = [ 1660 | "itoa 1.0.11", 1661 | "memchr", 1662 | "ryu", 1663 | "serde", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "serde_urlencoded" 1668 | version = "0.7.1" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1671 | dependencies = [ 1672 | "form_urlencoded", 1673 | "itoa 1.0.11", 1674 | "ryu", 1675 | "serde", 1676 | ] 1677 | 1678 | [[package]] 1679 | name = "servo_arc" 1680 | version = "0.1.1" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" 1683 | dependencies = [ 1684 | "nodrop", 1685 | "stable_deref_trait", 1686 | ] 1687 | 1688 | [[package]] 1689 | name = "simple-eyre" 1690 | version = "0.3.1" 1691 | source = "registry+https://github.com/rust-lang/crates.io-index" 1692 | checksum = "1b561532e8ffe7ecf09108c4f662896a9ec3eac4999eba84015ec3dcb8cc630a" 1693 | dependencies = [ 1694 | "eyre", 1695 | "indenter", 1696 | ] 1697 | 1698 | [[package]] 1699 | name = "siphasher" 1700 | version = "0.3.11" 1701 | source = "registry+https://github.com/rust-lang/crates.io-index" 1702 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 1703 | 1704 | [[package]] 1705 | name = "slab" 1706 | version = "0.4.9" 1707 | source = "registry+https://github.com/rust-lang/crates.io-index" 1708 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1709 | dependencies = [ 1710 | "autocfg", 1711 | ] 1712 | 1713 | [[package]] 1714 | name = "smallvec" 1715 | version = "1.13.2" 1716 | source = "registry+https://github.com/rust-lang/crates.io-index" 1717 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1718 | 1719 | [[package]] 1720 | name = "socket2" 1721 | version = "0.5.7" 1722 | source = "registry+https://github.com/rust-lang/crates.io-index" 1723 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 1724 | dependencies = [ 1725 | "libc", 1726 | "windows-sys 0.52.0", 1727 | ] 1728 | 1729 | [[package]] 1730 | name = "spin" 1731 | version = "0.9.8" 1732 | source = "registry+https://github.com/rust-lang/crates.io-index" 1733 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1734 | 1735 | [[package]] 1736 | name = "stable_deref_trait" 1737 | version = "1.2.0" 1738 | source = "registry+https://github.com/rust-lang/crates.io-index" 1739 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1740 | 1741 | [[package]] 1742 | name = "string_cache" 1743 | version = "0.8.7" 1744 | source = "registry+https://github.com/rust-lang/crates.io-index" 1745 | checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" 1746 | dependencies = [ 1747 | "new_debug_unreachable", 1748 | "once_cell", 1749 | "parking_lot", 1750 | "phf_shared 0.10.0", 1751 | "precomputed-hash", 1752 | "serde", 1753 | ] 1754 | 1755 | [[package]] 1756 | name = "string_cache_codegen" 1757 | version = "0.5.2" 1758 | source = "registry+https://github.com/rust-lang/crates.io-index" 1759 | checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" 1760 | dependencies = [ 1761 | "phf_generator 0.10.0", 1762 | "phf_shared 0.10.0", 1763 | "proc-macro2", 1764 | "quote", 1765 | ] 1766 | 1767 | [[package]] 1768 | name = "strsim" 1769 | version = "0.11.1" 1770 | source = "registry+https://github.com/rust-lang/crates.io-index" 1771 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1772 | 1773 | [[package]] 1774 | name = "subtle" 1775 | version = "2.6.1" 1776 | source = "registry+https://github.com/rust-lang/crates.io-index" 1777 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1778 | 1779 | [[package]] 1780 | name = "syn" 1781 | version = "1.0.109" 1782 | source = "registry+https://github.com/rust-lang/crates.io-index" 1783 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1784 | dependencies = [ 1785 | "proc-macro2", 1786 | "quote", 1787 | "unicode-ident", 1788 | ] 1789 | 1790 | [[package]] 1791 | name = "syn" 1792 | version = "2.0.72" 1793 | source = "registry+https://github.com/rust-lang/crates.io-index" 1794 | checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" 1795 | dependencies = [ 1796 | "proc-macro2", 1797 | "quote", 1798 | "unicode-ident", 1799 | ] 1800 | 1801 | [[package]] 1802 | name = "sync_wrapper" 1803 | version = "1.0.1" 1804 | source = "registry+https://github.com/rust-lang/crates.io-index" 1805 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" 1806 | 1807 | [[package]] 1808 | name = "tempfile" 1809 | version = "3.12.0" 1810 | source = "registry+https://github.com/rust-lang/crates.io-index" 1811 | checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" 1812 | dependencies = [ 1813 | "cfg-if", 1814 | "fastrand", 1815 | "once_cell", 1816 | "rustix", 1817 | "windows-sys 0.59.0", 1818 | ] 1819 | 1820 | [[package]] 1821 | name = "tendril" 1822 | version = "0.4.3" 1823 | source = "registry+https://github.com/rust-lang/crates.io-index" 1824 | checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" 1825 | dependencies = [ 1826 | "futf", 1827 | "mac", 1828 | "utf-8", 1829 | ] 1830 | 1831 | [[package]] 1832 | name = "termcolor" 1833 | version = "1.4.1" 1834 | source = "registry+https://github.com/rust-lang/crates.io-index" 1835 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1836 | dependencies = [ 1837 | "winapi-util", 1838 | ] 1839 | 1840 | [[package]] 1841 | name = "thin-slice" 1842 | version = "0.1.1" 1843 | source = "registry+https://github.com/rust-lang/crates.io-index" 1844 | checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" 1845 | 1846 | [[package]] 1847 | name = "thiserror" 1848 | version = "1.0.63" 1849 | source = "registry+https://github.com/rust-lang/crates.io-index" 1850 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 1851 | dependencies = [ 1852 | "thiserror-impl", 1853 | ] 1854 | 1855 | [[package]] 1856 | name = "thiserror-impl" 1857 | version = "1.0.63" 1858 | source = "registry+https://github.com/rust-lang/crates.io-index" 1859 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 1860 | dependencies = [ 1861 | "proc-macro2", 1862 | "quote", 1863 | "syn 2.0.72", 1864 | ] 1865 | 1866 | [[package]] 1867 | name = "time" 1868 | version = "0.3.36" 1869 | source = "registry+https://github.com/rust-lang/crates.io-index" 1870 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1871 | dependencies = [ 1872 | "deranged", 1873 | "itoa 1.0.11", 1874 | "num-conv", 1875 | "powerfmt", 1876 | "serde", 1877 | "time-core", 1878 | "time-macros", 1879 | ] 1880 | 1881 | [[package]] 1882 | name = "time-core" 1883 | version = "0.1.2" 1884 | source = "registry+https://github.com/rust-lang/crates.io-index" 1885 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1886 | 1887 | [[package]] 1888 | name = "time-macros" 1889 | version = "0.2.18" 1890 | source = "registry+https://github.com/rust-lang/crates.io-index" 1891 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 1892 | dependencies = [ 1893 | "num-conv", 1894 | "time-core", 1895 | ] 1896 | 1897 | [[package]] 1898 | name = "tinyvec" 1899 | version = "1.8.0" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1902 | dependencies = [ 1903 | "tinyvec_macros", 1904 | ] 1905 | 1906 | [[package]] 1907 | name = "tinyvec_macros" 1908 | version = "0.1.1" 1909 | source = "registry+https://github.com/rust-lang/crates.io-index" 1910 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1911 | 1912 | [[package]] 1913 | name = "tokio" 1914 | version = "1.39.2" 1915 | source = "registry+https://github.com/rust-lang/crates.io-index" 1916 | checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" 1917 | dependencies = [ 1918 | "backtrace", 1919 | "bytes", 1920 | "libc", 1921 | "mio", 1922 | "pin-project-lite", 1923 | "socket2", 1924 | "tokio-macros", 1925 | "windows-sys 0.52.0", 1926 | ] 1927 | 1928 | [[package]] 1929 | name = "tokio-macros" 1930 | version = "2.4.0" 1931 | source = "registry+https://github.com/rust-lang/crates.io-index" 1932 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 1933 | dependencies = [ 1934 | "proc-macro2", 1935 | "quote", 1936 | "syn 2.0.72", 1937 | ] 1938 | 1939 | [[package]] 1940 | name = "tokio-native-tls" 1941 | version = "0.3.1" 1942 | source = "registry+https://github.com/rust-lang/crates.io-index" 1943 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1944 | dependencies = [ 1945 | "native-tls", 1946 | "tokio", 1947 | ] 1948 | 1949 | [[package]] 1950 | name = "tokio-rustls" 1951 | version = "0.26.0" 1952 | source = "registry+https://github.com/rust-lang/crates.io-index" 1953 | checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" 1954 | dependencies = [ 1955 | "rustls", 1956 | "rustls-pki-types", 1957 | "tokio", 1958 | ] 1959 | 1960 | [[package]] 1961 | name = "tokio-socks" 1962 | version = "0.5.2" 1963 | source = "registry+https://github.com/rust-lang/crates.io-index" 1964 | checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" 1965 | dependencies = [ 1966 | "either", 1967 | "futures-util", 1968 | "thiserror", 1969 | "tokio", 1970 | ] 1971 | 1972 | [[package]] 1973 | name = "tokio-util" 1974 | version = "0.7.11" 1975 | source = "registry+https://github.com/rust-lang/crates.io-index" 1976 | checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" 1977 | dependencies = [ 1978 | "bytes", 1979 | "futures-core", 1980 | "futures-sink", 1981 | "pin-project-lite", 1982 | "tokio", 1983 | ] 1984 | 1985 | [[package]] 1986 | name = "tower" 1987 | version = "0.4.13" 1988 | source = "registry+https://github.com/rust-lang/crates.io-index" 1989 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 1990 | dependencies = [ 1991 | "futures-core", 1992 | "futures-util", 1993 | "pin-project", 1994 | "pin-project-lite", 1995 | "tokio", 1996 | "tower-layer", 1997 | "tower-service", 1998 | ] 1999 | 2000 | [[package]] 2001 | name = "tower-layer" 2002 | version = "0.3.2" 2003 | source = "registry+https://github.com/rust-lang/crates.io-index" 2004 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 2005 | 2006 | [[package]] 2007 | name = "tower-service" 2008 | version = "0.3.2" 2009 | source = "registry+https://github.com/rust-lang/crates.io-index" 2010 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 2011 | 2012 | [[package]] 2013 | name = "tracing" 2014 | version = "0.1.40" 2015 | source = "registry+https://github.com/rust-lang/crates.io-index" 2016 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 2017 | dependencies = [ 2018 | "pin-project-lite", 2019 | "tracing-core", 2020 | ] 2021 | 2022 | [[package]] 2023 | name = "tracing-core" 2024 | version = "0.1.32" 2025 | source = "registry+https://github.com/rust-lang/crates.io-index" 2026 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 2027 | dependencies = [ 2028 | "once_cell", 2029 | ] 2030 | 2031 | [[package]] 2032 | name = "try-lock" 2033 | version = "0.2.5" 2034 | source = "registry+https://github.com/rust-lang/crates.io-index" 2035 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2036 | 2037 | [[package]] 2038 | name = "unicase" 2039 | version = "2.7.0" 2040 | source = "registry+https://github.com/rust-lang/crates.io-index" 2041 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 2042 | dependencies = [ 2043 | "version_check", 2044 | ] 2045 | 2046 | [[package]] 2047 | name = "unicode-bidi" 2048 | version = "0.3.15" 2049 | source = "registry+https://github.com/rust-lang/crates.io-index" 2050 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 2051 | 2052 | [[package]] 2053 | name = "unicode-ident" 2054 | version = "1.0.12" 2055 | source = "registry+https://github.com/rust-lang/crates.io-index" 2056 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 2057 | 2058 | [[package]] 2059 | name = "unicode-normalization" 2060 | version = "0.1.23" 2061 | source = "registry+https://github.com/rust-lang/crates.io-index" 2062 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 2063 | dependencies = [ 2064 | "tinyvec", 2065 | ] 2066 | 2067 | [[package]] 2068 | name = "untrusted" 2069 | version = "0.9.0" 2070 | source = "registry+https://github.com/rust-lang/crates.io-index" 2071 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2072 | 2073 | [[package]] 2074 | name = "url" 2075 | version = "2.5.2" 2076 | source = "registry+https://github.com/rust-lang/crates.io-index" 2077 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 2078 | dependencies = [ 2079 | "form_urlencoded", 2080 | "idna", 2081 | "percent-encoding", 2082 | ] 2083 | 2084 | [[package]] 2085 | name = "utf-8" 2086 | version = "0.7.6" 2087 | source = "registry+https://github.com/rust-lang/crates.io-index" 2088 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 2089 | 2090 | [[package]] 2091 | name = "vcpkg" 2092 | version = "0.2.15" 2093 | source = "registry+https://github.com/rust-lang/crates.io-index" 2094 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2095 | 2096 | [[package]] 2097 | name = "version_check" 2098 | version = "0.9.5" 2099 | source = "registry+https://github.com/rust-lang/crates.io-index" 2100 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2101 | 2102 | [[package]] 2103 | name = "want" 2104 | version = "0.3.1" 2105 | source = "registry+https://github.com/rust-lang/crates.io-index" 2106 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2107 | dependencies = [ 2108 | "try-lock", 2109 | ] 2110 | 2111 | [[package]] 2112 | name = "wasi" 2113 | version = "0.9.0+wasi-snapshot-preview1" 2114 | source = "registry+https://github.com/rust-lang/crates.io-index" 2115 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 2116 | 2117 | [[package]] 2118 | name = "wasi" 2119 | version = "0.11.0+wasi-snapshot-preview1" 2120 | source = "registry+https://github.com/rust-lang/crates.io-index" 2121 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2122 | 2123 | [[package]] 2124 | name = "wasm-bindgen" 2125 | version = "0.2.92" 2126 | source = "registry+https://github.com/rust-lang/crates.io-index" 2127 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 2128 | dependencies = [ 2129 | "cfg-if", 2130 | "wasm-bindgen-macro", 2131 | ] 2132 | 2133 | [[package]] 2134 | name = "wasm-bindgen-backend" 2135 | version = "0.2.92" 2136 | source = "registry+https://github.com/rust-lang/crates.io-index" 2137 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 2138 | dependencies = [ 2139 | "bumpalo", 2140 | "log", 2141 | "once_cell", 2142 | "proc-macro2", 2143 | "quote", 2144 | "syn 2.0.72", 2145 | "wasm-bindgen-shared", 2146 | ] 2147 | 2148 | [[package]] 2149 | name = "wasm-bindgen-futures" 2150 | version = "0.4.42" 2151 | source = "registry+https://github.com/rust-lang/crates.io-index" 2152 | checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" 2153 | dependencies = [ 2154 | "cfg-if", 2155 | "js-sys", 2156 | "wasm-bindgen", 2157 | "web-sys", 2158 | ] 2159 | 2160 | [[package]] 2161 | name = "wasm-bindgen-macro" 2162 | version = "0.2.92" 2163 | source = "registry+https://github.com/rust-lang/crates.io-index" 2164 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 2165 | dependencies = [ 2166 | "quote", 2167 | "wasm-bindgen-macro-support", 2168 | ] 2169 | 2170 | [[package]] 2171 | name = "wasm-bindgen-macro-support" 2172 | version = "0.2.92" 2173 | source = "registry+https://github.com/rust-lang/crates.io-index" 2174 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 2175 | dependencies = [ 2176 | "proc-macro2", 2177 | "quote", 2178 | "syn 2.0.72", 2179 | "wasm-bindgen-backend", 2180 | "wasm-bindgen-shared", 2181 | ] 2182 | 2183 | [[package]] 2184 | name = "wasm-bindgen-shared" 2185 | version = "0.2.92" 2186 | source = "registry+https://github.com/rust-lang/crates.io-index" 2187 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 2188 | 2189 | [[package]] 2190 | name = "web-sys" 2191 | version = "0.3.69" 2192 | source = "registry+https://github.com/rust-lang/crates.io-index" 2193 | checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" 2194 | dependencies = [ 2195 | "js-sys", 2196 | "wasm-bindgen", 2197 | ] 2198 | 2199 | [[package]] 2200 | name = "webpki-roots" 2201 | version = "0.26.3" 2202 | source = "registry+https://github.com/rust-lang/crates.io-index" 2203 | checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" 2204 | dependencies = [ 2205 | "rustls-pki-types", 2206 | ] 2207 | 2208 | [[package]] 2209 | name = "winapi-util" 2210 | version = "0.1.9" 2211 | source = "registry+https://github.com/rust-lang/crates.io-index" 2212 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 2213 | dependencies = [ 2214 | "windows-sys 0.59.0", 2215 | ] 2216 | 2217 | [[package]] 2218 | name = "windows-sys" 2219 | version = "0.48.0" 2220 | source = "registry+https://github.com/rust-lang/crates.io-index" 2221 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2222 | dependencies = [ 2223 | "windows-targets 0.48.5", 2224 | ] 2225 | 2226 | [[package]] 2227 | name = "windows-sys" 2228 | version = "0.52.0" 2229 | source = "registry+https://github.com/rust-lang/crates.io-index" 2230 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2231 | dependencies = [ 2232 | "windows-targets 0.52.6", 2233 | ] 2234 | 2235 | [[package]] 2236 | name = "windows-sys" 2237 | version = "0.59.0" 2238 | source = "registry+https://github.com/rust-lang/crates.io-index" 2239 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2240 | dependencies = [ 2241 | "windows-targets 0.52.6", 2242 | ] 2243 | 2244 | [[package]] 2245 | name = "windows-targets" 2246 | version = "0.48.5" 2247 | source = "registry+https://github.com/rust-lang/crates.io-index" 2248 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2249 | dependencies = [ 2250 | "windows_aarch64_gnullvm 0.48.5", 2251 | "windows_aarch64_msvc 0.48.5", 2252 | "windows_i686_gnu 0.48.5", 2253 | "windows_i686_msvc 0.48.5", 2254 | "windows_x86_64_gnu 0.48.5", 2255 | "windows_x86_64_gnullvm 0.48.5", 2256 | "windows_x86_64_msvc 0.48.5", 2257 | ] 2258 | 2259 | [[package]] 2260 | name = "windows-targets" 2261 | version = "0.52.6" 2262 | source = "registry+https://github.com/rust-lang/crates.io-index" 2263 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2264 | dependencies = [ 2265 | "windows_aarch64_gnullvm 0.52.6", 2266 | "windows_aarch64_msvc 0.52.6", 2267 | "windows_i686_gnu 0.52.6", 2268 | "windows_i686_gnullvm", 2269 | "windows_i686_msvc 0.52.6", 2270 | "windows_x86_64_gnu 0.52.6", 2271 | "windows_x86_64_gnullvm 0.52.6", 2272 | "windows_x86_64_msvc 0.52.6", 2273 | ] 2274 | 2275 | [[package]] 2276 | name = "windows_aarch64_gnullvm" 2277 | version = "0.48.5" 2278 | source = "registry+https://github.com/rust-lang/crates.io-index" 2279 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2280 | 2281 | [[package]] 2282 | name = "windows_aarch64_gnullvm" 2283 | version = "0.52.6" 2284 | source = "registry+https://github.com/rust-lang/crates.io-index" 2285 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2286 | 2287 | [[package]] 2288 | name = "windows_aarch64_msvc" 2289 | version = "0.48.5" 2290 | source = "registry+https://github.com/rust-lang/crates.io-index" 2291 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2292 | 2293 | [[package]] 2294 | name = "windows_aarch64_msvc" 2295 | version = "0.52.6" 2296 | source = "registry+https://github.com/rust-lang/crates.io-index" 2297 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2298 | 2299 | [[package]] 2300 | name = "windows_i686_gnu" 2301 | version = "0.48.5" 2302 | source = "registry+https://github.com/rust-lang/crates.io-index" 2303 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2304 | 2305 | [[package]] 2306 | name = "windows_i686_gnu" 2307 | version = "0.52.6" 2308 | source = "registry+https://github.com/rust-lang/crates.io-index" 2309 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2310 | 2311 | [[package]] 2312 | name = "windows_i686_gnullvm" 2313 | version = "0.52.6" 2314 | source = "registry+https://github.com/rust-lang/crates.io-index" 2315 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2316 | 2317 | [[package]] 2318 | name = "windows_i686_msvc" 2319 | version = "0.48.5" 2320 | source = "registry+https://github.com/rust-lang/crates.io-index" 2321 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2322 | 2323 | [[package]] 2324 | name = "windows_i686_msvc" 2325 | version = "0.52.6" 2326 | source = "registry+https://github.com/rust-lang/crates.io-index" 2327 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2328 | 2329 | [[package]] 2330 | name = "windows_x86_64_gnu" 2331 | version = "0.48.5" 2332 | source = "registry+https://github.com/rust-lang/crates.io-index" 2333 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2334 | 2335 | [[package]] 2336 | name = "windows_x86_64_gnu" 2337 | version = "0.52.6" 2338 | source = "registry+https://github.com/rust-lang/crates.io-index" 2339 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2340 | 2341 | [[package]] 2342 | name = "windows_x86_64_gnullvm" 2343 | version = "0.48.5" 2344 | source = "registry+https://github.com/rust-lang/crates.io-index" 2345 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2346 | 2347 | [[package]] 2348 | name = "windows_x86_64_gnullvm" 2349 | version = "0.52.6" 2350 | source = "registry+https://github.com/rust-lang/crates.io-index" 2351 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2352 | 2353 | [[package]] 2354 | name = "windows_x86_64_msvc" 2355 | version = "0.48.5" 2356 | source = "registry+https://github.com/rust-lang/crates.io-index" 2357 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2358 | 2359 | [[package]] 2360 | name = "windows_x86_64_msvc" 2361 | version = "0.52.6" 2362 | source = "registry+https://github.com/rust-lang/crates.io-index" 2363 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2364 | 2365 | [[package]] 2366 | name = "winreg" 2367 | version = "0.52.0" 2368 | source = "registry+https://github.com/rust-lang/crates.io-index" 2369 | checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" 2370 | dependencies = [ 2371 | "cfg-if", 2372 | "windows-sys 0.48.0", 2373 | ] 2374 | 2375 | [[package]] 2376 | name = "xdg" 2377 | version = "2.5.2" 2378 | source = "registry+https://github.com/rust-lang/crates.io-index" 2379 | checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" 2380 | 2381 | [[package]] 2382 | name = "zerocopy" 2383 | version = "0.7.35" 2384 | source = "registry+https://github.com/rust-lang/crates.io-index" 2385 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 2386 | dependencies = [ 2387 | "byteorder", 2388 | "zerocopy-derive", 2389 | ] 2390 | 2391 | [[package]] 2392 | name = "zerocopy-derive" 2393 | version = "0.7.35" 2394 | source = "registry+https://github.com/rust-lang/crates.io-index" 2395 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 2396 | dependencies = [ 2397 | "proc-macro2", 2398 | "quote", 2399 | "syn 2.0.72", 2400 | ] 2401 | 2402 | [[package]] 2403 | name = "zeroize" 2404 | version = "1.8.1" 2405 | source = "registry+https://github.com/rust-lang/crates.io-index" 2406 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2407 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsspls" 3 | version = "0.10.0" 4 | edition = "2021" 5 | authors = [ 6 | "Wesley Moore " 7 | ] 8 | 9 | homepage = "https://github.com/wezm/rsspls" 10 | repository = "https://github.com/wezm/rsspls.git" 11 | 12 | readme = "README.md" 13 | license = "MIT OR Apache-2.0" 14 | 15 | description = "Generate RSS feeds from websites" 16 | keywords = ["rss", "cli", "html", "webpage", "feed"] 17 | categories = ["command-line-utilities", "web-programming"] 18 | 19 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 20 | 21 | [dependencies] 22 | anydate = "0.4.0" 23 | atomicwrites = "0.4.3" 24 | basic-toml = "0.1.9" 25 | chrono = { version = "0.4.38", default-features = false } 26 | cryptoxide = { version = "0.4.4", features = ["blake2"], default-features = false } 27 | futures = { version = "0.3.30", default-features = false, features = ["std"] } 28 | kuchiki = "0.8.1" 29 | log = "0.4.22" 30 | mime_guess = { version = "2.0.5", default-features = false } 31 | pico-args = "0.5.0" 32 | pretty_env_logger = "0.5.0" 33 | reqwest = { version = "0.12.5", default-features = false, features = ["gzip", "socks"] } 34 | rss = "2.0.8" 35 | serde = { version = "1.0.205", features = ["derive"] } 36 | simple-eyre = "0.3.1" 37 | tokio = { version = "1.39.2", features = ["rt-multi-thread", "macros"] } 38 | url = "2.5.2" 39 | 40 | [dependencies.time] 41 | version = "0.3.36" 42 | features = ["parsing", "formatting", "macros"] 43 | 44 | [target.'cfg(windows)'.dependencies] 45 | dirs = "5.0.1" 46 | 47 | [target.'cfg(not(windows))'.dependencies] 48 | xdg = "2.5.2" 49 | 50 | [profile.release] 51 | strip = "debuginfo" 52 | 53 | [features] 54 | default = ["rust-tls"] 55 | native-tls = ["reqwest/native-tls"] 56 | rust-tls = ["reqwest/rustls-tls"] 57 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Wesley Moore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | RSS Please 4 |

5 | 6 |
7 | A small tool (rsspls) to generate RSS feeds from web 8 | pages that lack them. It runs on BSD, Linux, macOS, Windows, and 9 | more. 10 |
11 | 12 |
13 | 14 |
15 | 16 | Build Status 17 | 18 | Version 19 | 20 | License 21 |
22 | 23 |
24 | 25 | `rsspls` generates RSS feeds from web pages. Example use cases: 26 | 27 | * Create a feed for a blog that does not have one so that you will know when 28 | there are new posts. 29 | * Create a feed from the search results on real estate agent's website so that 30 | you know when there are new listings—without having to check manually all the 31 | time. 32 | * Create a feed of the upcoming tour dates of your favourite band or DJ. 33 | * Create a feed of the product page for a company, so you know when new 34 | products are added. 35 | 36 | The idea is that you will then subscribe to the generated feeds in your feed 37 | reader. This will typically require the feeds to be hosted via a web server. 38 | 39 | For more information including installation instructions, documentation, and 40 | news visit the [RSS Please website][website]. 41 | 42 |
43 | Visit Website 44 |
45 | 46 | Build From Source 47 | ----------------- 48 | 49 | **Minimum Supported Rust Version:** 1.70.0 50 | 51 | `rsspls` is implemented in Rust. See the Rust website for [instructions on 52 | installing the toolchain][rustup]. 53 | 54 | ### From Git Checkout or Release Tarball 55 | 56 | Build the binary with `cargo build --release --locked`. The binary will be in 57 | `target/release/rsspls`. 58 | 59 | ### From crates.io 60 | 61 | `cargo install rsspls` 62 | 63 | Credits 64 | ------- 65 | 66 | * [RSS feed icon](http://www.feedicons.com/) by The Mozilla Foundation 67 | 68 | Licence 69 | ------- 70 | 71 | This project is dual licenced under either of: 72 | 73 | - Apache License, Version 2.0 ([LICENSE-APACHE](https://github.com/wezm/rsspls/blob/master/LICENSE-APACHE)) 74 | - MIT license ([LICENSE-MIT](https://github.com/wezm/rsspls/blob/master/LICENSE-MIT)) 75 | 76 | at your option. 77 | 78 | [rustup]: https://www.rust-lang.org/tools/install 79 | [website]: https://rsspls.7bit.org/ 80 | -------------------------------------------------------------------------------- /feed-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/cache.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | 4 | use basic_toml as toml; 5 | use log::debug; 6 | use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::config::ConfigHash; 10 | 11 | #[derive(Debug, Serialize)] 12 | pub struct RequestCacheWrite<'a> { 13 | pub headers: Vec<(&'a str, &'a str)>, 14 | pub version: &'a str, 15 | pub config_hash: ConfigHash<'a>, 16 | } 17 | 18 | #[derive(Debug, Deserialize)] 19 | struct RequestCacheRead { 20 | headers: Vec<(String, String)>, 21 | /// The version of rsspls that created this request cache 22 | /// 23 | /// May be missing if the cache was created by an older version. 24 | #[serde(default)] 25 | version: Option, 26 | /// Hash of the config 27 | /// 28 | /// Used as cache buster when config changes. 29 | /// 30 | /// May be missing if the cache was created by an older version. 31 | #[serde(default)] 32 | config_hash: Option, 33 | } 34 | 35 | pub fn deserialise_cached_headers( 36 | path: &Path, 37 | config_hash: ConfigHash<'_>, 38 | ) -> Option> { 39 | let raw = fs::read(path).ok()?; 40 | let cache: RequestCacheRead = toml::from_slice(&raw).ok()?; 41 | 42 | if cache.version.as_deref() != Some(crate::version()) { 43 | debug!( 44 | "cache version ({:?}) != to this version ({:?}), ignoring cache at: {}", 45 | cache.version, 46 | crate::version(), 47 | path.display() 48 | ); 49 | return None; 50 | } else if cache.config_hash.as_deref() != Some(config_hash.0) { 51 | debug!( 52 | "cache config hash mismatch ({:?}) != ({:?}), ignoring cache at: {}", 53 | cache.config_hash, 54 | config_hash, 55 | path.display() 56 | ); 57 | return None; 58 | } 59 | 60 | debug!("using cache at: {}", path.display()); 61 | Some( 62 | cache 63 | .headers 64 | .into_iter() 65 | .filter_map(|(name, value)| { 66 | HeaderName::try_from(name) 67 | .ok() 68 | .zip(HeaderValue::try_from(value).ok()) 69 | }) 70 | .collect(), 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::env; 3 | use std::ffi::OsStr; 4 | use std::path::PathBuf; 5 | 6 | use pico_args::Arguments; 7 | use simple_eyre::eyre; 8 | 9 | use crate::version_string; 10 | 11 | pub struct Cli { 12 | pub config_path: Option, 13 | pub output_path: Option, 14 | } 15 | 16 | pub fn parse_args() -> eyre::Result> { 17 | let mut pargs = Arguments::from_env(); 18 | if pargs.contains(["-V", "--version"]) { 19 | return print_version(); 20 | } else if pargs.contains(["-h", "--help"]) { 21 | return print_usage(); 22 | } 23 | 24 | Ok(Some(Cli { 25 | config_path: pargs.opt_value_from_os_str(["-c", "--config"], pathbuf)?, 26 | output_path: pargs.opt_value_from_os_str(["-o", "--output"], pathbuf)?, 27 | })) 28 | } 29 | 30 | fn pathbuf(s: &OsStr) -> Result { 31 | Ok(PathBuf::from(s)) 32 | } 33 | 34 | fn print_version() -> eyre::Result> { 35 | println!("{}", version_string()); 36 | Ok(None) 37 | } 38 | 39 | pub fn print_usage() -> eyre::Result> { 40 | println!( 41 | "{} 42 | 43 | {bin} generates RSS feeds from web pages. 44 | 45 | USAGE: 46 | {bin} [OPTIONS] -o OUTPUT_DIR 47 | 48 | OPTIONS: 49 | -h, --help 50 | Prints this help information 51 | 52 | -c, --config 53 | Specify the path to the configuration file. 54 | $XDG_CONFIG_HOME/rsspls/feeds.toml is used if not supplied. 55 | 56 | -o, --output 57 | Directory to write generated feeds to. 58 | 59 | -V, --version 60 | Prints version information 61 | 62 | FILES: 63 | ~/$XDG_CONFIG_HOME/rsspls/feeds.toml rsspls configuration file. 64 | 65 | ~/$XDG_CONFIG_HOME/rsspls Configuration directory. 66 | 67 | ~/XDG_CACHE_HOME/rsspls Cache directory. 68 | 69 | Note: XDG_CONFIG_HOME defaults to ~/.config, XDG_CACHE_HOME 70 | defaults to ~/.cache. 71 | 72 | AUTHOR 73 | {} 74 | 75 | SEE ALSO 76 | https://github.com/wezm/rsspls Source code and issue tracker.", 77 | version_string(), 78 | env!("CARGO_PKG_AUTHORS"), 79 | bin = env!("CARGO_PKG_NAME") 80 | ); 81 | Ok(None) 82 | } 83 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::marker::PhantomData; 3 | use std::path::PathBuf; 4 | use std::str::FromStr; 5 | use std::{fmt, fs}; 6 | 7 | use basic_toml as toml; 8 | use cryptoxide::{blake2b::Blake2b, digest::Digest}; 9 | use eyre::WrapErr; 10 | use log::{debug, warn}; 11 | use serde::{de, Deserialize, Deserializer, Serialize}; 12 | use simple_eyre::eyre; 13 | use time::format_description::OwnedFormatItem; 14 | use time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; 15 | 16 | #[derive(Debug, Eq, PartialEq, Serialize, Clone, Copy)] 17 | pub struct ConfigHash<'a>(pub &'a str); 18 | 19 | #[derive(Debug, Deserialize)] 20 | pub struct Config { 21 | pub rsspls: RssplsConfig, 22 | pub feed: Vec, 23 | /// Blake2b digest of the config file 24 | #[serde(skip)] 25 | pub hash: String, 26 | } 27 | 28 | #[derive(Debug, Deserialize)] 29 | pub struct RssplsConfig { 30 | pub output: Option, 31 | pub proxy: Option, 32 | /// Whether to allow fetching web pages from file URLs 33 | #[serde(default)] 34 | pub file_urls: bool, 35 | } 36 | 37 | #[derive(Debug, Deserialize)] 38 | pub struct ChannelConfig { 39 | pub title: String, 40 | pub filename: String, 41 | pub user_agent: Option, 42 | pub config: FeedConfig, 43 | } 44 | 45 | // TODO: Rename? 46 | #[derive(Debug, Deserialize)] 47 | pub struct FeedConfig { 48 | pub url: String, 49 | pub item: String, 50 | pub heading: String, 51 | pub link: Option, 52 | #[serde(default, deserialize_with = "string_or_seq_string")] 53 | pub summary: Vec, 54 | #[serde(default, deserialize_with = "opt_string_or_struct")] 55 | pub date: Option, 56 | pub media: Option, 57 | } 58 | 59 | #[derive(Debug, Default, Deserialize)] 60 | pub struct DateConfig { 61 | pub selector: String, 62 | #[serde(rename = "type", default)] 63 | type_: DateType, 64 | #[serde(deserialize_with = "deserialize_format")] 65 | pub format: Option, 66 | } 67 | 68 | #[derive(Debug, Default, Deserialize, Copy, Clone)] 69 | enum DateType { 70 | Date, 71 | #[default] 72 | DateTime, 73 | } 74 | 75 | impl Config { 76 | /// Read the config file path and the supplied path or default if None 77 | pub fn read(config_path: Option) -> eyre::Result { 78 | let dirs = crate::dirs::new()?; 79 | let config_path = config_path.ok_or(()).or_else(|()| { 80 | dirs.place_config_file("feeds.toml") 81 | .wrap_err("unable to create path to config file") 82 | })?; 83 | let raw_config = fs::read(&config_path).wrap_err_with(|| { 84 | format!( 85 | "unable to read configuration file: {}", 86 | config_path.display() 87 | ) 88 | })?; 89 | let mut context = Blake2b::new(32); 90 | context.input(&raw_config); 91 | let digest = context.result_str(); 92 | 93 | let mut config: Config = toml::from_slice(&raw_config).wrap_err_with(|| { 94 | format!( 95 | "unable to parse configuration file: {}", 96 | config_path.display() 97 | ) 98 | })?; 99 | config.hash = digest; 100 | Ok(config) 101 | } 102 | } 103 | 104 | impl DateConfig { 105 | pub fn selector(&self) -> &str { 106 | &self.selector 107 | } 108 | 109 | pub fn parse(&self, date: &str) -> eyre::Result { 110 | match self { 111 | DateConfig { format: None, .. } => { 112 | debug!("attempting to parse {} with anydate", date); 113 | anydate::parse(date) 114 | .map(|chrono| { 115 | // Convert chrono DateTime to time OffsetDateTime 116 | OffsetDateTime::from_unix_timestamp(chrono.timestamp()) 117 | .unwrap() 118 | .to_offset( 119 | UtcOffset::from_whole_seconds(chrono.timezone().local_minus_utc()) 120 | .unwrap(), 121 | ) 122 | }) 123 | .map_err(eyre::Report::from) 124 | } 125 | DateConfig { 126 | format: Some(format), 127 | .. 128 | } => { 129 | debug!("attempting to parse {} with supplied format", date); 130 | match self.type_ { 131 | DateType::Date => Date::parse(date, format) 132 | .map(|date| PrimitiveDateTime::new(date, Time::MIDNIGHT).assume_utc()) 133 | .map_err(|err| { 134 | debug!("parsing with format failed: {}", err); 135 | eyre::Report::from(err) 136 | }), 137 | DateType::DateTime => OffsetDateTime::parse(date, format) 138 | .or_else(|_| { 139 | PrimitiveDateTime::parse(date, format) 140 | .map(|primitive| primitive.assume_utc()) 141 | }) 142 | .map_err(|err| { 143 | debug!("parsing with format failed: {}", err); 144 | eyre::Report::from(err) 145 | }), 146 | } 147 | } 148 | } 149 | } 150 | } 151 | 152 | impl FromStr for DateConfig { 153 | // This implementation of `from_str` can never fail, so use the 154 | // `Infallible` type as the error type. 155 | type Err = Infallible; 156 | 157 | fn from_str(s: &str) -> Result { 158 | Ok(DateConfig { 159 | selector: s.to_string(), 160 | ..Default::default() 161 | }) 162 | } 163 | } 164 | 165 | pub fn deserialize_format<'de, D>(deserializer: D) -> Result, D::Error> 166 | where 167 | D: Deserializer<'de>, 168 | { 169 | let s: Option = Option::deserialize(deserializer)?; 170 | s.map(|s| time::format_description::parse_owned::<2>(&s)) 171 | .transpose() 172 | .map_err(|err| { 173 | warn!("unable to parse date format: {}", err); 174 | serde::de::Error::custom(err) 175 | }) 176 | } 177 | 178 | // https://serde.rs/string-or-struct.html 179 | fn string_or_struct<'de, T, D>(deserializer: D) -> Result 180 | where 181 | T: Deserialize<'de> + FromStr, 182 | D: Deserializer<'de>, 183 | { 184 | // This is a Visitor that forwards string types to T's `FromStr` impl and 185 | // forwards map types to T's `Deserialize` impl. The `PhantomData` is to 186 | // keep the compiler from complaining about T being an unused generic type 187 | // parameter. We need T in order to know the Value type for the Visitor 188 | // impl. 189 | struct StringOrStruct(PhantomData T>); 190 | 191 | impl<'de, T> de::Visitor<'de> for StringOrStruct 192 | where 193 | T: Deserialize<'de> + FromStr, 194 | { 195 | type Value = T; 196 | 197 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 198 | formatter.write_str("string or map") 199 | } 200 | 201 | fn visit_str(self, value: &str) -> Result 202 | where 203 | E: de::Error, 204 | { 205 | Ok(FromStr::from_str(value).unwrap()) 206 | } 207 | 208 | fn visit_map(self, map: M) -> Result 209 | where 210 | M: de::MapAccess<'de>, 211 | { 212 | // `MapAccessDeserializer` is a wrapper that turns a `MapAccess` 213 | // into a `Deserializer`, allowing it to be used as the input to T's 214 | // `Deserialize` implementation. T then deserializes itself using 215 | // the entries from the map visitor. 216 | Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) 217 | } 218 | } 219 | 220 | deserializer.deserialize_any(StringOrStruct(PhantomData)) 221 | } 222 | 223 | // https://stackoverflow.com/a/43627388/38820 224 | fn string_or_seq_string<'de, D>(deserializer: D) -> Result, D::Error> 225 | where 226 | D: Deserializer<'de>, 227 | { 228 | struct StringOrVec(PhantomData>); 229 | 230 | impl<'de> de::Visitor<'de> for StringOrVec { 231 | type Value = Vec; 232 | 233 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 234 | formatter.write_str("string or sequence of strings") 235 | } 236 | 237 | fn visit_str(self, value: &str) -> Result 238 | where 239 | E: de::Error, 240 | { 241 | Ok(vec![value.to_owned()]) 242 | } 243 | 244 | fn visit_seq(self, visitor: S) -> Result 245 | where 246 | S: de::SeqAccess<'de>, 247 | { 248 | Deserialize::deserialize(de::value::SeqAccessDeserializer::new(visitor)) 249 | } 250 | } 251 | 252 | deserializer.deserialize_any(StringOrVec(PhantomData)) 253 | } 254 | 255 | // https://github.com/emk/compose_yml/blob/7e8e0f47dcc41cf08e15fe082ef4c40b5f0475eb/src/v2/string_or_struct.rs#L69 256 | fn opt_string_or_struct<'de, T, D>(d: D) -> Result, D::Error> 257 | where 258 | T: Deserialize<'de> + FromStr, 259 | D: Deserializer<'de>, 260 | { 261 | /// Declare an internal visitor type to handle our input. 262 | struct OptStringOrStruct(PhantomData); 263 | 264 | impl<'de, T> de::Visitor<'de> for OptStringOrStruct 265 | where 266 | T: Deserialize<'de> + FromStr, 267 | { 268 | type Value = Option; 269 | 270 | fn visit_none(self) -> Result 271 | where 272 | E: de::Error, 273 | { 274 | Ok(None) 275 | } 276 | 277 | fn visit_some(self, deserializer: D) -> Result 278 | where 279 | D: Deserializer<'de>, 280 | { 281 | string_or_struct(deserializer).map(Some) 282 | } 283 | 284 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 285 | write!(formatter, "a null, a string or a map") 286 | } 287 | } 288 | 289 | d.deserialize_option(OptStringOrStruct(PhantomData)) 290 | } 291 | 292 | #[cfg(test)] 293 | mod tests { 294 | use super::*; 295 | 296 | fn test_date(format: &'static str) -> DateConfig { 297 | DateConfig { 298 | selector: String::new(), 299 | type_: DateType::Date, 300 | format: Some(time::format_description::parse_owned::<2>(format).unwrap()), 301 | } 302 | } 303 | 304 | fn test_anydate() -> DateConfig { 305 | DateConfig { 306 | selector: String::new(), 307 | type_: DateType::Date, 308 | format: None, 309 | } 310 | } 311 | 312 | #[test] 313 | fn test_without_format() { 314 | assert!(test_anydate().parse("January 8, 2021").is_ok()); 315 | assert!(test_anydate().parse("2022-07-13").is_ok()); 316 | assert!(test_anydate().parse("12/31/1999").is_ok()); 317 | } 318 | 319 | #[test] 320 | fn test_with_date_format() { 321 | assert!(test_date("[day padding:none]/[month padding:none]/[year]") 322 | .parse("1/2/1945") 323 | .is_ok()); 324 | assert!(test_date("[weekday case_sensitive:false], [month repr:long case_sensitive:false] [day padding:none][first [st][nd][rd][th]], [year]") 325 | .parse("Friday, January 8th, 2021").is_ok()); 326 | assert!(test_date("[weekday case_sensitive:false], [month repr:long case_sensitive:false] [day padding:none], [year]") 327 | .parse("Friday, January 8, 2021").is_ok()); 328 | } 329 | 330 | #[test] 331 | fn test_with_date_time_format() { 332 | assert!(test_date("[weekday case_sensitive:false], [month repr:long case_sensitive:false] [day padding:none][first [st][nd][rd][th]], [year] [hour repr:12]:[minute][period case:lower]") 333 | .parse("Friday, January 8th, 2021 12:13pm").is_ok()); 334 | assert!(test_date("[weekday case_sensitive:false], [month repr:long case_sensitive:false] [day padding:none], [year] [hour repr:24]:[minute]") 335 | .parse("Friday, January 8, 2021 21:33").is_ok()); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/dirs.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | use eyre::eyre; 5 | use simple_eyre::eyre; 6 | 7 | pub type Dirs = Arc>; 8 | 9 | pub struct BaseDirs; 10 | 11 | pub fn new() -> eyre::Result { 12 | Ok(BaseDirs) 13 | } 14 | 15 | pub fn home_dir() -> Option { 16 | ::dirs::home_dir() 17 | } 18 | 19 | impl BaseDirs { 20 | pub fn place_config_file>(&self, path: P) -> eyre::Result { 21 | ::dirs::config_dir() 22 | .ok_or_else(|| eyre!("unable to dermine user config dir")) 23 | .map(|mut config| { 24 | config.push("rsspls"); 25 | config.push(path); 26 | config 27 | }) 28 | } 29 | 30 | pub fn place_cache_file>(&self, path: P) -> eyre::Result { 31 | ::dirs::cache_dir() 32 | .ok_or_else(|| eyre!("unable to dermine user cache dir")) 33 | .map(|mut config| { 34 | config.push("rsspls"); 35 | config.push(path); 36 | config 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/feed.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, mem}; 2 | 3 | use basic_toml as toml; 4 | use kuchiki::traits::TendrilSink; 5 | use kuchiki::{ElementData, NodeDataRef, NodeRef}; 6 | use log::{debug, error, info, warn}; 7 | use mime_guess::mime; 8 | use reqwest::header::HeaderMap; 9 | use reqwest::{RequestBuilder, StatusCode}; 10 | use rss::{Channel, ChannelBuilder, EnclosureBuilder, GuidBuilder, Item, ItemBuilder}; 11 | use simple_eyre::eyre::{self, bail, eyre, WrapErr}; 12 | use time::format_description::well_known::Rfc2822; 13 | use time::OffsetDateTime; 14 | use tokio::task; 15 | use url::Url; 16 | 17 | use crate::cache::RequestCacheWrite; 18 | use crate::config::{ChannelConfig, ConfigHash, DateConfig, FeedConfig}; 19 | use crate::Client; 20 | 21 | #[derive(Debug)] 22 | pub enum ProcessResult { 23 | NotModified, 24 | Ok { 25 | channel: Channel, 26 | headers: Option, 27 | }, 28 | } 29 | 30 | pub enum FetchResult { 31 | NotModified, 32 | Ok { 33 | html: String, 34 | headers: Option, 35 | }, 36 | } 37 | 38 | pub async fn process_feed( 39 | client: &Client, 40 | channel_config: &ChannelConfig, 41 | config_hash: ConfigHash<'_>, 42 | cached_headers: &Option, 43 | ) -> eyre::Result { 44 | let config = &channel_config.config; 45 | info!("processing {}", config.url); 46 | let url: Url = config 47 | .url 48 | .parse() 49 | .wrap_err_with(|| format!("unable to parse {} as a URL", config.url))?; 50 | 51 | let (html, serialised_headers) = 52 | match fetch_webpage(client, &url, cached_headers, channel_config, config_hash).await? { 53 | FetchResult::Ok { html, headers } => (html, headers), 54 | FetchResult::NotModified => return Ok(ProcessResult::NotModified), 55 | }; 56 | 57 | let link_selector = config.link.as_ref().unwrap_or(&config.heading); 58 | 59 | let doc = kuchiki::parse_html().one(html); 60 | let base_url = Url::options().base_url(Some(&url)); 61 | rewrite_urls(&doc, &base_url)?; 62 | 63 | let mut items = Vec::new(); 64 | for item in doc 65 | .select(&config.item) 66 | .map_err(|()| eyre!("invalid selector for item: {}", config.item))? 67 | { 68 | match process_item(config, item, link_selector, &base_url) { 69 | Ok(rss_item) => items.push(rss_item), 70 | Err(err) => { 71 | let report = err.wrap_err(format!( 72 | "unable to process RSS item matching '{}'", 73 | config.item 74 | )); 75 | error!("{report:?}"); 76 | } 77 | } 78 | } 79 | 80 | let channel = ChannelBuilder::default() 81 | .title(&channel_config.title) 82 | .link(url.to_string()) 83 | .generator(Some(crate::version_string())) 84 | .items(items) 85 | .build(); 86 | 87 | Ok(ProcessResult::Ok { 88 | channel, 89 | headers: serialised_headers, 90 | }) 91 | } 92 | 93 | async fn fetch_webpage( 94 | client: &Client, 95 | url: &Url, 96 | cached_headers: &Option, 97 | channel_config: &ChannelConfig, 98 | config_hash: ConfigHash<'_>, 99 | ) -> eyre::Result { 100 | if url.scheme() == "file" { 101 | if client.file_urls { 102 | fetch_webpage_local(url).await 103 | } else { 104 | bail!("unable to fetch: {url} as file URLs are not enabled in config") 105 | } 106 | } else { 107 | fetch_webpage_http(client, url, cached_headers, channel_config, config_hash).await 108 | } 109 | } 110 | 111 | async fn fetch_webpage_http( 112 | client: &Client, 113 | url: &Url, 114 | cached_headers: &Option, 115 | channel_config: &ChannelConfig, 116 | config_hash: ConfigHash<'_>, 117 | ) -> eyre::Result { 118 | let config = &channel_config.config; 119 | 120 | let req = add_headers( 121 | client.http.get(url.clone()), 122 | cached_headers, 123 | &channel_config.user_agent, 124 | ); 125 | 126 | let resp = req 127 | .send() 128 | .await 129 | .wrap_err_with(|| format!("unable to fetch {}", url))?; 130 | 131 | // Check response 132 | let status = resp.status(); 133 | if status == StatusCode::NOT_MODIFIED { 134 | // Cache hit, nothing to do 135 | info!("{} is unmodified", url); 136 | return Ok(FetchResult::NotModified); 137 | } 138 | 139 | if !status.is_success() { 140 | return Err(eyre!( 141 | "failed to fetch {}: {} {}", 142 | config.url, 143 | status.as_str(), 144 | status.canonical_reason().unwrap_or("Unknown Status") 145 | )); 146 | } 147 | 148 | if config.link.is_none() { 149 | info!( 150 | "no explicit link selector provided, falling back to heading selector: {:?}", 151 | config.heading 152 | ); 153 | } 154 | 155 | // Collect the headers for later 156 | let headers: Vec<_> = resp 157 | .headers() 158 | .iter() 159 | .filter_map(|(name, value)| value.to_str().ok().map(|val| (name.as_str(), val))) 160 | .collect(); 161 | let map = RequestCacheWrite { 162 | headers, 163 | version: crate::version(), 164 | config_hash, 165 | }; 166 | let serialised_headers = toml::to_string(&map) 167 | .map_err(|err| warn!("unable to serialise headers: {}", err)) 168 | .ok(); 169 | 170 | // Read body 171 | let html = resp.text().await.wrap_err("unable to read response body")?; 172 | 173 | Ok(FetchResult::Ok { 174 | html, 175 | headers: serialised_headers, 176 | }) 177 | } 178 | 179 | async fn fetch_webpage_local(url: &Url) -> eyre::Result { 180 | let path = url 181 | .to_file_path() 182 | .map_err(|()| eyre!("unable to extract path from: {}", url))?; 183 | debug!("read {}", path.display()); 184 | let html = task::spawn_blocking(move || { 185 | fs::read_to_string(&path).wrap_err_with(|| format!("error reading {}", path.display())) 186 | }) 187 | .await 188 | .wrap_err_with(|| format!("error joining task for {url}"))??; 189 | 190 | Ok(FetchResult::Ok { 191 | html, 192 | headers: None, 193 | }) 194 | } 195 | 196 | fn process_item( 197 | config: &FeedConfig, 198 | item: NodeDataRef, 199 | link_selector: &str, 200 | base_url: &url::ParseOptions, 201 | ) -> eyre::Result { 202 | let title = item 203 | .as_node() 204 | .select_first(&config.heading) 205 | .map_err(|()| eyre!("invalid selector for heading: {}", config.heading))?; 206 | let link = item 207 | .as_node() 208 | .select_first(link_selector) 209 | .map_err(|()| eyre!("invalid selector for link: {}", link_selector))?; 210 | // TODO: Need to make links absolute (probably ones in content too) 211 | let attrs = link.attributes.borrow(); 212 | let link_url = attrs 213 | .get("href") 214 | .ok_or_else(|| eyre!("element selected as link has no 'href' attribute"))?; 215 | let title_text = title.text_contents(); 216 | let description = extract_description(config, &item, &title_text)?; 217 | let date = extract_pub_date(config, &item)?; 218 | let guid = GuidBuilder::default() 219 | .value(link_url) 220 | .permalink(false) 221 | .build(); 222 | 223 | let mut rss_item_builder = ItemBuilder::default(); 224 | rss_item_builder 225 | .title(title_text) 226 | .link(base_url.parse(link_url).ok().map(|u| u.to_string())) 227 | .guid(Some(guid)) 228 | .pub_date(date.map(|date| date.format(&Rfc2822).unwrap())) 229 | .description(description); 230 | 231 | // Media enclosure 232 | if let Some(media_selector) = &config.media { 233 | debug!("checking for media matching {media_selector}"); 234 | let media = item 235 | .as_node() 236 | .select_first(media_selector) 237 | .map_err(|()| eyre!("invalid selector for media: {}", media_selector))?; 238 | 239 | let media_attrs = media.attributes.borrow(); 240 | let media_url = media_attrs 241 | .get("src") 242 | .or_else(|| media_attrs.get("href")) 243 | .ok_or_else(|| eyre!("element selected as media has no 'src' or 'href' attribute"))?; 244 | 245 | let parsed_url = base_url 246 | .parse(media_url) 247 | .map_err(|e| eyre!("media enclosure url invalid: {e}"))?; 248 | 249 | // Guessing the MIME type from the url as we don't have the full media 250 | let media_mime_type = parsed_url 251 | .path_segments() 252 | .and_then(|segments| segments.last()) 253 | .map(|media_filename| mime_guess::from_path(media_filename).first_or_octet_stream()) 254 | .unwrap_or_else(|| mime::APPLICATION_OCTET_STREAM); 255 | 256 | let mut enclosure_bld = EnclosureBuilder::default(); 257 | enclosure_bld.url(parsed_url.to_string()); 258 | enclosure_bld.mime_type(media_mime_type.to_string()); 259 | // "When an enclosure's size cannot be determined, a publisher should use a length of 0." 260 | // https://www.rssboard.org/rss-profile#element-channel-item-enclosure 261 | enclosure_bld.length("0".to_string()); 262 | 263 | rss_item_builder.enclosure(Some(enclosure_bld.build())); 264 | } 265 | 266 | Ok(rss_item_builder.build()) 267 | } 268 | 269 | fn rewrite_urls(doc: &NodeRef, base_url: &url::ParseOptions) -> eyre::Result<()> { 270 | for el in doc 271 | .select("*[href]") 272 | .map_err(|()| eyre!("unable to select links for rewriting"))? 273 | { 274 | let mut attrs = el.attributes.borrow_mut(); 275 | attrs.get_mut("href").and_then(|href| { 276 | let mut url = base_url.parse(href).ok().map(|url| url.to_string())?; 277 | mem::swap(href, &mut url); 278 | Some(()) 279 | }); 280 | } 281 | 282 | Ok(()) 283 | } 284 | 285 | fn add_headers( 286 | mut req: RequestBuilder, 287 | cached_headers: &Option, 288 | user_agent: &Option, 289 | ) -> RequestBuilder { 290 | use reqwest::header::{ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH, LAST_MODIFIED, USER_AGENT}; 291 | 292 | if let Some(ua) = user_agent { 293 | debug!("add User-Agent: {:?}", ua); 294 | req = req.header(USER_AGENT, ua); 295 | } 296 | 297 | let headers = match cached_headers { 298 | Some(headers) => headers, 299 | None => return req, 300 | }; 301 | 302 | if let Some(last_modified) = headers.get(LAST_MODIFIED) { 303 | debug!("add If-Modified-Since: {:?}", last_modified.to_str().ok()); 304 | req = req.header(IF_MODIFIED_SINCE, last_modified); 305 | } 306 | if let Some(etag) = headers.get(ETAG) { 307 | debug!("add If-None-Match: {:?}", etag.to_str().ok()); 308 | req = req.header(IF_NONE_MATCH, etag); 309 | } 310 | req 311 | } 312 | 313 | fn extract_pub_date( 314 | config: &FeedConfig, 315 | item: &NodeDataRef, 316 | ) -> eyre::Result> { 317 | config 318 | .date 319 | .as_ref() 320 | .map(|date| { 321 | item.as_node() 322 | .select_first(date.selector()) 323 | .map_err(|()| eyre!("invalid selector for date: {}", date.selector())) 324 | .map(|node| parse_date(date, &node)) 325 | }) 326 | .transpose() 327 | .map(Option::flatten) 328 | } 329 | 330 | fn parse_date(date: &DateConfig, node: &NodeDataRef) -> Option { 331 | let attrs = node.attributes.borrow(); 332 | (&node.name.local == "time") 333 | .then(|| attrs.get("datetime")) 334 | .flatten() 335 | .and_then(|datetime| { 336 | debug!("trying datetime attribute"); 337 | date.parse(trim_date(datetime)).ok() 338 | }) 339 | .map(|x| { 340 | debug!("using datetime attribute"); 341 | x 342 | }) 343 | .or_else(|| { 344 | let text = node.text_contents(); 345 | let text = trim_date(&text); 346 | date.parse(text) 347 | .map_err(|_err| { 348 | warn!("unable to parse date '{}'", text); 349 | }) 350 | .ok() 351 | }) 352 | } 353 | 354 | // Trim non-alphanumeric chars from either side of the string 355 | fn trim_date(s: &str) -> &str { 356 | s.trim_matches(|c: char| !c.is_alphanumeric()) 357 | } 358 | 359 | fn extract_description( 360 | config: &FeedConfig, 361 | item: &NodeDataRef, 362 | title: &str, 363 | ) -> eyre::Result> { 364 | let mut description = Vec::new(); 365 | 366 | for selector in &config.summary { 367 | let nodes = item 368 | .as_node() 369 | .select(selector) 370 | .map_err(|()| { 371 | warn!( 372 | "summary selector '{selector}' for item with title '{}' did not match anything", 373 | title.trim() 374 | ) 375 | }) 376 | .ok(); 377 | let Some(nodes) = nodes else { 378 | continue; 379 | }; 380 | 381 | for node in nodes { 382 | node.as_node() 383 | .serialize(&mut description) 384 | .wrap_err("unable to serialise description")? 385 | } 386 | } 387 | 388 | if !description.is_empty() { 389 | // NOTE(unwrap): Should be safe as XML has to be legit Unicode) 390 | Ok(Some(String::from_utf8(description).unwrap())) 391 | } else { 392 | Ok(None) 393 | } 394 | } 395 | 396 | #[cfg(test)] 397 | mod tests { 398 | use std::path::{Path, PathBuf}; 399 | use std::{env, process}; 400 | 401 | use reqwest::Client as HttpClient; 402 | 403 | use super::*; 404 | 405 | const HTML: &str = include_str!("../tests/local.html"); 406 | 407 | struct RmOnDrop(PathBuf); 408 | 409 | impl RmOnDrop { 410 | fn new(path: PathBuf) -> Self { 411 | RmOnDrop(path) 412 | } 413 | 414 | fn path(&self) -> &Path { 415 | &self.0 416 | } 417 | } 418 | 419 | impl Drop for RmOnDrop { 420 | fn drop(&mut self) { 421 | let _ = fs::remove_file(&self.0); 422 | } 423 | } 424 | 425 | fn test_config() -> FeedConfig { 426 | FeedConfig { 427 | url: String::new(), 428 | item: String::new(), 429 | heading: String::new(), 430 | link: None, 431 | summary: Vec::new(), 432 | date: None, 433 | media: None, 434 | } 435 | } 436 | 437 | #[test] 438 | fn test_trim_date() { 439 | assert_eq!(trim_date("2021-05-20 —"), "2021-05-20"); 440 | assert_eq!( 441 | trim_date("2022-04-20T06:38:27+10:00"), 442 | "2022-04-20T06:38:27+10:00" 443 | ); 444 | } 445 | 446 | #[test] 447 | fn test_rewrite_urls() { 448 | let html = r#"cool thing
ok
example"#; 449 | let expected = r#"cool thing
ok
example"#; 450 | let doc = kuchiki::parse_html().one(html); 451 | let base_url = "http://example.com".parse().unwrap(); 452 | let base = Url::options().base_url(Some(&base_url)); 453 | rewrite_urls(&doc, &base).unwrap(); 454 | let rewritten = doc.to_string(); 455 | assert_eq!(rewritten, expected); 456 | } 457 | 458 | #[test] 459 | fn test_extract_description_multi() { 460 | // Test CSS selector for description that matches multiple elements 461 | let html = r#"

one

two"#; 462 | let doc = kuchiki::parse_html().one(html); 463 | let item = doc.select_first(".item").unwrap(); 464 | let config = FeedConfig { 465 | summary: vec!["span, p".to_string()], 466 | ..test_config() 467 | }; 468 | 469 | let description = extract_description(&config, &item, "title") 470 | .unwrap() 471 | .unwrap(); 472 | 473 | // Items come out in DOM order 474 | assert_eq!(description, "

one

two"); 475 | } 476 | 477 | #[test] 478 | fn test_extract_description_array() { 479 | // Test CSS selector for description that matches multiple elements 480 | let html = r#"

one

two"#; 481 | let doc = kuchiki::parse_html().one(html); 482 | let item = doc.select_first(".item").unwrap(); 483 | let config = FeedConfig { 484 | summary: vec!["span".to_string(), "p".to_string()], 485 | ..test_config() 486 | }; 487 | 488 | let description = extract_description(&config, &item, "title") 489 | .unwrap() 490 | .unwrap(); 491 | 492 | // Items come out in the order of the selector array 493 | assert_eq!(description, "two

one

"); 494 | } 495 | 496 | #[test] 497 | fn test_process_local_html() { 498 | let html_file_name = format!("rsspls.local.{}.html", process::id()); 499 | let local_html = RmOnDrop::new(env::temp_dir().join(&html_file_name)); 500 | fs::write(local_html.path(), HTML.as_bytes()).expect("unable to write test HTML"); 501 | 502 | let url = Url::from_file_path(local_html.path()) 503 | .expect("unable to construct file URL for test HTML"); 504 | 505 | let client = Client { 506 | file_urls: true, 507 | http: HttpClient::new(), 508 | }; 509 | 510 | let config = FeedConfig { 511 | url: url.to_string(), 512 | item: "nav a".to_string(), 513 | heading: "a".to_string(), 514 | ..test_config() 515 | }; 516 | let channel_config = ChannelConfig { 517 | title: "Local Site".to_string(), 518 | filename: Path::new(&html_file_name) 519 | .with_extension("rss") 520 | .to_string_lossy() 521 | .into_owned(), 522 | user_agent: None, 523 | config, 524 | }; 525 | let config_hash = ConfigHash(&html_file_name); 526 | 527 | let runtime = tokio::runtime::Builder::new_current_thread() 528 | .build() 529 | .unwrap(); 530 | let res = runtime 531 | .block_on(process_feed(&client, &channel_config, config_hash, &None)) 532 | .expect("unable to process local feed"); 533 | 534 | let ProcessResult::Ok { channel, .. } = res else { 535 | panic!("expected ProcessResult::Ok but got: {:?}", res) 536 | }; 537 | 538 | assert_eq!(channel.items().len(), 5); 539 | assert_eq!(channel.items()[0].title, Some("Install".to_string())); 540 | } 541 | 542 | #[test] 543 | fn test_process_local_files_disabled() { 544 | let html_file_name = "rsspls.local.html"; 545 | let local_html = env::temp_dir().join(&html_file_name); 546 | let url = 547 | Url::from_file_path(&local_html).expect("unable to construct file URL for test HTML"); 548 | 549 | let client = Client { 550 | file_urls: false, 551 | http: HttpClient::new(), 552 | }; 553 | 554 | let config = FeedConfig { 555 | url: url.to_string(), 556 | item: "nav a".to_string(), 557 | heading: "a".to_string(), 558 | ..test_config() 559 | }; 560 | let channel_config = ChannelConfig { 561 | title: "Local Site".to_string(), 562 | filename: Path::new(&html_file_name) 563 | .with_extension("rss") 564 | .to_string_lossy() 565 | .into_owned(), 566 | user_agent: None, 567 | config, 568 | }; 569 | let config_hash = ConfigHash(&html_file_name); 570 | 571 | let runtime = tokio::runtime::Builder::new_current_thread() 572 | .build() 573 | .unwrap(); 574 | let res = runtime.block_on(process_feed(&client, &channel_config, config_hash, &None)); 575 | 576 | let Err(err) = res else { 577 | panic!("expected error, got: {:?}", res) 578 | }; 579 | 580 | assert!(err 581 | .to_string() 582 | .contains("file URLs are not enabled in config")); 583 | } 584 | } 585 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cache; 2 | mod cli; 3 | mod config; 4 | mod feed; 5 | 6 | #[cfg(windows)] 7 | mod dirs; 8 | 9 | #[cfg(not(windows))] 10 | mod xdg; 11 | 12 | #[cfg(not(windows))] 13 | use crate::xdg as dirs; 14 | 15 | use std::path::{Path, PathBuf}; 16 | use std::process::ExitCode; 17 | use std::sync::{Arc, Mutex}; 18 | use std::time::Duration; 19 | use std::{env, fs}; 20 | 21 | use atomicwrites::AtomicFile; 22 | use eyre::{eyre, Report, WrapErr}; 23 | use futures::future; 24 | use log::{debug, error, info}; 25 | use reqwest::Client as HttpClient; 26 | use rss::Channel; 27 | use simple_eyre::eyre; 28 | 29 | use crate::cache::deserialise_cached_headers; 30 | use crate::config::ConfigHash; 31 | use crate::config::{ChannelConfig, Config}; 32 | use crate::dirs::Dirs; 33 | use crate::feed::{process_feed, ProcessResult}; 34 | 35 | const RSSPLS_LOG: &str = "RSSPLS_LOG"; 36 | 37 | #[derive(Clone)] 38 | pub struct Client { 39 | /// Whether file URLs are enabled 40 | file_urls: bool, 41 | /// HTTP client 42 | http: HttpClient, 43 | } 44 | 45 | #[tokio::main] 46 | async fn main() -> ExitCode { 47 | match try_main().await { 48 | Ok(true) => ExitCode::SUCCESS, 49 | Ok(false) => ExitCode::FAILURE, 50 | Err(report) => { 51 | error!("{:?}", report); 52 | ExitCode::FAILURE 53 | } 54 | } 55 | } 56 | 57 | async fn try_main() -> eyre::Result { 58 | simple_eyre::install()?; 59 | match env::var_os(RSSPLS_LOG) { 60 | None => env::set_var(RSSPLS_LOG, "info"), 61 | Some(_) => {} 62 | } 63 | pretty_env_logger::try_init_custom_env(RSSPLS_LOG)?; 64 | 65 | let cli = cli::parse_args().wrap_err("unable to parse CLI arguments")?; 66 | let cli = match cli { 67 | Some(cli) => cli, 68 | // Help or version info was printed and we should return 69 | None => return Ok(true), 70 | }; 71 | 72 | let config = Config::read(cli.config_path)?; 73 | 74 | // Determine output directory 75 | let output_dir = match cli.output_path { 76 | Some(path) => Some(path), 77 | None => config 78 | .rsspls 79 | .output 80 | .map(|ref path| { 81 | dirs::home_dir() 82 | .ok_or_else(|| eyre!("unable to determine home directory")) 83 | .map(|home| expand_tilde(path, home)) 84 | }) 85 | .transpose()?, 86 | } 87 | .ok_or_else(|| { 88 | eyre!("output directory must be supplied via --output or be present in configuration file") 89 | })?; 90 | 91 | // Ensure output directory exists 92 | if !output_dir.exists() { 93 | fs::create_dir_all(&output_dir).wrap_err_with(|| { 94 | format!( 95 | "unable to create output directory: {}", 96 | output_dir.display() 97 | ) 98 | })?; 99 | info!("created output directory: {}", output_dir.display()); 100 | } 101 | 102 | // Set up the HTTP client 103 | let connect_timeout = Duration::from_secs(10); 104 | let timeout = Duration::from_secs(30); 105 | let mut client_builder = HttpClient::builder() 106 | .connect_timeout(connect_timeout) 107 | .timeout(timeout); 108 | 109 | // Add proxy if provided 110 | match config.rsspls.proxy { 111 | Some(proxy) => { 112 | debug!("using proxy from configuration file: {}", proxy); 113 | client_builder = client_builder.proxy(reqwest::Proxy::all(proxy)?) 114 | } 115 | None => { 116 | if let Ok(proxy) = env::var("http_proxy") { 117 | debug!("using http proxy from 'http_proxy' env var: {}", proxy); 118 | client_builder = client_builder.proxy(reqwest::Proxy::http(proxy)?) 119 | } 120 | if let Ok(proxy) = env::var("HTTPS_PROXY") { 121 | debug!("using https proxy from 'HTTPS_PROXY' env var: {}", proxy); 122 | client_builder = client_builder.proxy(reqwest::Proxy::https(proxy)?) 123 | } 124 | } 125 | }; 126 | 127 | let client = Client { 128 | file_urls: config.rsspls.file_urls, 129 | http: client_builder 130 | .build() 131 | .wrap_err("unable to build HTTP client")?, 132 | }; 133 | 134 | // Wrap up xdg::BaseDirectories for sharing between tasks. Mutex is used so that only one 135 | // thread at a time will attempt to create cache directories. 136 | let dirs = dirs::new()?; 137 | let dirs = Arc::new(Mutex::new(dirs)); 138 | 139 | // Spawn the tasks 140 | let config_hash = Arc::new(config.hash.clone()); 141 | let futures = config.feed.into_iter().map(|feed| { 142 | let client = client.clone(); // Client uses Arc internally 143 | let output_dir = output_dir.clone(); 144 | let dirs = Arc::clone(&dirs); 145 | let config_hash = Arc::clone(&config_hash); 146 | tokio::spawn(async move { 147 | let res = process( 148 | &feed, 149 | &client, 150 | ConfigHash(config_hash.as_str()), 151 | output_dir, 152 | dirs, 153 | ) 154 | .await; 155 | if let Err(ref report) = res { 156 | // Eat errors when processing feeds so that we don't stop processing the others. 157 | // Errors are reported, then we return a boolean indicating success or not, which 158 | // is used to set the exit status of the program later. 159 | error!("{:?}", report); 160 | } 161 | res.is_ok() 162 | }) 163 | }); 164 | 165 | // Run all the futures at the same time 166 | // The ? here will fail on an error if the JoinHandle fails 167 | let ok = future::try_join_all(futures) 168 | .await? 169 | .into_iter() 170 | .fold(true, |ok, succeeded| ok & succeeded); 171 | 172 | Ok(ok) 173 | } 174 | 175 | async fn process( 176 | feed: &ChannelConfig, 177 | client: &Client, 178 | config_hash: ConfigHash<'_>, 179 | output_dir: PathBuf, 180 | dirs: Dirs, 181 | ) -> Result<(), Report> { 182 | // Generate paths up front so we report any errors before making requests 183 | let filename = Path::new(&feed.filename); 184 | let filename = filename 185 | .file_name() 186 | .map(Path::new) 187 | .ok_or_else(|| eyre!("{} is not a valid file name", filename.display()))?; 188 | let output_path = output_dir.join(filename); 189 | let cache_filename = filename.with_extension("toml"); 190 | let cache_path = { 191 | let dirs = dirs.lock().map_err(|_| eyre!("unable to acquire mutex"))?; 192 | dirs.place_cache_file(&cache_filename) 193 | .wrap_err("unable to create path to cache file") 194 | }?; 195 | let cached_headers = deserialise_cached_headers(&cache_path, config_hash); 196 | 197 | process_feed(client, feed, config_hash, &cached_headers) 198 | .await 199 | .and_then(|ref process_result| { 200 | match process_result { 201 | ProcessResult::NotModified => Ok(()), 202 | ProcessResult::Ok { channel, headers } => { 203 | // TODO: channel.validate() 204 | write_channel(channel, &output_path).wrap_err_with(|| { 205 | format!("unable to write output file: {}", output_path.display()) 206 | })?; 207 | 208 | // Update the cache 209 | if let Some(headers) = headers { 210 | debug!("write cache {}", cache_path.display()); 211 | fs::write(cache_path, headers).wrap_err("unable to write to cache")?; 212 | } 213 | 214 | Ok(()) 215 | } 216 | } 217 | }) 218 | .wrap_err_with(|| format!("error processing feed for {}", feed.config.url)) 219 | } 220 | 221 | fn write_channel(channel: &Channel, output_path: &Path) -> Result<(), Report> { 222 | // Write the new file into a temporary location, then move it into place 223 | let file = AtomicFile::new(output_path, atomicwrites::AllowOverwrite); 224 | file.write(|f| { 225 | info!("write {}", output_path.display()); 226 | channel 227 | .write_to(f) 228 | .map(drop) 229 | .wrap_err("unable to write feed") 230 | }) 231 | .map_err(|err| match err { 232 | atomicwrites::Error::Internal(atomic_err) => atomic_err.into(), 233 | atomicwrites::Error::User(myerr) => myerr, 234 | }) 235 | } 236 | 237 | pub fn version_string() -> String { 238 | format!("{} version {}", env!("CARGO_PKG_NAME"), version()) 239 | } 240 | 241 | fn version() -> &'static str { 242 | env!("CARGO_PKG_VERSION") 243 | } 244 | 245 | fn expand_tilde>(path: P, mut home: PathBuf) -> PathBuf { 246 | let path = path.into(); 247 | 248 | // NOTE: starts_with only considers whole path components 249 | if path.starts_with("~") { 250 | if path == Path::new("~") { 251 | home 252 | } else { 253 | home.push(path.strip_prefix("~").unwrap()); 254 | home 255 | } 256 | } else { 257 | path 258 | } 259 | } 260 | 261 | #[cfg(test)] 262 | mod tests { 263 | use super::*; 264 | 265 | #[test] 266 | #[cfg(not(windows))] 267 | fn test_home() { 268 | let expanded = expand_tilde("asdf", PathBuf::from("/home/foo")); 269 | assert_eq!(expanded, Path::new("asdf")); 270 | 271 | let expanded = expand_tilde("~asdf", PathBuf::from("/home/foo")); 272 | assert_eq!(expanded, Path::new("~asdf")); 273 | 274 | let expanded = expand_tilde("~/some/where", PathBuf::from("/home/foo")); 275 | assert_eq!(expanded, Path::new("/home/foo/some/where")); 276 | 277 | let expanded = expand_tilde("~/some/where", PathBuf::from("/")); 278 | assert_eq!(expanded, Path::new("/some/where")); 279 | } 280 | 281 | #[test] 282 | #[cfg(windows)] 283 | fn test_home_windows() { 284 | let expanded = expand_tilde("asdf", PathBuf::from(r"C:\Users\Foo")); 285 | assert_eq!(expanded, Path::new("asdf")); 286 | 287 | let expanded = expand_tilde("~asdf", PathBuf::from(r"C:\Users\Foo")); 288 | assert_eq!(expanded, Path::new("~asdf")); 289 | 290 | let expanded = expand_tilde(r"~\some\where", PathBuf::from(r"C:\Users\Foo")); 291 | assert_eq!(expanded, Path::new(r"C:\Users\Foo\some\where")); 292 | 293 | let expanded = expand_tilde(r"~\some\where", PathBuf::from(r"C:\")); 294 | assert_eq!(expanded, Path::new(r"C:\some\where")); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/xdg.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::PathBuf, 3 | sync::{Arc, Mutex}, 4 | }; 5 | 6 | use eyre::WrapErr; 7 | use simple_eyre::eyre; 8 | 9 | pub type Dirs = Arc>; 10 | 11 | pub fn new() -> eyre::Result { 12 | xdg::BaseDirectories::with_prefix("rsspls") 13 | .wrap_err("unable to determine home directory of current user") 14 | } 15 | 16 | pub fn home_dir() -> Option { 17 | // This module only supports Unix, and the behavior of `std::env::home_dir()` is only 18 | // problematic on Windows. 19 | #[allow(deprecated)] 20 | std::env::home_dir() 21 | } 22 | -------------------------------------------------------------------------------- /tests/local.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Documentation - RSS Please 7 | 8 | 9 | 10 | 11 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 | 67 | 68 | 69 | 73 | 74 | 75 | 97 | 98 |
99 | 100 | 101 |
102 | 103 | 104 | 105 | 106 | 107 |
108 |
109 | 110 |
111 | How it Works 112 |
113 | 114 | 115 |
116 | Supported Platforms 117 |
118 | 119 | 120 |
121 | Usage 122 |
123 | 124 | 125 |
126 | Configuration 127 |
128 | 129 | 130 | 133 | 134 |
135 | - output 136 |
137 | 138 |
139 | - proxy 140 |
141 | 142 |
143 | - feed.title 144 |
145 | 146 |
147 | - feed.filename 148 |
149 | 150 |
151 | - feed.config.url 152 |
153 | 154 |
155 | - feed.config.item 156 |
157 | 158 |
159 | - feed.config.heading 160 |
161 | 162 |
163 | - feed.config.link 164 |
165 | 166 |
167 | - feed.config.summary 168 |
169 | 170 |
171 | - feed.config.date 172 |
173 | 174 |
175 | - feed.config.media 176 |
177 | 178 | 179 | 180 | 183 | 184 | 185 |
186 | Logging 187 |
188 | 189 | 190 | 193 | 194 | 195 |
196 | Caching 197 |
198 | 199 | 200 |
201 |
202 | 203 | 204 | 205 |
206 | 207 | 208 |

Documentation

209 | 210 | 211 | 212 |

How it Works

213 |

rsspls fetches each page specified by the configuration and extracts elements 214 | from the page using CSS selectors. For example elements are matched 215 | to determine the title and content of the feed entry. The generated feeds are 216 | written to an output directory. HTTP caching is used to only update the feed 217 | when the source page changes.

218 |

Supported Platforms

219 |

rsspls should work on all platforms supported by the Rust compiler 220 | including Linux, macOS, Windows, and BSD. Pre-compiled binaries are available 221 | for common platforms. See the install page for details.

222 |

Usage

223 |
rsspls [OPTIONS] -o OUTPUT_DIR
224 | 
225 | OPTIONS:
226 |     -h, --help
227 |             Prints this help information
228 | 
229 |     -c, --config
230 |             Specify the path to the configuration file.
231 |             $XDG_CONFIG_HOME/rsspls/feeds.toml is used if not supplied.
232 | 
233 |     -o, --output
234 |             Directory to write generated feeds to.
235 | 
236 |     -V, --version
237 |             Prints version information
238 | 
239 | FILES:
240 |      ~/$XDG_CONFIG_HOME/rsspls/feeds.toml    rsspls configuration file.
241 |      ~/$XDG_CONFIG_HOME/rsspls               Configuration directory.
242 |      ~/XDG_CACHE_HOME/rsspls                 Cache directory.
243 | 
244 |      Note: XDG_CONFIG_HOME defaults to ~/.config, XDG_CACHE_HOME
245 |      defaults to ~/.cache.
246 | 
247 |

Configuration

248 |

Unless specified via the --config command line option rsspls reads its 249 | configuration from one of the following paths:

250 |
    251 |
  • UNIX-like systems: 252 |
      253 |
    • $XDG_CONFIG_HOME/rsspls/feeds.toml
    • 254 |
    • ~/.config/rsspls/feeds.toml if XDG_CONFIG_HOME is unset.
    • 255 |
    256 |
  • 257 |
  • Windows: 258 |
      259 |
    • C:\Users\You\AppData\Roaming\rsspls\feeds.toml
    • 260 |
    261 |
  • 262 |
263 |

The configuration file is in TOML format.

264 |

The parts of the page to extract for the feed are specified using CSS 265 | selectors.

266 |

Annotated Sample Configuration

267 |

The sample file below demonstrates all the parts of the configuration.

268 |
# The configuration must start with the [rsspls] section
269 | [rsspls]
270 | # Optional output directory to write the feeds to. If not specified it must be supplied via
271 | # the --output command line option.
272 | output = "/tmp"
273 | # Optional proxy address. If specified, all requests will be routed through it.
274 | # The address needs to be in the format: protocol://ip_address:port
275 | # The supported protocols are: http, https, socks and socks5h.
276 | # It can also be specified as environment variable `http_proxy` or `HTTPS_PROXY`.
277 | # The config file takes precedence, then the env vars in the above order.
278 | # proxy = socks5://10.64.0.1:1080
279 | 
280 | # Next is the array of feeds, each one starts with [[feed]]
281 | [[feed]]
282 | # The title of the channel in the feed
283 | title = "My Great RSS Feed"
284 | 
285 | # The output filename without the output directory to write this feed to.
286 | # Note: this is a filename only, not a path. It should not contain slashes.
287 | filename = "wezm.rss"
288 | 
289 | # Optional User-Agent header to be set for the HTTP request.
290 | # user_agent = "Mozilla/5.0"
291 | 
292 | # The configuration for the feed
293 | [feed.config]
294 | # The URL of the web page to generate the feed from.
295 | url = "https://www.wezm.net/"
296 | 
297 | # A CSS selector to select elements on the page that represent items in the feed.
298 | item = "article"
299 | 
300 | # A CSS selector relative to `item` to an element that will supply the title for the item.
301 | heading = "h3"
302 | 
303 | # A CSS selector relative to `item` to an element that will supply the link for the item.
304 | # Note: This element must have a `href` attribute.
305 | # Note: If not supplied rsspls will attempt to use the heading selector for link for backwards
306 | #       compatibility with earlier versions. A message will be emitted in this case.
307 | link = "h3 a"
308 | 
309 | # Optional CSS selector relative to `item` that will supply the content of the RSS item.
310 | summary = ".post-body"
311 | 
312 | # Optional CSS selector relative to `item` that supplies media content (audio, video, image)
313 | # to be added as an RSS enclosure.
314 | # Note: The media URL must be given by the `src` or `href` attribute of the selected element.
315 | # Note: Currently if the item does not match the media selector then it will be skipped.
316 | # media = "figure img"
317 | 
318 | # Optional CSS selector relative to `item` that supples the publication date of the RSS item.
319 | date = "time"
320 | 
321 | # Alternatively for more control `date` can be specified as a table:
322 | # [feed.config.date]
323 | # selector = "time"
324 | # # Optional type of value being parsed.
325 | # # Defaults to DateTime, can also be Date if you're parsing a value without a time.
326 | # type = "Date" 
327 | # # format of the date to parse. See the following for the syntax
328 | # # https://time-rs.github.io/book/api/format-description.html
329 | # format = "[day padding:none]/[month padding:none]/[year]" # will parse 1/2/1934 style dates
330 | 
331 | # A second example feed
332 | [[feed]]
333 | title = "Example Site"
334 | filename = "example.rss"
335 | 
336 | [feed.config]
337 | url = "https://example.com/"
338 | item = "div"
339 | heading = "a"
340 | 
341 |

The first example above (for my blog WezM.net) matches HTML that looks like this:

342 |
<section class="posts-section">
343 |   <h2>Recent Posts</h2>
344 | 
345 |   <article id="garage-door-monitor">
346 |     <h3><a href="https://www.wezm.net/v2/posts/2022/garage-door-monitor/">Monitoring My Garage Door With a Raspberry Pi, Rust, and a 13Mb Linux System</a></h3>
347 |     <div class="post-metadata">
348 |       <div class="date-published">
349 |         <time datetime="2022-04-20T06:38:27+10:00">20 April 2022</time>
350 |       </div>
351 |     </div>
352 | 
353 |     <div class="post-body">
354 |       <p>I’ve accidentally left our garage door open a few times. To combat this I built
355 |         a monitor that sends an alert via Mattermost when the door has been left open
356 |         for more than 5 minutes. This turned out to be a super fun project. I used
357 |         parts on hand as much as possible, implemented the monitoring application in
358 |         Rust, and then built a stripped down Linux image to run it.
359 |       </p>
360 |     </div>
361 | 
362 |     <a href="https://www.wezm.net/v2/posts/2022/garage-door-monitor/">Continue Reading →</a>
363 |   </article>
364 | 
365 |   <article id="monospace-kobo-ereader">
366 |     <!-- another article -->
367 |   </article>
368 | 
369 |   <!-- more articles -->
370 | 
371 |   <a href="https://www.wezm.net/v2/posts/">View more posts →</a>
372 | </section>
373 | 
374 |

output

375 |

Optional output directory to write the feeds to. If not specified it must be 376 | supplied via the --output command line option. Directory will be created if 377 | it does not exist.

378 |

Tilde expansion is performed on the path in the config file. This allows you to 379 | refer to the home directory of the user running rsspls. For example, 380 | ~/Documents/rsspls could be used to place the output in your Documents 381 | folder.

382 |

proxy

383 |

Optional proxy address. If specified, all requests will be routed through it. 384 | The address needs to be in the format: protocol://ip_address:port 385 | The supported protocols are: http, https, socks and socks5h.

386 |

The proxy for http and https requests can also be specified with the 387 | environment variables http_proxy and HTTPS_PROXY respectively. 388 | The config file takes precedence over environment variables.

389 |

feed.title

390 |

The title of the channel in the generated feed.

391 |

feed.filename

392 |

The output filename to write this feed to. Note: this is a filename only, not a 393 | path. It should not contain slashes. It will be written to the output 394 | directory.

395 |

feed.config.url

396 |

The URL of the web page to generate the feed from. The page at this address 397 | will be fetched processed to turn it into a feed.

398 |

feed.config.item

399 |

A CSS selector to select elements on the page that represent items in the feed. 400 | The other CSS selectors match elements inside the elements that this selector 401 | matches.

402 |

feed.config.heading

403 |

A CSS selector relative to item to an element that will supply the title for 404 | the item in the feed.

405 | 406 |

CSS selector relative to item to an element that will supply the 407 | link for the item in the feed.

408 |

Note: This element must have a href attribute.

409 |

Note: If not supplied rsspls will attempt to use the 410 | feed.config.heading selector as the link element for backwards compatibility 411 | with earlier versions. A warning message will be emitted in this case. It is 412 | recommended to specify the link selector explicitly.

413 |

feed.config.summary

414 |

Optional CSS selector relative to item that will supply the content of the 415 | RSS item. This value may be a single CSS selector, or an array of CSS 416 | selectors.

417 |

The CSS selectors may also include a comma separated list of elements to match. 418 | For example: summary = "p, blockquote" will match p or blockquote 419 | elements, adding them to the RSS feed in the order then are encountered in the 420 | HTML document.

421 |

The array form of summary allows the order of the matched elements to be 422 | controlled, enabling elements to be added to the feed in a different order to 423 | the source HTML document. For example, summary = ["p", "blockquote"] causes 424 | rsspls to make a pass over the source HTML document, adding p elements to 425 | the feed, followed by a pass adding blockquote elements to the feed.

426 |

feed.config.date

427 |

The optional date key in the configuration can be a string or a table. If it’s a 428 | string then it’s used as CSS selector relative to item to find the element 429 | containing the date and rsspls will attempt to automatically parse the value.

430 |

If automatic parsing fails you can manually specify the format using the table 431 | form of date, which looks like this:

432 |
[feed.config.date]
433 | selector = "time" # required
434 | type = "Date"
435 | format = "[day padding:none]/[month padding:none]/[year]" # will parse 1/2/1934 style dates
436 | 
437 |

If the element matched by the date selector is a <time> element then 438 | rsspls will first try to parse the value in the datetime attribute if 439 | present. If the attribute is missing or the element is not a time element 440 | then rsspls will use the supplied format or attempt automatic parsing of the 441 | text content of the element.

442 |

feed.config.date.selector

443 |

CSS selector relative to item that supples the publication date of 444 | the RSS item.

445 |

feed.config.date.type

446 |

Optional type of value being parsed. Either Date or DateTime.

447 |

type is Date when you want to parse just a date. Use DateTime if you’re 448 | parsing a date and time with the format. Defaults to DateTime.

449 |

feed.config.date.format

450 |

Format description using the syntax described on this page: 451 | https://time-rs.github.io/book/api/format-description.html 452 | of how to parse the date.

453 |

feed.config.media

454 |

Optional CSS selector relative to item that supplies media content (audio, 455 | video, image) to be added as an RSS enclosure.

456 |

Note: The media URL must be given by the src or href attribute of the 457 | selected element.

458 |

Note: Currently if the item does not match the media selector then it will 459 | be skipped.

460 |

Hosting, Updating, and Subscribing

461 |

In order to have the feeds update you will need to arrange for 462 | rsspls to be run periodically. You might do this with cron, systemd 463 | timers, or the Windows equivalent.

464 |

To subscribe to feeds you can run rsspls locally and use a feed reader that 465 | supports local file feeds. Or, more likely it is expected that rsspls will be 466 | run on a web server that is serving the directory the feeds are written to.

467 |

Logging

468 |

rsspls logs messages to stderr. Logging can be controlled by the 469 | RSSPLS_LOG environment variable. Log level and target module can controlled 470 | according to the env_logger documentation. For example, to enable 471 | debug logging for rsspls you would use:

472 |

RSSPLS_LOG=rsspls=debug

473 |

The supported log levels are:

474 |
    475 |
  • error
  • 476 |
  • warn
  • 477 |
  • info
  • 478 |
  • debug
  • 479 |
  • trace
  • 480 |
  • off (disable logging)
  • 481 |
482 |

The default log level is info.

483 |

Caveats & Error Handling

484 |

rsspls just fetches and parses the HTML of the web page you specify. It does 485 | not run JavaScript. If the website is entirely generated by JavaScript (such as 486 | Twitter) then rsspls will not work.

487 |

If errors are encountered processing the page due to invalid selectors, or 488 | missing elements an error message will be logged. If the error is non-recoverable 489 | rsspls will exit with a non-zero exit status.

490 |

If an error is encountered processing an item for the feed a warning will by 491 | logged and processing will continue with the next item. rsspls will still 492 | exit with success (0) in this case.

493 |

Caching

494 |

When websites respond with cache headers rsspls will make a conditional 495 | request on subsequent runs and will not regenerate the feed if the server 496 | responds with 304 Not Modified. Cache data is stored in 497 | $XDG_CACHE_HOME/rsspls, which defaults to ~/.cache/rsspls on UNIX-like 498 | systems or C:\Users\You\AppData\Local\rsspls on Windows.

499 | 500 | 501 |
502 | 503 | 504 | 505 |
506 | 507 | 508 | 516 | 517 | 518 | 519 | 520 | 544 | 545 | 546 | -------------------------------------------------------------------------------- /visit-website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/rsspls/0838f4ab653b7baaf13b82fa38eda8c64691bcc9/visit-website.png --------------------------------------------------------------------------------