Terminal Markdown previewer — GUI-like experience.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: auto update

RivoLink 643caf86 2b73b5af

+1621 -1
+6
.github/workflows/release-build.yml
··· 114 114 path: dist 115 115 merge-multiple: true 116 116 117 + - name: Generate checksums 118 + shell: bash 119 + run: | 120 + cd dist 121 + sha256sum leaf-* > checksums.txt 122 + 117 123 - name: Ensure GitHub release exists 118 124 env: 119 125 GH_REPO: ${{ github.repository }}
+1088
Cargo.lock
··· 21 21 checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" 22 22 23 23 [[package]] 24 + name = "atomic-waker" 25 + version = "1.1.2" 26 + source = "registry+https://github.com/rust-lang/crates.io-index" 27 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 28 + 29 + [[package]] 24 30 name = "base64" 25 31 version = "0.22.1" 26 32 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 42 48 checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" 43 49 44 50 [[package]] 51 + name = "block-buffer" 52 + version = "0.10.4" 53 + source = "registry+https://github.com/rust-lang/crates.io-index" 54 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 55 + dependencies = [ 56 + "generic-array", 57 + ] 58 + 59 + [[package]] 60 + name = "bumpalo" 61 + version = "3.20.2" 62 + source = "registry+https://github.com/rust-lang/crates.io-index" 63 + checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" 64 + 65 + [[package]] 66 + name = "bytes" 67 + version = "1.11.1" 68 + source = "registry+https://github.com/rust-lang/crates.io-index" 69 + checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" 70 + 71 + [[package]] 45 72 name = "cassowary" 46 73 version = "0.3.0" 47 74 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 73 100 checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 74 101 75 102 [[package]] 103 + name = "cfg_aliases" 104 + version = "0.2.1" 105 + source = "registry+https://github.com/rust-lang/crates.io-index" 106 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 107 + 108 + [[package]] 76 109 name = "compact_str" 77 110 version = "0.8.1" 78 111 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 87 120 ] 88 121 89 122 [[package]] 123 + name = "cpufeatures" 124 + version = "0.2.17" 125 + source = "registry+https://github.com/rust-lang/crates.io-index" 126 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 127 + dependencies = [ 128 + "libc", 129 + ] 130 + 131 + [[package]] 90 132 name = "crc32fast" 91 133 version = "1.5.0" 92 134 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 121 163 ] 122 164 123 165 [[package]] 166 + name = "crypto-common" 167 + version = "0.1.7" 168 + source = "registry+https://github.com/rust-lang/crates.io-index" 169 + checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 170 + dependencies = [ 171 + "generic-array", 172 + "typenum", 173 + ] 174 + 175 + [[package]] 124 176 name = "darling" 125 177 version = "0.23.0" 126 178 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 164 216 ] 165 217 166 218 [[package]] 219 + name = "digest" 220 + version = "0.10.7" 221 + source = "registry+https://github.com/rust-lang/crates.io-index" 222 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 223 + dependencies = [ 224 + "block-buffer", 225 + "crypto-common", 226 + ] 227 + 228 + [[package]] 229 + name = "displaydoc" 230 + version = "0.2.5" 231 + source = "registry+https://github.com/rust-lang/crates.io-index" 232 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 233 + dependencies = [ 234 + "proc-macro2", 235 + "quote", 236 + "syn", 237 + ] 238 + 239 + [[package]] 167 240 name = "either" 168 241 version = "1.15.0" 169 242 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 214 287 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 215 288 216 289 [[package]] 290 + name = "form_urlencoded" 291 + version = "1.2.2" 292 + source = "registry+https://github.com/rust-lang/crates.io-index" 293 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 294 + dependencies = [ 295 + "percent-encoding", 296 + ] 297 + 298 + [[package]] 299 + name = "futures-channel" 300 + version = "0.3.32" 301 + source = "registry+https://github.com/rust-lang/crates.io-index" 302 + checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" 303 + dependencies = [ 304 + "futures-core", 305 + "futures-sink", 306 + ] 307 + 308 + [[package]] 309 + name = "futures-core" 310 + version = "0.3.32" 311 + source = "registry+https://github.com/rust-lang/crates.io-index" 312 + checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" 313 + 314 + [[package]] 315 + name = "futures-io" 316 + version = "0.3.32" 317 + source = "registry+https://github.com/rust-lang/crates.io-index" 318 + checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" 319 + 320 + [[package]] 321 + name = "futures-sink" 322 + version = "0.3.32" 323 + source = "registry+https://github.com/rust-lang/crates.io-index" 324 + checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" 325 + 326 + [[package]] 327 + name = "futures-task" 328 + version = "0.3.32" 329 + source = "registry+https://github.com/rust-lang/crates.io-index" 330 + checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" 331 + 332 + [[package]] 333 + name = "futures-util" 334 + version = "0.3.32" 335 + source = "registry+https://github.com/rust-lang/crates.io-index" 336 + checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" 337 + dependencies = [ 338 + "futures-core", 339 + "futures-io", 340 + "futures-sink", 341 + "futures-task", 342 + "memchr", 343 + "pin-project-lite", 344 + "slab", 345 + ] 346 + 347 + [[package]] 348 + name = "generic-array" 349 + version = "0.14.7" 350 + source = "registry+https://github.com/rust-lang/crates.io-index" 351 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 352 + dependencies = [ 353 + "typenum", 354 + "version_check", 355 + ] 356 + 357 + [[package]] 217 358 name = "getopts" 218 359 version = "0.2.24" 219 360 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 223 364 ] 224 365 225 366 [[package]] 367 + name = "getrandom" 368 + version = "0.2.17" 369 + source = "registry+https://github.com/rust-lang/crates.io-index" 370 + checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 371 + dependencies = [ 372 + "cfg-if", 373 + "js-sys", 374 + "libc", 375 + "wasi", 376 + "wasm-bindgen", 377 + ] 378 + 379 + [[package]] 380 + name = "getrandom" 381 + version = "0.3.4" 382 + source = "registry+https://github.com/rust-lang/crates.io-index" 383 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 384 + dependencies = [ 385 + "cfg-if", 386 + "js-sys", 387 + "libc", 388 + "r-efi", 389 + "wasip2", 390 + "wasm-bindgen", 391 + ] 392 + 393 + [[package]] 226 394 name = "hashbrown" 227 395 version = "0.15.5" 228 396 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 246 414 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 247 415 248 416 [[package]] 417 + name = "http" 418 + version = "1.4.0" 419 + source = "registry+https://github.com/rust-lang/crates.io-index" 420 + checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 421 + dependencies = [ 422 + "bytes", 423 + "itoa", 424 + ] 425 + 426 + [[package]] 427 + name = "http-body" 428 + version = "1.0.1" 429 + source = "registry+https://github.com/rust-lang/crates.io-index" 430 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 431 + dependencies = [ 432 + "bytes", 433 + "http", 434 + ] 435 + 436 + [[package]] 437 + name = "http-body-util" 438 + version = "0.1.3" 439 + source = "registry+https://github.com/rust-lang/crates.io-index" 440 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 441 + dependencies = [ 442 + "bytes", 443 + "futures-core", 444 + "http", 445 + "http-body", 446 + "pin-project-lite", 447 + ] 448 + 449 + [[package]] 450 + name = "httparse" 451 + version = "1.10.1" 452 + source = "registry+https://github.com/rust-lang/crates.io-index" 453 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 454 + 455 + [[package]] 456 + name = "hyper" 457 + version = "1.9.0" 458 + source = "registry+https://github.com/rust-lang/crates.io-index" 459 + checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" 460 + dependencies = [ 461 + "atomic-waker", 462 + "bytes", 463 + "futures-channel", 464 + "futures-core", 465 + "http", 466 + "http-body", 467 + "httparse", 468 + "itoa", 469 + "pin-project-lite", 470 + "smallvec", 471 + "tokio", 472 + "want", 473 + ] 474 + 475 + [[package]] 476 + name = "hyper-rustls" 477 + version = "0.27.7" 478 + source = "registry+https://github.com/rust-lang/crates.io-index" 479 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 480 + dependencies = [ 481 + "http", 482 + "hyper", 483 + "hyper-util", 484 + "rustls", 485 + "rustls-pki-types", 486 + "tokio", 487 + "tokio-rustls", 488 + "tower-service", 489 + "webpki-roots", 490 + ] 491 + 492 + [[package]] 493 + name = "hyper-util" 494 + version = "0.1.20" 495 + source = "registry+https://github.com/rust-lang/crates.io-index" 496 + checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" 497 + dependencies = [ 498 + "base64", 499 + "bytes", 500 + "futures-channel", 501 + "futures-util", 502 + "http", 503 + "http-body", 504 + "hyper", 505 + "ipnet", 506 + "libc", 507 + "percent-encoding", 508 + "pin-project-lite", 509 + "socket2", 510 + "tokio", 511 + "tower-service", 512 + "tracing", 513 + ] 514 + 515 + [[package]] 516 + name = "icu_collections" 517 + version = "2.2.0" 518 + source = "registry+https://github.com/rust-lang/crates.io-index" 519 + checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" 520 + dependencies = [ 521 + "displaydoc", 522 + "potential_utf", 523 + "utf8_iter", 524 + "yoke", 525 + "zerofrom", 526 + "zerovec", 527 + ] 528 + 529 + [[package]] 530 + name = "icu_locale_core" 531 + version = "2.2.0" 532 + source = "registry+https://github.com/rust-lang/crates.io-index" 533 + checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" 534 + dependencies = [ 535 + "displaydoc", 536 + "litemap", 537 + "tinystr", 538 + "writeable", 539 + "zerovec", 540 + ] 541 + 542 + [[package]] 543 + name = "icu_normalizer" 544 + version = "2.2.0" 545 + source = "registry+https://github.com/rust-lang/crates.io-index" 546 + checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" 547 + dependencies = [ 548 + "icu_collections", 549 + "icu_normalizer_data", 550 + "icu_properties", 551 + "icu_provider", 552 + "smallvec", 553 + "zerovec", 554 + ] 555 + 556 + [[package]] 557 + name = "icu_normalizer_data" 558 + version = "2.2.0" 559 + source = "registry+https://github.com/rust-lang/crates.io-index" 560 + checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" 561 + 562 + [[package]] 563 + name = "icu_properties" 564 + version = "2.2.0" 565 + source = "registry+https://github.com/rust-lang/crates.io-index" 566 + checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" 567 + dependencies = [ 568 + "icu_collections", 569 + "icu_locale_core", 570 + "icu_properties_data", 571 + "icu_provider", 572 + "zerotrie", 573 + "zerovec", 574 + ] 575 + 576 + [[package]] 577 + name = "icu_properties_data" 578 + version = "2.2.0" 579 + source = "registry+https://github.com/rust-lang/crates.io-index" 580 + checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" 581 + 582 + [[package]] 583 + name = "icu_provider" 584 + version = "2.2.0" 585 + source = "registry+https://github.com/rust-lang/crates.io-index" 586 + checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" 587 + dependencies = [ 588 + "displaydoc", 589 + "icu_locale_core", 590 + "writeable", 591 + "yoke", 592 + "zerofrom", 593 + "zerotrie", 594 + "zerovec", 595 + ] 596 + 597 + [[package]] 249 598 name = "ident_case" 250 599 version = "1.0.1" 251 600 source = "registry+https://github.com/rust-lang/crates.io-index" 252 601 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 253 602 254 603 [[package]] 604 + name = "idna" 605 + version = "1.1.0" 606 + source = "registry+https://github.com/rust-lang/crates.io-index" 607 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 608 + dependencies = [ 609 + "idna_adapter", 610 + "smallvec", 611 + "utf8_iter", 612 + ] 613 + 614 + [[package]] 615 + name = "idna_adapter" 616 + version = "1.2.1" 617 + source = "registry+https://github.com/rust-lang/crates.io-index" 618 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 619 + dependencies = [ 620 + "icu_normalizer", 621 + "icu_properties", 622 + ] 623 + 624 + [[package]] 255 625 name = "indexmap" 256 626 version = "2.13.1" 257 627 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 284 654 ] 285 655 286 656 [[package]] 657 + name = "ipnet" 658 + version = "2.12.0" 659 + source = "registry+https://github.com/rust-lang/crates.io-index" 660 + checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" 661 + 662 + [[package]] 663 + name = "iri-string" 664 + version = "0.7.12" 665 + source = "registry+https://github.com/rust-lang/crates.io-index" 666 + checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" 667 + dependencies = [ 668 + "memchr", 669 + "serde", 670 + ] 671 + 672 + [[package]] 287 673 name = "itertools" 288 674 version = "0.13.0" 289 675 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 299 685 checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" 300 686 301 687 [[package]] 688 + name = "js-sys" 689 + version = "0.3.95" 690 + source = "registry+https://github.com/rust-lang/crates.io-index" 691 + checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" 692 + dependencies = [ 693 + "cfg-if", 694 + "futures-util", 695 + "once_cell", 696 + "wasm-bindgen", 697 + ] 698 + 699 + [[package]] 302 700 name = "leaf" 303 701 version = "1.4.2" 304 702 dependencies = [ ··· 306 704 "crossterm", 307 705 "pulldown-cmark", 308 706 "ratatui", 707 + "reqwest", 708 + "semver", 709 + "serde", 710 + "sha2", 309 711 "syntect", 310 712 "unicode-width 0.1.14", 311 713 ] ··· 327 729 version = "0.4.15" 328 730 source = "registry+https://github.com/rust-lang/crates.io-index" 329 731 checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 732 + 733 + [[package]] 734 + name = "litemap" 735 + version = "0.8.2" 736 + source = "registry+https://github.com/rust-lang/crates.io-index" 737 + checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" 330 738 331 739 [[package]] 332 740 name = "lock_api" ··· 353 761 ] 354 762 355 763 [[package]] 764 + name = "lru-slab" 765 + version = "0.1.2" 766 + source = "registry+https://github.com/rust-lang/crates.io-index" 767 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 768 + 769 + [[package]] 356 770 name = "memchr" 357 771 version = "2.8.0" 358 772 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 444 858 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 445 859 446 860 [[package]] 861 + name = "percent-encoding" 862 + version = "2.3.2" 863 + source = "registry+https://github.com/rust-lang/crates.io-index" 864 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 865 + 866 + [[package]] 867 + name = "pin-project-lite" 868 + version = "0.2.17" 869 + source = "registry+https://github.com/rust-lang/crates.io-index" 870 + checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" 871 + 872 + [[package]] 447 873 name = "pkg-config" 448 874 version = "0.3.32" 449 875 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 463 889 ] 464 890 465 891 [[package]] 892 + name = "potential_utf" 893 + version = "0.1.5" 894 + source = "registry+https://github.com/rust-lang/crates.io-index" 895 + checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" 896 + dependencies = [ 897 + "zerovec", 898 + ] 899 + 900 + [[package]] 466 901 name = "powerfmt" 467 902 version = "0.2.0" 468 903 source = "registry+https://github.com/rust-lang/crates.io-index" 469 904 checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 470 905 471 906 [[package]] 907 + name = "ppv-lite86" 908 + version = "0.2.21" 909 + source = "registry+https://github.com/rust-lang/crates.io-index" 910 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 911 + dependencies = [ 912 + "zerocopy", 913 + ] 914 + 915 + [[package]] 472 916 name = "proc-macro2" 473 917 version = "1.0.106" 474 918 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 506 950 ] 507 951 508 952 [[package]] 953 + name = "quinn" 954 + version = "0.11.9" 955 + source = "registry+https://github.com/rust-lang/crates.io-index" 956 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 957 + dependencies = [ 958 + "bytes", 959 + "cfg_aliases", 960 + "pin-project-lite", 961 + "quinn-proto", 962 + "quinn-udp", 963 + "rustc-hash", 964 + "rustls", 965 + "socket2", 966 + "thiserror", 967 + "tokio", 968 + "tracing", 969 + "web-time", 970 + ] 971 + 972 + [[package]] 973 + name = "quinn-proto" 974 + version = "0.11.14" 975 + source = "registry+https://github.com/rust-lang/crates.io-index" 976 + checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" 977 + dependencies = [ 978 + "bytes", 979 + "getrandom 0.3.4", 980 + "lru-slab", 981 + "rand", 982 + "ring", 983 + "rustc-hash", 984 + "rustls", 985 + "rustls-pki-types", 986 + "slab", 987 + "thiserror", 988 + "tinyvec", 989 + "tracing", 990 + "web-time", 991 + ] 992 + 993 + [[package]] 994 + name = "quinn-udp" 995 + version = "0.5.14" 996 + source = "registry+https://github.com/rust-lang/crates.io-index" 997 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 998 + dependencies = [ 999 + "cfg_aliases", 1000 + "libc", 1001 + "once_cell", 1002 + "socket2", 1003 + "tracing", 1004 + "windows-sys 0.59.0", 1005 + ] 1006 + 1007 + [[package]] 509 1008 name = "quote" 510 1009 version = "1.0.45" 511 1010 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 515 1014 ] 516 1015 517 1016 [[package]] 1017 + name = "r-efi" 1018 + version = "5.3.0" 1019 + source = "registry+https://github.com/rust-lang/crates.io-index" 1020 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1021 + 1022 + [[package]] 1023 + name = "rand" 1024 + version = "0.9.2" 1025 + source = "registry+https://github.com/rust-lang/crates.io-index" 1026 + checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 1027 + dependencies = [ 1028 + "rand_chacha", 1029 + "rand_core", 1030 + ] 1031 + 1032 + [[package]] 1033 + name = "rand_chacha" 1034 + version = "0.9.0" 1035 + source = "registry+https://github.com/rust-lang/crates.io-index" 1036 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1037 + dependencies = [ 1038 + "ppv-lite86", 1039 + "rand_core", 1040 + ] 1041 + 1042 + [[package]] 1043 + name = "rand_core" 1044 + version = "0.9.5" 1045 + source = "registry+https://github.com/rust-lang/crates.io-index" 1046 + checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" 1047 + dependencies = [ 1048 + "getrandom 0.3.4", 1049 + ] 1050 + 1051 + [[package]] 518 1052 name = "ratatui" 519 1053 version = "0.29.0" 520 1054 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 551 1085 checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" 552 1086 553 1087 [[package]] 1088 + name = "reqwest" 1089 + version = "0.12.28" 1090 + source = "registry+https://github.com/rust-lang/crates.io-index" 1091 + checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 1092 + dependencies = [ 1093 + "base64", 1094 + "bytes", 1095 + "futures-channel", 1096 + "futures-core", 1097 + "futures-util", 1098 + "http", 1099 + "http-body", 1100 + "http-body-util", 1101 + "hyper", 1102 + "hyper-rustls", 1103 + "hyper-util", 1104 + "js-sys", 1105 + "log", 1106 + "percent-encoding", 1107 + "pin-project-lite", 1108 + "quinn", 1109 + "rustls", 1110 + "rustls-pki-types", 1111 + "serde", 1112 + "serde_json", 1113 + "serde_urlencoded", 1114 + "sync_wrapper", 1115 + "tokio", 1116 + "tokio-rustls", 1117 + "tower", 1118 + "tower-http", 1119 + "tower-service", 1120 + "url", 1121 + "wasm-bindgen", 1122 + "wasm-bindgen-futures", 1123 + "web-sys", 1124 + "webpki-roots", 1125 + ] 1126 + 1127 + [[package]] 1128 + name = "ring" 1129 + version = "0.17.14" 1130 + source = "registry+https://github.com/rust-lang/crates.io-index" 1131 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1132 + dependencies = [ 1133 + "cc", 1134 + "cfg-if", 1135 + "getrandom 0.2.17", 1136 + "libc", 1137 + "untrusted", 1138 + "windows-sys 0.52.0", 1139 + ] 1140 + 1141 + [[package]] 1142 + name = "rustc-hash" 1143 + version = "2.1.2" 1144 + source = "registry+https://github.com/rust-lang/crates.io-index" 1145 + checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" 1146 + 1147 + [[package]] 554 1148 name = "rustix" 555 1149 version = "0.38.44" 556 1150 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 564 1158 ] 565 1159 566 1160 [[package]] 1161 + name = "rustls" 1162 + version = "0.23.37" 1163 + source = "registry+https://github.com/rust-lang/crates.io-index" 1164 + checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" 1165 + dependencies = [ 1166 + "once_cell", 1167 + "ring", 1168 + "rustls-pki-types", 1169 + "rustls-webpki", 1170 + "subtle", 1171 + "zeroize", 1172 + ] 1173 + 1174 + [[package]] 1175 + name = "rustls-pki-types" 1176 + version = "1.14.0" 1177 + source = "registry+https://github.com/rust-lang/crates.io-index" 1178 + checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" 1179 + dependencies = [ 1180 + "web-time", 1181 + "zeroize", 1182 + ] 1183 + 1184 + [[package]] 1185 + name = "rustls-webpki" 1186 + version = "0.103.11" 1187 + source = "registry+https://github.com/rust-lang/crates.io-index" 1188 + checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" 1189 + dependencies = [ 1190 + "ring", 1191 + "rustls-pki-types", 1192 + "untrusted", 1193 + ] 1194 + 1195 + [[package]] 567 1196 name = "rustversion" 568 1197 version = "1.0.22" 569 1198 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 591 1220 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 592 1221 593 1222 [[package]] 1223 + name = "semver" 1224 + version = "1.0.28" 1225 + source = "registry+https://github.com/rust-lang/crates.io-index" 1226 + checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" 1227 + 1228 + [[package]] 594 1229 name = "serde" 595 1230 version = "1.0.228" 596 1231 source = "registry+https://github.com/rust-lang/crates.io-index" 597 1232 checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 598 1233 dependencies = [ 599 1234 "serde_core", 1235 + "serde_derive", 600 1236 ] 601 1237 602 1238 [[package]] ··· 633 1269 ] 634 1270 635 1271 [[package]] 1272 + name = "serde_urlencoded" 1273 + version = "0.7.1" 1274 + source = "registry+https://github.com/rust-lang/crates.io-index" 1275 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1276 + dependencies = [ 1277 + "form_urlencoded", 1278 + "itoa", 1279 + "ryu", 1280 + "serde", 1281 + ] 1282 + 1283 + [[package]] 1284 + name = "sha2" 1285 + version = "0.10.9" 1286 + source = "registry+https://github.com/rust-lang/crates.io-index" 1287 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 1288 + dependencies = [ 1289 + "cfg-if", 1290 + "cpufeatures", 1291 + "digest", 1292 + ] 1293 + 1294 + [[package]] 636 1295 name = "shlex" 637 1296 version = "1.3.0" 638 1297 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 676 1335 checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" 677 1336 678 1337 [[package]] 1338 + name = "slab" 1339 + version = "0.4.12" 1340 + source = "registry+https://github.com/rust-lang/crates.io-index" 1341 + checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" 1342 + 1343 + [[package]] 679 1344 name = "smallvec" 680 1345 version = "1.15.1" 681 1346 source = "registry+https://github.com/rust-lang/crates.io-index" 682 1347 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 683 1348 684 1349 [[package]] 1350 + name = "socket2" 1351 + version = "0.6.3" 1352 + source = "registry+https://github.com/rust-lang/crates.io-index" 1353 + checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" 1354 + dependencies = [ 1355 + "libc", 1356 + "windows-sys 0.61.2", 1357 + ] 1358 + 1359 + [[package]] 1360 + name = "stable_deref_trait" 1361 + version = "1.2.1" 1362 + source = "registry+https://github.com/rust-lang/crates.io-index" 1363 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 1364 + 1365 + [[package]] 685 1366 name = "static_assertions" 686 1367 version = "1.1.0" 687 1368 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 716 1397 ] 717 1398 718 1399 [[package]] 1400 + name = "subtle" 1401 + version = "2.6.1" 1402 + source = "registry+https://github.com/rust-lang/crates.io-index" 1403 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1404 + 1405 + [[package]] 719 1406 name = "syn" 720 1407 version = "2.0.117" 721 1408 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 727 1414 ] 728 1415 729 1416 [[package]] 1417 + name = "sync_wrapper" 1418 + version = "1.0.2" 1419 + source = "registry+https://github.com/rust-lang/crates.io-index" 1420 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1421 + dependencies = [ 1422 + "futures-core", 1423 + ] 1424 + 1425 + [[package]] 1426 + name = "synstructure" 1427 + version = "0.13.2" 1428 + source = "registry+https://github.com/rust-lang/crates.io-index" 1429 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1430 + dependencies = [ 1431 + "proc-macro2", 1432 + "quote", 1433 + "syn", 1434 + ] 1435 + 1436 + [[package]] 730 1437 name = "syntect" 731 1438 version = "5.3.0" 732 1439 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 799 1506 ] 800 1507 801 1508 [[package]] 1509 + name = "tinystr" 1510 + version = "0.8.3" 1511 + source = "registry+https://github.com/rust-lang/crates.io-index" 1512 + checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" 1513 + dependencies = [ 1514 + "displaydoc", 1515 + "zerovec", 1516 + ] 1517 + 1518 + [[package]] 1519 + name = "tinyvec" 1520 + version = "1.11.0" 1521 + source = "registry+https://github.com/rust-lang/crates.io-index" 1522 + checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" 1523 + dependencies = [ 1524 + "tinyvec_macros", 1525 + ] 1526 + 1527 + [[package]] 1528 + name = "tinyvec_macros" 1529 + version = "0.1.1" 1530 + source = "registry+https://github.com/rust-lang/crates.io-index" 1531 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1532 + 1533 + [[package]] 1534 + name = "tokio" 1535 + version = "1.51.1" 1536 + source = "registry+https://github.com/rust-lang/crates.io-index" 1537 + checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" 1538 + dependencies = [ 1539 + "bytes", 1540 + "libc", 1541 + "mio", 1542 + "pin-project-lite", 1543 + "socket2", 1544 + "windows-sys 0.61.2", 1545 + ] 1546 + 1547 + [[package]] 1548 + name = "tokio-rustls" 1549 + version = "0.26.4" 1550 + source = "registry+https://github.com/rust-lang/crates.io-index" 1551 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 1552 + dependencies = [ 1553 + "rustls", 1554 + "tokio", 1555 + ] 1556 + 1557 + [[package]] 1558 + name = "tower" 1559 + version = "0.5.3" 1560 + source = "registry+https://github.com/rust-lang/crates.io-index" 1561 + checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" 1562 + dependencies = [ 1563 + "futures-core", 1564 + "futures-util", 1565 + "pin-project-lite", 1566 + "sync_wrapper", 1567 + "tokio", 1568 + "tower-layer", 1569 + "tower-service", 1570 + ] 1571 + 1572 + [[package]] 1573 + name = "tower-http" 1574 + version = "0.6.8" 1575 + source = "registry+https://github.com/rust-lang/crates.io-index" 1576 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 1577 + dependencies = [ 1578 + "bitflags", 1579 + "bytes", 1580 + "futures-util", 1581 + "http", 1582 + "http-body", 1583 + "iri-string", 1584 + "pin-project-lite", 1585 + "tower", 1586 + "tower-layer", 1587 + "tower-service", 1588 + ] 1589 + 1590 + [[package]] 1591 + name = "tower-layer" 1592 + version = "0.3.3" 1593 + source = "registry+https://github.com/rust-lang/crates.io-index" 1594 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1595 + 1596 + [[package]] 1597 + name = "tower-service" 1598 + version = "0.3.3" 1599 + source = "registry+https://github.com/rust-lang/crates.io-index" 1600 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1601 + 1602 + [[package]] 1603 + name = "tracing" 1604 + version = "0.1.44" 1605 + source = "registry+https://github.com/rust-lang/crates.io-index" 1606 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 1607 + dependencies = [ 1608 + "pin-project-lite", 1609 + "tracing-core", 1610 + ] 1611 + 1612 + [[package]] 1613 + name = "tracing-core" 1614 + version = "0.1.36" 1615 + source = "registry+https://github.com/rust-lang/crates.io-index" 1616 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 1617 + dependencies = [ 1618 + "once_cell", 1619 + ] 1620 + 1621 + [[package]] 1622 + name = "try-lock" 1623 + version = "0.2.5" 1624 + source = "registry+https://github.com/rust-lang/crates.io-index" 1625 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1626 + 1627 + [[package]] 1628 + name = "typenum" 1629 + version = "1.19.0" 1630 + source = "registry+https://github.com/rust-lang/crates.io-index" 1631 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 1632 + 1633 + [[package]] 802 1634 name = "unicase" 803 1635 version = "2.9.0" 804 1636 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 840 1672 checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 841 1673 842 1674 [[package]] 1675 + name = "untrusted" 1676 + version = "0.9.0" 1677 + source = "registry+https://github.com/rust-lang/crates.io-index" 1678 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1679 + 1680 + [[package]] 1681 + name = "url" 1682 + version = "2.5.8" 1683 + source = "registry+https://github.com/rust-lang/crates.io-index" 1684 + checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" 1685 + dependencies = [ 1686 + "form_urlencoded", 1687 + "idna", 1688 + "percent-encoding", 1689 + "serde", 1690 + ] 1691 + 1692 + [[package]] 1693 + name = "utf8_iter" 1694 + version = "1.0.4" 1695 + source = "registry+https://github.com/rust-lang/crates.io-index" 1696 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1697 + 1698 + [[package]] 1699 + name = "version_check" 1700 + version = "0.9.5" 1701 + source = "registry+https://github.com/rust-lang/crates.io-index" 1702 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1703 + 1704 + [[package]] 843 1705 name = "walkdir" 844 1706 version = "2.5.0" 845 1707 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 850 1712 ] 851 1713 852 1714 [[package]] 1715 + name = "want" 1716 + version = "0.3.1" 1717 + source = "registry+https://github.com/rust-lang/crates.io-index" 1718 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1719 + dependencies = [ 1720 + "try-lock", 1721 + ] 1722 + 1723 + [[package]] 853 1724 name = "wasi" 854 1725 version = "0.11.1+wasi-snapshot-preview1" 855 1726 source = "registry+https://github.com/rust-lang/crates.io-index" 856 1727 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 857 1728 858 1729 [[package]] 1730 + name = "wasip2" 1731 + version = "1.0.2+wasi-0.2.9" 1732 + source = "registry+https://github.com/rust-lang/crates.io-index" 1733 + checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" 1734 + dependencies = [ 1735 + "wit-bindgen", 1736 + ] 1737 + 1738 + [[package]] 1739 + name = "wasm-bindgen" 1740 + version = "0.2.118" 1741 + source = "registry+https://github.com/rust-lang/crates.io-index" 1742 + checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" 1743 + dependencies = [ 1744 + "cfg-if", 1745 + "once_cell", 1746 + "rustversion", 1747 + "wasm-bindgen-macro", 1748 + "wasm-bindgen-shared", 1749 + ] 1750 + 1751 + [[package]] 1752 + name = "wasm-bindgen-futures" 1753 + version = "0.4.68" 1754 + source = "registry+https://github.com/rust-lang/crates.io-index" 1755 + checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" 1756 + dependencies = [ 1757 + "js-sys", 1758 + "wasm-bindgen", 1759 + ] 1760 + 1761 + [[package]] 1762 + name = "wasm-bindgen-macro" 1763 + version = "0.2.118" 1764 + source = "registry+https://github.com/rust-lang/crates.io-index" 1765 + checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" 1766 + dependencies = [ 1767 + "quote", 1768 + "wasm-bindgen-macro-support", 1769 + ] 1770 + 1771 + [[package]] 1772 + name = "wasm-bindgen-macro-support" 1773 + version = "0.2.118" 1774 + source = "registry+https://github.com/rust-lang/crates.io-index" 1775 + checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" 1776 + dependencies = [ 1777 + "bumpalo", 1778 + "proc-macro2", 1779 + "quote", 1780 + "syn", 1781 + "wasm-bindgen-shared", 1782 + ] 1783 + 1784 + [[package]] 1785 + name = "wasm-bindgen-shared" 1786 + version = "0.2.118" 1787 + source = "registry+https://github.com/rust-lang/crates.io-index" 1788 + checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" 1789 + dependencies = [ 1790 + "unicode-ident", 1791 + ] 1792 + 1793 + [[package]] 1794 + name = "web-sys" 1795 + version = "0.3.95" 1796 + source = "registry+https://github.com/rust-lang/crates.io-index" 1797 + checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" 1798 + dependencies = [ 1799 + "js-sys", 1800 + "wasm-bindgen", 1801 + ] 1802 + 1803 + [[package]] 1804 + name = "web-time" 1805 + version = "1.1.0" 1806 + source = "registry+https://github.com/rust-lang/crates.io-index" 1807 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 1808 + dependencies = [ 1809 + "js-sys", 1810 + "wasm-bindgen", 1811 + ] 1812 + 1813 + [[package]] 1814 + name = "webpki-roots" 1815 + version = "1.0.6" 1816 + source = "registry+https://github.com/rust-lang/crates.io-index" 1817 + checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" 1818 + dependencies = [ 1819 + "rustls-pki-types", 1820 + ] 1821 + 1822 + [[package]] 859 1823 name = "winapi" 860 1824 version = "0.3.9" 861 1825 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 891 1855 version = "0.2.1" 892 1856 source = "registry+https://github.com/rust-lang/crates.io-index" 893 1857 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1858 + 1859 + [[package]] 1860 + name = "windows-sys" 1861 + version = "0.52.0" 1862 + source = "registry+https://github.com/rust-lang/crates.io-index" 1863 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1864 + dependencies = [ 1865 + "windows-targets", 1866 + ] 894 1867 895 1868 [[package]] 896 1869 name = "windows-sys" ··· 975 1948 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 976 1949 977 1950 [[package]] 1951 + name = "wit-bindgen" 1952 + version = "0.51.0" 1953 + source = "registry+https://github.com/rust-lang/crates.io-index" 1954 + checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 1955 + 1956 + [[package]] 1957 + name = "writeable" 1958 + version = "0.6.3" 1959 + source = "registry+https://github.com/rust-lang/crates.io-index" 1960 + checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" 1961 + 1962 + [[package]] 978 1963 name = "yaml-rust" 979 1964 version = "0.4.5" 980 1965 source = "registry+https://github.com/rust-lang/crates.io-index" 981 1966 checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 982 1967 dependencies = [ 983 1968 "linked-hash-map", 1969 + ] 1970 + 1971 + [[package]] 1972 + name = "yoke" 1973 + version = "0.8.2" 1974 + source = "registry+https://github.com/rust-lang/crates.io-index" 1975 + checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" 1976 + dependencies = [ 1977 + "stable_deref_trait", 1978 + "yoke-derive", 1979 + "zerofrom", 1980 + ] 1981 + 1982 + [[package]] 1983 + name = "yoke-derive" 1984 + version = "0.8.2" 1985 + source = "registry+https://github.com/rust-lang/crates.io-index" 1986 + checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" 1987 + dependencies = [ 1988 + "proc-macro2", 1989 + "quote", 1990 + "syn", 1991 + "synstructure", 1992 + ] 1993 + 1994 + [[package]] 1995 + name = "zerocopy" 1996 + version = "0.8.48" 1997 + source = "registry+https://github.com/rust-lang/crates.io-index" 1998 + checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" 1999 + dependencies = [ 2000 + "zerocopy-derive", 2001 + ] 2002 + 2003 + [[package]] 2004 + name = "zerocopy-derive" 2005 + version = "0.8.48" 2006 + source = "registry+https://github.com/rust-lang/crates.io-index" 2007 + checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" 2008 + dependencies = [ 2009 + "proc-macro2", 2010 + "quote", 2011 + "syn", 2012 + ] 2013 + 2014 + [[package]] 2015 + name = "zerofrom" 2016 + version = "0.1.7" 2017 + source = "registry+https://github.com/rust-lang/crates.io-index" 2018 + checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" 2019 + dependencies = [ 2020 + "zerofrom-derive", 2021 + ] 2022 + 2023 + [[package]] 2024 + name = "zerofrom-derive" 2025 + version = "0.1.7" 2026 + source = "registry+https://github.com/rust-lang/crates.io-index" 2027 + checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" 2028 + dependencies = [ 2029 + "proc-macro2", 2030 + "quote", 2031 + "syn", 2032 + "synstructure", 2033 + ] 2034 + 2035 + [[package]] 2036 + name = "zeroize" 2037 + version = "1.8.2" 2038 + source = "registry+https://github.com/rust-lang/crates.io-index" 2039 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 2040 + 2041 + [[package]] 2042 + name = "zerotrie" 2043 + version = "0.2.4" 2044 + source = "registry+https://github.com/rust-lang/crates.io-index" 2045 + checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" 2046 + dependencies = [ 2047 + "displaydoc", 2048 + "yoke", 2049 + "zerofrom", 2050 + ] 2051 + 2052 + [[package]] 2053 + name = "zerovec" 2054 + version = "0.11.6" 2055 + source = "registry+https://github.com/rust-lang/crates.io-index" 2056 + checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" 2057 + dependencies = [ 2058 + "yoke", 2059 + "zerofrom", 2060 + "zerovec-derive", 2061 + ] 2062 + 2063 + [[package]] 2064 + name = "zerovec-derive" 2065 + version = "0.11.3" 2066 + source = "registry+https://github.com/rust-lang/crates.io-index" 2067 + checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" 2068 + dependencies = [ 2069 + "proc-macro2", 2070 + "quote", 2071 + "syn", 984 2072 ] 985 2073 986 2074 [[package]]
+4
Cargo.toml
··· 16 16 syntect = "5.2" 17 17 anyhow = "1.0" 18 18 unicode-width = "0.1" 19 + reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } 20 + serde = { version = "1.0", features = ["derive"] } 21 + semver = "1.0" 22 + sha2 = "0.10"
+12
README.md
··· 75 75 76 76 # Open the file picker in the current directory 77 77 leaf 78 + 79 + # Update to the latest published version 80 + leaf --update 78 81 ``` 79 82 80 83 ## Keybindings ··· 107 110 - ✅ Bold, italic, strikethrough, blockquotes, lists, and horizontal rules 108 111 - ✅ YAML frontmatter is ignored in both preview and TOC 109 112 - ✅ Native stdin input with bounded size 113 + - ✅ `leaf --update` to fetch, verify via published SHA256, and install the latest release on supported platforms 110 114 - ✅ File picker when launched without a file 111 115 - ✅ Theme picker with runtime preview 112 116 - ✅ Help modal with in-app shortcuts ··· 141 145 - https://aka.ms/vc14/vc_redist.x64.exe 142 146 143 147 For `leaf-windows-x86_64.exe`, the relevant package is the latest supported **X64** Visual C++ v14 Redistributable. 148 + 149 + ### Windows: `leaf --update` 150 + 151 + `leaf --update` verifies the downloaded binary against the published `checksums.txt` SHA256 before installation. 152 + 153 + On Unix-like systems, it then replaces the current binary in place. 154 + 155 + On Windows, replacing the running `.exe` can be blocked by the OS. If `leaf --update` cannot complete there, rerun the PowerShell installer from the install section.
+13 -1
src/cli.rs
··· 5 5 #[derive(Debug, Default, PartialEq, Eq)] 6 6 pub(crate) struct CliOptions { 7 7 pub(crate) watch: bool, 8 + pub(crate) update: bool, 8 9 pub(crate) debug_input: bool, 9 10 pub(crate) print_help: bool, 10 11 pub(crate) print_version: bool, ··· 13 14 } 14 15 15 16 pub(crate) fn usage_text() -> &'static str { 16 - "Usage: leaf [--watch] [--theme arctic|forest|ocean|solarized-dark] <file.md>\n echo '# Hello' | leaf" 17 + "Usage: leaf [--watch] [--theme arctic|forest|ocean|solarized-dark] [file.md]\n leaf --update\n echo '# Hello' | leaf" 17 18 } 18 19 19 20 pub(crate) fn version_text() -> &'static str { ··· 45 46 46 47 match arg.as_str() { 47 48 "--watch" | "-w" => options.watch = true, 49 + "--update" => options.update = true, 48 50 "--debug-input" => options.debug_input = true, 49 51 "--help" | "-h" => options.print_help = true, 50 52 "--version" | "-V" => options.print_version = true, ··· 64 66 _ if arg.starts_with('-') => anyhow::bail!("Unknown flag: {arg}"), 65 67 _ if options.file_arg.is_none() => options.file_arg = Some(arg.clone()), 66 68 _ => anyhow::bail!("Too many file arguments"), 69 + } 70 + } 71 + 72 + if options.update { 73 + let has_non_update_flags = options.watch 74 + || options.debug_input 75 + || options.file_arg.is_some() 76 + || options.theme != ThemePreset::default(); 77 + if has_non_update_flags { 78 + anyhow::bail!("--update must be used on its own"); 67 79 } 68 80 } 69 81
+11
src/main.rs
··· 12 12 #[cfg(test)] 13 13 mod tests; 14 14 mod theme; 15 + mod update; 15 16 16 17 use app::{App, AppConfig}; 17 18 use cli::{parse_cli, print_usage, print_version, CliOptions}; ··· 19 20 use runtime::run; 20 21 use terminal::{finish_with_restore, TerminalSession}; 21 22 use theme::{current_syntect_theme, set_theme_preset}; 23 + use update::run_update; 22 24 23 25 const MAX_STDIN_BYTES: usize = 8 * 1024 * 1024; 24 26 ··· 32 34 pub(crate) use runtime::should_handle_key; 33 35 #[cfg(test)] 34 36 pub(crate) use theme::{parse_theme_preset, theme_preset_label, ThemePreset, THEME_PRESETS}; 37 + #[cfg(test)] 38 + pub(crate) use update::{ 39 + asset_name_for_target, expected_asset_download_url, find_expected_checksum, 40 + is_newer_version, validate_download_size, validate_sha256_hex, 41 + }; 35 42 #[cfg(test)] 36 43 pub(crate) use read_stdin_limited as read_stdin_with_limit; 37 44 ··· 64 71 } 65 72 if options.print_version { 66 73 print_version(); 74 + return Ok(()); 75 + } 76 + if options.update { 77 + run_update()?; 67 78 return Ok(()); 68 79 } 69 80 let CliOptions {
+120
src/tests.rs
··· 1 1 use crate::theme::{current_theme_preset, set_theme_preset, theme_preset_index}; 2 + use crate::update::TestAsset; 2 3 use crate::*; 3 4 use crate::app::FileChange; 4 5 use crate::markdown::{ ··· 100 101 err.to_string() 101 102 .contains("stdin exceeds the maximum supported size") 102 103 ); 104 + } 105 + 106 + #[test] 107 + fn parse_cli_accepts_update_on_its_own() { 108 + let args = vec!["leaf".to_string(), "--update".to_string()]; 109 + let options = parse_cli(&args).unwrap(); 110 + 111 + assert!(options.update); 112 + assert!(!options.watch); 113 + assert_eq!(options.file_arg, None); 114 + } 115 + 116 + #[test] 117 + fn parse_cli_rejects_update_with_other_flags() { 118 + let args = vec![ 119 + "leaf".to_string(), 120 + "--update".to_string(), 121 + "--watch".to_string(), 122 + ]; 123 + 124 + let err = parse_cli(&args).unwrap_err(); 125 + assert!(err.to_string().contains("--update must be used on its own")); 126 + } 127 + 128 + #[test] 129 + fn asset_name_matches_supported_release_targets() { 130 + assert_eq!(asset_name_for_target("macos", "x86_64"), Some("leaf-macos-x86_64")); 131 + assert_eq!(asset_name_for_target("macos", "aarch64"), Some("leaf-macos-arm64")); 132 + assert_eq!(asset_name_for_target("linux", "x86_64"), Some("leaf-linux-x86_64")); 133 + assert_eq!(asset_name_for_target("linux", "aarch64"), Some("leaf-linux-arm64")); 134 + assert_eq!(asset_name_for_target("android", "aarch64"), Some("leaf-android-arm64")); 135 + assert_eq!( 136 + asset_name_for_target("windows", "x86_64"), 137 + Some("leaf-windows-x86_64.exe") 138 + ); 139 + assert_eq!(asset_name_for_target("linux", "arm"), None); 140 + } 141 + 142 + #[test] 143 + fn newer_version_comparison_accepts_optional_v_prefix() { 144 + assert!(is_newer_version("1.4.2", "v1.4.3").unwrap()); 145 + assert!(!is_newer_version("1.4.2", "1.4.2").unwrap()); 146 + assert!(!is_newer_version("1.4.2", "1.4.1").unwrap()); 147 + } 148 + 149 + #[test] 150 + fn expected_asset_download_url_selects_matching_asset() { 151 + let assets = vec![ 152 + TestAsset { 153 + name: "leaf-linux-x86_64", 154 + download_url: "https://example.test/linux", 155 + }, 156 + TestAsset { 157 + name: "leaf-windows-x86_64.exe", 158 + download_url: "https://example.test/windows", 159 + }, 160 + ]; 161 + 162 + let url = expected_asset_download_url("1.4.3", &assets, "leaf-linux-x86_64").unwrap(); 163 + assert_eq!(url, "https://example.test/linux"); 164 + } 165 + 166 + #[test] 167 + fn expected_asset_download_url_errors_when_asset_is_missing() { 168 + let assets = vec![TestAsset { 169 + name: "leaf-linux-x86_64", 170 + download_url: "https://example.test/linux", 171 + }]; 172 + 173 + let err = expected_asset_download_url("1.4.3", &assets, "leaf-macos-arm64").unwrap_err(); 174 + assert!(err.to_string().contains("does not contain asset")); 175 + } 176 + 177 + #[test] 178 + fn validate_download_size_accepts_matching_non_zero_sizes() { 179 + assert!(validate_download_size(Some(42), 42).is_ok()); 180 + assert!(validate_download_size(None, 42).is_ok()); 181 + } 182 + 183 + #[test] 184 + fn validate_download_size_rejects_zero_or_mismatched_sizes() { 185 + let empty_err = validate_download_size(None, 0).unwrap_err(); 186 + assert!(empty_err.to_string().contains("is empty")); 187 + 188 + let mismatch_err = validate_download_size(Some(42), 41).unwrap_err(); 189 + assert!(mismatch_err.to_string().contains("size mismatch")); 190 + } 191 + 192 + #[test] 193 + fn find_expected_checksum_extracts_matching_asset_checksum() { 194 + let checksums = "\ 195 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa leaf-linux-x86_64 196 + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb leaf-windows-x86_64.exe 197 + "; 198 + 199 + let checksum = find_expected_checksum(checksums, "leaf-windows-x86_64.exe").unwrap(); 200 + assert_eq!( 201 + checksum, 202 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" 203 + ); 204 + } 205 + 206 + #[test] 207 + fn find_expected_checksum_rejects_missing_or_invalid_entries() { 208 + let missing = find_expected_checksum("abcd leaf-linux-x86_64\n", "leaf-macos-arm64") 209 + .unwrap_err(); 210 + assert!(missing.to_string().contains("does not contain")); 211 + 212 + let invalid = find_expected_checksum("xyz leaf-linux-x86_64\n", "leaf-linux-x86_64") 213 + .unwrap_err(); 214 + assert!(invalid.to_string().contains("Invalid SHA256 checksum format")); 215 + } 216 + 217 + #[test] 218 + fn validate_sha256_hex_accepts_expected_format() { 219 + assert!(validate_sha256_hex( 220 + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" 221 + ) 222 + .is_ok()); 103 223 } 104 224 105 225 #[test]
+367
src/update.rs
··· 1 + use anyhow::{bail, Context, Result}; 2 + use reqwest::blocking::Client; 3 + use semver::Version; 4 + use serde::Deserialize; 5 + use sha2::{Digest, Sha256}; 6 + use std::{ 7 + fs, 8 + path::{Path, PathBuf}, 9 + time::Duration, 10 + }; 11 + 12 + const GITHUB_API_BASE: &str = "https://api.github.com/repos/RivoLink/leaf"; 13 + const GITHUB_API_RELEASES_LATEST_PATH: &str = "/releases/latest"; 14 + const CHECKSUMS_ASSET_NAME: &str = "checksums.txt"; 15 + const HTTP_TIMEOUT: Duration = Duration::from_secs(20); 16 + 17 + #[derive(Debug, Deserialize)] 18 + struct GithubRelease { 19 + tag_name: String, 20 + assets: Vec<GithubAsset>, 21 + } 22 + 23 + #[derive(Debug, Deserialize)] 24 + struct GithubAsset { 25 + name: String, 26 + browser_download_url: String, 27 + } 28 + 29 + pub(crate) fn run_update() -> Result<()> { 30 + let current_version = env!("CARGO_PKG_VERSION"); 31 + let asset_name = current_asset_name()?; 32 + let release = fetch_latest_release()?; 33 + let latest_version = normalize_version_tag(&release.tag_name); 34 + 35 + if !is_newer_version(current_version, latest_version)? { 36 + println!("leaf {current_version} is already up to date"); 37 + return Ok(()); 38 + } 39 + 40 + let download_url = expected_asset_download_url(&release.tag_name, &release.assets, asset_name)?; 41 + let checksums_url = 42 + expected_asset_download_url(&release.tag_name, &release.assets, CHECKSUMS_ASSET_NAME)?; 43 + let checksums = download_text_asset(checksums_url)?; 44 + let expected_checksum = find_expected_checksum(&checksums, asset_name)?; 45 + let current_exe = std::env::current_exe().context("Cannot locate current executable")?; 46 + let temp_path = temp_download_path(&current_exe); 47 + 48 + download_asset(download_url, &temp_path)?; 49 + verify_download_checksum(&temp_path, expected_checksum)?; 50 + 51 + match replace_binary(&current_exe, &temp_path) { 52 + Ok(()) => { 53 + println!("Updated leaf from {current_version} to {latest_version}"); 54 + Ok(()) 55 + } 56 + Err(err) => { 57 + cleanup_file_if_exists(&temp_path); 58 + Err(err) 59 + } 60 + } 61 + } 62 + 63 + fn current_asset_name() -> Result<&'static str> { 64 + asset_name_for_target(std::env::consts::OS, std::env::consts::ARCH) 65 + .ok_or_else(|| anyhow::anyhow!("Unsupported platform: {} {}", std::env::consts::OS, std::env::consts::ARCH)) 66 + } 67 + 68 + pub(crate) fn asset_name_for_target(os: &str, arch: &str) -> Option<&'static str> { 69 + match (os, arch) { 70 + ("macos", "x86_64") => Some("leaf-macos-x86_64"), 71 + ("macos", "aarch64") => Some("leaf-macos-arm64"), 72 + ("linux", "x86_64") => Some("leaf-linux-x86_64"), 73 + ("linux", "aarch64") => Some("leaf-linux-arm64"), 74 + ("android", "aarch64") => Some("leaf-android-arm64"), 75 + ("windows", "x86_64") => Some("leaf-windows-x86_64.exe"), 76 + _ => None, 77 + } 78 + } 79 + 80 + fn fetch_latest_release() -> Result<GithubRelease> { 81 + let client = http_client()?; 82 + 83 + let response = client 84 + .get(format!( 85 + "{GITHUB_API_BASE}{GITHUB_API_RELEASES_LATEST_PATH}" 86 + )) 87 + .header(reqwest::header::USER_AGENT, "leaf-updater") 88 + .send() 89 + .context("Cannot reach GitHub releases API")?; 90 + 91 + let status = response.status(); 92 + if status == reqwest::StatusCode::FORBIDDEN { 93 + bail!("GitHub API request was forbidden or rate-limited"); 94 + } 95 + if status == reqwest::StatusCode::NOT_FOUND { 96 + bail!("Latest GitHub release was not found"); 97 + } 98 + if !status.is_success() { 99 + bail!("GitHub releases API returned HTTP {status}"); 100 + } 101 + 102 + response 103 + .json::<GithubRelease>() 104 + .context("Cannot parse GitHub release metadata") 105 + } 106 + 107 + pub(crate) fn expected_asset_download_url<'a>( 108 + tag_name: &str, 109 + assets: &'a [impl AsRefAsset], 110 + expected_asset: &str, 111 + ) -> Result<&'a str> { 112 + let _ = normalize_version_tag(tag_name); 113 + assets 114 + .iter() 115 + .find(|asset| asset.name() == expected_asset) 116 + .map(|asset| asset.download_url()) 117 + .ok_or_else(|| anyhow::anyhow!("Release does not contain asset {expected_asset}")) 118 + } 119 + 120 + pub(crate) trait AsRefAsset { 121 + fn name(&self) -> &str; 122 + fn download_url(&self) -> &str; 123 + } 124 + 125 + impl AsRefAsset for GithubAsset { 126 + fn name(&self) -> &str { 127 + &self.name 128 + } 129 + 130 + fn download_url(&self) -> &str { 131 + &self.browser_download_url 132 + } 133 + } 134 + 135 + pub(crate) fn is_newer_version(current: &str, remote: &str) -> Result<bool> { 136 + let current = Version::parse(normalize_version_tag(current)) 137 + .with_context(|| format!("Invalid current version: {current}"))?; 138 + let remote = Version::parse(normalize_version_tag(remote)) 139 + .with_context(|| format!("Invalid remote version: {remote}"))?; 140 + Ok(remote > current) 141 + } 142 + 143 + fn normalize_version_tag(version: &str) -> &str { 144 + version.strip_prefix('v').unwrap_or(version) 145 + } 146 + 147 + fn download_asset(url: &str, destination: &Path) -> Result<()> { 148 + cleanup_file_if_exists(destination); 149 + let _cleanup = TempFileGuard::new(destination.to_path_buf()); 150 + let client = http_client()?; 151 + let mut response = client 152 + .get(url) 153 + .header(reqwest::header::USER_AGENT, "leaf-updater") 154 + .send() 155 + .with_context(|| format!("Cannot download release asset: {url}"))?; 156 + 157 + let expected_len = validate_download_response(response.status(), response.content_length())?; 158 + let mut file = fs::File::create(destination) 159 + .with_context(|| format!("Cannot create temporary file: {}", destination.display()))?; 160 + let copied = response 161 + .copy_to(&mut file) 162 + .with_context(|| format!("Cannot write downloaded asset: {}", destination.display()))?; 163 + validate_download_size(expected_len, copied)?; 164 + file.sync_all() 165 + .with_context(|| format!("Cannot flush temporary file: {}", destination.display()))?; 166 + _cleanup.disarm(); 167 + Ok(()) 168 + } 169 + 170 + fn download_text_asset(url: &str) -> Result<String> { 171 + let client = http_client()?; 172 + let response = client 173 + .get(url) 174 + .header(reqwest::header::USER_AGENT, "leaf-updater") 175 + .send() 176 + .with_context(|| format!("Cannot download release metadata asset: {url}"))?; 177 + 178 + validate_download_response(response.status(), response.content_length())?; 179 + response 180 + .text() 181 + .with_context(|| format!("Cannot read release metadata asset: {url}")) 182 + } 183 + 184 + fn http_client() -> Result<Client> { 185 + let client = Client::builder() 186 + .timeout(HTTP_TIMEOUT) 187 + .build() 188 + .context("Cannot initialize HTTP client")?; 189 + Ok(client) 190 + } 191 + 192 + fn validate_download_response(status: reqwest::StatusCode, content_length: Option<u64>) -> Result<Option<u64>> { 193 + if status == reqwest::StatusCode::FORBIDDEN { 194 + bail!("Release asset download was forbidden or rate-limited"); 195 + } 196 + if status == reqwest::StatusCode::NOT_FOUND { 197 + bail!("Release asset was not found"); 198 + } 199 + if !status.is_success() { 200 + bail!("Release asset download returned HTTP {status}"); 201 + } 202 + if matches!(content_length, Some(0)) { 203 + bail!("Release asset download returned an empty body"); 204 + } 205 + Ok(content_length) 206 + } 207 + 208 + pub(crate) fn validate_download_size(expected: Option<u64>, actual: u64) -> Result<()> { 209 + if actual == 0 { 210 + bail!("Downloaded release asset is empty"); 211 + } 212 + if let Some(expected) = expected { 213 + if expected != actual { 214 + bail!( 215 + "Downloaded release asset size mismatch: expected {expected} bytes, got {actual}" 216 + ); 217 + } 218 + } 219 + Ok(()) 220 + } 221 + 222 + pub(crate) fn find_expected_checksum<'a>(checksums: &'a str, asset_name: &str) -> Result<&'a str> { 223 + for line in checksums.lines() { 224 + let trimmed = line.trim(); 225 + if trimmed.is_empty() { 226 + continue; 227 + } 228 + let mut parts = trimmed.split_whitespace(); 229 + let Some(checksum) = parts.next() else { 230 + continue; 231 + }; 232 + let Some(filename) = parts.next() else { 233 + continue; 234 + }; 235 + let normalized_filename = filename.trim_start_matches('*'); 236 + if normalized_filename == asset_name { 237 + validate_sha256_hex(checksum)?; 238 + return Ok(checksum); 239 + } 240 + } 241 + 242 + bail!("checksums.txt does not contain {asset_name}") 243 + } 244 + 245 + pub(crate) fn validate_sha256_hex(value: &str) -> Result<()> { 246 + if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) { 247 + bail!("Invalid SHA256 checksum format"); 248 + } 249 + Ok(()) 250 + } 251 + 252 + fn verify_download_checksum(path: &Path, expected_checksum: &str) -> Result<()> { 253 + let bytes = fs::read(path) 254 + .with_context(|| format!("Cannot read downloaded asset for checksum: {}", path.display()))?; 255 + let actual_checksum = format!("{:x}", Sha256::digest(&bytes)); 256 + 257 + if actual_checksum != expected_checksum { 258 + bail!( 259 + "Downloaded release asset checksum mismatch: expected {expected_checksum}, got {actual_checksum}" 260 + ); 261 + } 262 + Ok(()) 263 + } 264 + 265 + fn temp_download_path(current_exe: &Path) -> PathBuf { 266 + let extension = current_exe 267 + .extension() 268 + .map(|ext| format!("{}.download", ext.to_string_lossy())) 269 + .unwrap_or_else(|| "download".to_string()); 270 + current_exe.with_extension(extension) 271 + } 272 + 273 + #[cfg(unix)] 274 + fn replace_binary(current_exe: &Path, downloaded_path: &Path) -> Result<()> { 275 + let permissions = fs::metadata(current_exe) 276 + .with_context(|| format!("Cannot read current binary metadata: {}", current_exe.display()))? 277 + .permissions(); 278 + fs::set_permissions(downloaded_path, permissions).with_context(|| { 279 + format!( 280 + "Cannot apply executable permissions to {}", 281 + downloaded_path.display() 282 + ) 283 + })?; 284 + fs::rename(downloaded_path, current_exe).with_context(|| { 285 + format!( 286 + "Cannot replace current binary at {}", 287 + current_exe.display() 288 + ) 289 + })?; 290 + Ok(()) 291 + } 292 + 293 + #[cfg(windows)] 294 + fn replace_binary(current_exe: &Path, downloaded_path: &Path) -> Result<()> { 295 + let backup_path = current_exe.with_extension("old"); 296 + cleanup_file_if_exists(&backup_path); 297 + 298 + fs::rename(current_exe, &backup_path).with_context(|| { 299 + format!( 300 + "Cannot replace the running Windows binary at {}. Try the PowerShell installer instead.", 301 + current_exe.display() 302 + ) 303 + })?; 304 + 305 + if let Err(err) = fs::rename(downloaded_path, current_exe) { 306 + let _ = fs::rename(&backup_path, current_exe); 307 + bail!( 308 + "Cannot install the updated Windows binary at {}: {err}. Try the PowerShell installer instead.", 309 + current_exe.display() 310 + ); 311 + } 312 + 313 + cleanup_file_if_exists(&backup_path); 314 + Ok(()) 315 + } 316 + 317 + fn cleanup_file_if_exists(path: &Path) { 318 + if path.exists() { 319 + let _ = fs::remove_file(path); 320 + } 321 + } 322 + 323 + struct TempFileGuard { 324 + path: PathBuf, 325 + armed: bool, 326 + } 327 + 328 + impl TempFileGuard { 329 + fn new(path: PathBuf) -> Self { 330 + Self { path, armed: true } 331 + } 332 + 333 + fn disarm(mut self) { 334 + self.armed = false; 335 + } 336 + } 337 + 338 + impl Drop for TempFileGuard { 339 + fn drop(&mut self) { 340 + if self.armed { 341 + cleanup_file_if_exists(&self.path); 342 + } 343 + } 344 + } 345 + 346 + #[cfg(test)] 347 + pub(crate) use test_support::TestAsset; 348 + 349 + #[cfg(test)] 350 + mod test_support { 351 + use super::AsRefAsset; 352 + 353 + pub(crate) struct TestAsset<'a> { 354 + pub(crate) name: &'a str, 355 + pub(crate) download_url: &'a str, 356 + } 357 + 358 + impl AsRefAsset for TestAsset<'_> { 359 + fn name(&self) -> &str { 360 + self.name 361 + } 362 + 363 + fn download_url(&self) -> &str { 364 + self.download_url 365 + } 366 + } 367 + }