A better Rust ATProto crate
102
fork

Configure Feed

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

confirmed fixed the blob bug. http download stream works.

Orual d45b5d1b 9714fd1f

+2051 -112
+817 -8
Cargo.lock
··· 31 31 ] 32 32 33 33 [[package]] 34 + name = "adler" 35 + version = "1.0.2" 36 + source = "registry+https://github.com/rust-lang/crates.io-index" 37 + checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 38 + 39 + [[package]] 34 40 name = "adler2" 35 41 version = "2.0.1" 36 42 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 58 64 checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" 59 65 60 66 [[package]] 67 + name = "aligned-vec" 68 + version = "0.6.4" 69 + source = "registry+https://github.com/rust-lang/crates.io-index" 70 + checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" 71 + dependencies = [ 72 + "equator", 73 + ] 74 + 75 + [[package]] 61 76 name = "alloc-no-stdlib" 62 77 version = "2.0.4" 63 78 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 79 94 checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 80 95 dependencies = [ 81 96 "libc", 97 + ] 98 + 99 + [[package]] 100 + name = "ansi_colours" 101 + version = "1.2.3" 102 + source = "registry+https://github.com/rust-lang/crates.io-index" 103 + checksum = "14eec43e0298190790f41679fe69ef7a829d2a2ddd78c8c00339e84710e435fe" 104 + dependencies = [ 105 + "rgb", 82 106 ] 83 107 84 108 [[package]] ··· 138 162 checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 139 163 140 164 [[package]] 165 + name = "arbitrary" 166 + version = "1.4.2" 167 + source = "registry+https://github.com/rust-lang/crates.io-index" 168 + checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" 169 + 170 + [[package]] 171 + name = "arg_enum_proc_macro" 172 + version = "0.3.4" 173 + source = "registry+https://github.com/rust-lang/crates.io-index" 174 + checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" 175 + dependencies = [ 176 + "proc-macro2", 177 + "quote", 178 + "syn 2.0.106", 179 + ] 180 + 181 + [[package]] 182 + name = "arrayvec" 183 + version = "0.7.6" 184 + source = "registry+https://github.com/rust-lang/crates.io-index" 185 + checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 186 + 187 + [[package]] 141 188 name = "ascii" 142 189 version = "1.1.0" 143 190 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 180 227 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 181 228 182 229 [[package]] 230 + name = "av1-grain" 231 + version = "0.2.4" 232 + source = "registry+https://github.com/rust-lang/crates.io-index" 233 + checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" 234 + dependencies = [ 235 + "anyhow", 236 + "arrayvec", 237 + "log", 238 + "nom", 239 + "num-rational", 240 + "v_frame", 241 + ] 242 + 243 + [[package]] 244 + name = "avif-serialize" 245 + version = "0.8.6" 246 + source = "registry+https://github.com/rust-lang/crates.io-index" 247 + checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" 248 + dependencies = [ 249 + "arrayvec", 250 + ] 251 + 252 + [[package]] 183 253 name = "axum" 184 254 version = "0.8.6" 185 255 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 280 350 "addr2line", 281 351 "cfg-if", 282 352 "libc", 283 - "miniz_oxide", 353 + "miniz_oxide 0.8.9", 284 354 "object", 285 355 "rustc-demangle", 286 356 "windows-link 0.2.0", ··· 324 394 version = "1.8.0" 325 395 source = "registry+https://github.com/rust-lang/crates.io-index" 326 396 checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 397 + 398 + [[package]] 399 + name = "bit_field" 400 + version = "0.10.3" 401 + source = "registry+https://github.com/rust-lang/crates.io-index" 402 + checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" 327 403 328 404 [[package]] 329 405 name = "bitflags" ··· 332 408 checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 333 409 334 410 [[package]] 411 + name = "bitstream-io" 412 + version = "2.6.0" 413 + source = "registry+https://github.com/rust-lang/crates.io-index" 414 + checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" 415 + 416 + [[package]] 335 417 name = "block-buffer" 336 418 version = "0.10.4" 337 419 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 430 512 ] 431 513 432 514 [[package]] 515 + name = "built" 516 + version = "0.7.7" 517 + source = "registry+https://github.com/rust-lang/crates.io-index" 518 + checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" 519 + 520 + [[package]] 433 521 name = "bumpalo" 434 522 version = "3.19.0" 435 523 source = "registry+https://github.com/rust-lang/crates.io-index" 436 524 checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 437 525 438 526 [[package]] 527 + name = "bytemuck" 528 + version = "1.24.0" 529 + source = "registry+https://github.com/rust-lang/crates.io-index" 530 + checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" 531 + 532 + [[package]] 439 533 name = "byteorder" 440 534 version = "1.5.0" 441 535 source = "registry+https://github.com/rust-lang/crates.io-index" 442 536 checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 537 + 538 + [[package]] 539 + name = "byteorder-lite" 540 + version = "0.1.0" 541 + source = "registry+https://github.com/rust-lang/crates.io-index" 542 + checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 443 543 444 544 [[package]] 445 545 name = "bytes" ··· 472 572 checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" 473 573 dependencies = [ 474 574 "find-msvc-tools", 575 + "jobserver", 576 + "libc", 475 577 "shlex", 476 578 ] 477 579 ··· 491 593 checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 492 594 493 595 [[package]] 596 + name = "cfg-expr" 597 + version = "0.15.8" 598 + source = "registry+https://github.com/rust-lang/crates.io-index" 599 + checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" 600 + dependencies = [ 601 + "smallvec", 602 + "target-lexicon", 603 + ] 604 + 605 + [[package]] 494 606 name = "cfg-if" 495 607 version = "1.0.3" 496 608 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 604 716 checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 605 717 606 718 [[package]] 719 + name = "color_quant" 720 + version = "1.1.0" 721 + source = "registry+https://github.com/rust-lang/crates.io-index" 722 + checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 723 + 724 + [[package]] 607 725 name = "colorchoice" 608 726 version = "1.0.4" 609 727 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 637 755 checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" 638 756 639 757 [[package]] 758 + name = "console" 759 + version = "0.15.11" 760 + source = "registry+https://github.com/rust-lang/crates.io-index" 761 + checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 762 + dependencies = [ 763 + "encode_unicode", 764 + "libc", 765 + "once_cell", 766 + "windows-sys 0.59.0", 767 + ] 768 + 769 + [[package]] 640 770 name = "const-oid" 641 771 version = "0.9.6" 642 772 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 706 836 ] 707 837 708 838 [[package]] 839 + name = "crossbeam-deque" 840 + version = "0.8.6" 841 + source = "registry+https://github.com/rust-lang/crates.io-index" 842 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 843 + dependencies = [ 844 + "crossbeam-epoch", 845 + "crossbeam-utils", 846 + ] 847 + 848 + [[package]] 849 + name = "crossbeam-epoch" 850 + version = "0.9.18" 851 + source = "registry+https://github.com/rust-lang/crates.io-index" 852 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 853 + dependencies = [ 854 + "crossbeam-utils", 855 + ] 856 + 857 + [[package]] 709 858 name = "crossbeam-utils" 710 859 version = "0.8.21" 711 860 source = "registry+https://github.com/rust-lang/crates.io-index" 712 861 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 713 862 714 863 [[package]] 864 + name = "crossterm" 865 + version = "0.28.1" 866 + source = "registry+https://github.com/rust-lang/crates.io-index" 867 + checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 868 + dependencies = [ 869 + "bitflags", 870 + "crossterm_winapi", 871 + "parking_lot", 872 + "rustix 0.38.44", 873 + "winapi", 874 + ] 875 + 876 + [[package]] 877 + name = "crossterm_winapi" 878 + version = "0.9.1" 879 + source = "registry+https://github.com/rust-lang/crates.io-index" 880 + checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 881 + dependencies = [ 882 + "winapi", 883 + ] 884 + 885 + [[package]] 715 886 name = "crunchy" 716 887 version = "0.2.4" 717 888 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1005 1176 ] 1006 1177 1007 1178 [[package]] 1179 + name = "encode_unicode" 1180 + version = "1.0.0" 1181 + source = "registry+https://github.com/rust-lang/crates.io-index" 1182 + checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 1183 + 1184 + [[package]] 1008 1185 name = "encoding_rs" 1009 1186 version = "0.8.35" 1010 1187 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1020 1197 checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" 1021 1198 dependencies = [ 1022 1199 "heck 0.5.0", 1200 + "proc-macro2", 1201 + "quote", 1202 + "syn 2.0.106", 1203 + ] 1204 + 1205 + [[package]] 1206 + name = "equator" 1207 + version = "0.4.2" 1208 + source = "registry+https://github.com/rust-lang/crates.io-index" 1209 + checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" 1210 + dependencies = [ 1211 + "equator-macro", 1212 + ] 1213 + 1214 + [[package]] 1215 + name = "equator-macro" 1216 + version = "0.4.2" 1217 + source = "registry+https://github.com/rust-lang/crates.io-index" 1218 + checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" 1219 + dependencies = [ 1023 1220 "proc-macro2", 1024 1221 "quote", 1025 1222 "syn 2.0.106", ··· 1081 1278 ] 1082 1279 1083 1280 [[package]] 1281 + name = "exr" 1282 + version = "1.73.0" 1283 + source = "registry+https://github.com/rust-lang/crates.io-index" 1284 + checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" 1285 + dependencies = [ 1286 + "bit_field", 1287 + "half", 1288 + "lebe", 1289 + "miniz_oxide 0.8.9", 1290 + "rayon-core", 1291 + "smallvec", 1292 + "zune-inflate", 1293 + ] 1294 + 1295 + [[package]] 1084 1296 name = "fastrand" 1085 1297 version = "2.3.0" 1086 1298 source = "registry+https://github.com/rust-lang/crates.io-index" 1087 1299 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 1300 + 1301 + [[package]] 1302 + name = "fax" 1303 + version = "0.2.6" 1304 + source = "registry+https://github.com/rust-lang/crates.io-index" 1305 + checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" 1306 + dependencies = [ 1307 + "fax_derive", 1308 + ] 1309 + 1310 + [[package]] 1311 + name = "fax_derive" 1312 + version = "0.2.0" 1313 + source = "registry+https://github.com/rust-lang/crates.io-index" 1314 + checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" 1315 + dependencies = [ 1316 + "proc-macro2", 1317 + "quote", 1318 + "syn 2.0.106", 1319 + ] 1320 + 1321 + [[package]] 1322 + name = "fdeflate" 1323 + version = "0.3.7" 1324 + source = "registry+https://github.com/rust-lang/crates.io-index" 1325 + checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 1326 + dependencies = [ 1327 + "simd-adler32", 1328 + ] 1088 1329 1089 1330 [[package]] 1090 1331 name = "ff" ··· 1127 1368 checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 1128 1369 dependencies = [ 1129 1370 "crc32fast", 1130 - "miniz_oxide", 1371 + "miniz_oxide 0.8.9", 1131 1372 ] 1132 1373 1133 1374 [[package]] ··· 1341 1582 "r-efi", 1342 1583 "wasip2", 1343 1584 "wasm-bindgen", 1585 + ] 1586 + 1587 + [[package]] 1588 + name = "gif" 1589 + version = "0.13.3" 1590 + source = "registry+https://github.com/rust-lang/crates.io-index" 1591 + checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" 1592 + dependencies = [ 1593 + "color_quant", 1594 + "weezl", 1344 1595 ] 1345 1596 1346 1597 [[package]] ··· 1765 2016 ] 1766 2017 1767 2018 [[package]] 2019 + name = "image" 2020 + version = "0.25.8" 2021 + source = "registry+https://github.com/rust-lang/crates.io-index" 2022 + checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" 2023 + dependencies = [ 2024 + "bytemuck", 2025 + "byteorder-lite", 2026 + "color_quant", 2027 + "exr", 2028 + "gif", 2029 + "image-webp", 2030 + "moxcms", 2031 + "num-traits", 2032 + "png", 2033 + "qoi", 2034 + "ravif", 2035 + "rayon", 2036 + "rgb", 2037 + "tiff 0.10.3", 2038 + "zune-core", 2039 + "zune-jpeg", 2040 + ] 2041 + 2042 + [[package]] 2043 + name = "image-webp" 2044 + version = "0.2.4" 2045 + source = "registry+https://github.com/rust-lang/crates.io-index" 2046 + checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" 2047 + dependencies = [ 2048 + "byteorder-lite", 2049 + "quick-error 2.0.1", 2050 + ] 2051 + 2052 + [[package]] 2053 + name = "imgref" 2054 + version = "1.12.0" 2055 + source = "registry+https://github.com/rust-lang/crates.io-index" 2056 + checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" 2057 + 2058 + [[package]] 1768 2059 name = "indexmap" 1769 2060 version = "1.9.3" 1770 2061 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1792 2083 version = "2.0.6" 1793 2084 source = "registry+https://github.com/rust-lang/crates.io-index" 1794 2085 checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 2086 + 2087 + [[package]] 2088 + name = "interpolate_name" 2089 + version = "0.2.4" 2090 + source = "registry+https://github.com/rust-lang/crates.io-index" 2091 + checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" 2092 + dependencies = [ 2093 + "proc-macro2", 2094 + "quote", 2095 + "syn 2.0.106", 2096 + ] 1795 2097 1796 2098 [[package]] 1797 2099 name = "inventory" ··· 1866 2168 1867 2169 [[package]] 1868 2170 name = "itertools" 2171 + version = "0.12.1" 2172 + source = "registry+https://github.com/rust-lang/crates.io-index" 2173 + checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 2174 + dependencies = [ 2175 + "either", 2176 + ] 2177 + 2178 + [[package]] 2179 + name = "itertools" 1869 2180 version = "0.14.0" 1870 2181 source = "registry+https://github.com/rust-lang/crates.io-index" 1871 2182 checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" ··· 1886 2197 "bon", 1887 2198 "bytes", 1888 2199 "clap", 2200 + "futures", 1889 2201 "getrandom 0.2.16", 1890 2202 "http", 2203 + "image", 1891 2204 "jacquard-api 0.5.5", 1892 2205 "jacquard-common 0.5.4", 1893 2206 "jacquard-derive 0.5.4", ··· 1895 2208 "jacquard-oauth", 1896 2209 "jose-jwk", 1897 2210 "miette", 2211 + "n0-future", 1898 2212 "p256", 1899 2213 "percent-encoding", 1900 2214 "rand_core 0.6.4", ··· 1905 2219 "serde_json", 1906 2220 "smol_str", 1907 2221 "thiserror 2.0.17", 2222 + "tiff 0.6.1", 1908 2223 "tokio", 1909 2224 "tracing", 1910 2225 "trait-variant", 1911 2226 "url", 2227 + "viuer", 1912 2228 ] 1913 2229 1914 2230 [[package]] ··· 2050 2366 source = "git+https://tangled.org/@nonbinary.computer/jacquard#77915fd4920b282b4b1342749dcdad9dce30cadf" 2051 2367 dependencies = [ 2052 2368 "heck 0.5.0", 2053 - "itertools", 2369 + "itertools 0.14.0", 2054 2370 "prettyplease", 2055 2371 "proc-macro2", 2056 2372 "quote", ··· 2107 2423 "jacquard-api 0.5.5", 2108 2424 "jacquard-common 0.5.4", 2109 2425 "miette", 2426 + "n0-future", 2110 2427 "percent-encoding", 2111 2428 "reqwest", 2112 2429 "serde", ··· 2163 2480 "jose-jwa", 2164 2481 "jose-jwk", 2165 2482 "miette", 2483 + "n0-future", 2166 2484 "p256", 2167 2485 "rand 0.8.5", 2168 2486 "rand_core 0.6.4", ··· 2205 2523 checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 2206 2524 2207 2525 [[package]] 2526 + name = "jobserver" 2527 + version = "0.1.34" 2528 + source = "registry+https://github.com/rust-lang/crates.io-index" 2529 + checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 2530 + dependencies = [ 2531 + "getrandom 0.3.4", 2532 + "libc", 2533 + ] 2534 + 2535 + [[package]] 2208 2536 name = "jose-b64" 2209 2537 version = "0.1.2" 2210 2538 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2241 2569 ] 2242 2570 2243 2571 [[package]] 2572 + name = "jpeg-decoder" 2573 + version = "0.1.22" 2574 + source = "registry+https://github.com/rust-lang/crates.io-index" 2575 + checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" 2576 + 2577 + [[package]] 2244 2578 name = "js-sys" 2245 2579 version = "0.3.81" 2246 2580 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2272 2606 dependencies = [ 2273 2607 "miette", 2274 2608 "num", 2275 - "winnow", 2609 + "winnow 0.6.24", 2276 2610 ] 2277 2611 2278 2612 [[package]] ··· 2296 2630 ] 2297 2631 2298 2632 [[package]] 2633 + name = "lebe" 2634 + version = "0.5.3" 2635 + source = "registry+https://github.com/rust-lang/crates.io-index" 2636 + checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" 2637 + 2638 + [[package]] 2299 2639 name = "libc" 2300 2640 version = "0.2.176" 2301 2641 source = "registry+https://github.com/rust-lang/crates.io-index" 2302 2642 checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" 2303 2643 2304 2644 [[package]] 2645 + name = "libfuzzer-sys" 2646 + version = "0.4.10" 2647 + source = "registry+https://github.com/rust-lang/crates.io-index" 2648 + checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" 2649 + dependencies = [ 2650 + "arbitrary", 2651 + "cc", 2652 + ] 2653 + 2654 + [[package]] 2305 2655 name = "libm" 2306 2656 version = "0.2.15" 2307 2657 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2326 2676 2327 2677 [[package]] 2328 2678 name = "linux-raw-sys" 2679 + version = "0.4.15" 2680 + source = "registry+https://github.com/rust-lang/crates.io-index" 2681 + checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 2682 + 2683 + [[package]] 2684 + name = "linux-raw-sys" 2329 2685 version = "0.11.0" 2330 2686 source = "registry+https://github.com/rust-lang/crates.io-index" 2331 2687 checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" ··· 2365 2721 ] 2366 2722 2367 2723 [[package]] 2724 + name = "loop9" 2725 + version = "0.1.5" 2726 + source = "registry+https://github.com/rust-lang/crates.io-index" 2727 + checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" 2728 + dependencies = [ 2729 + "imgref", 2730 + ] 2731 + 2732 + [[package]] 2368 2733 name = "lru-cache" 2369 2734 version = "0.1.2" 2370 2735 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2380 2745 checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 2381 2746 2382 2747 [[package]] 2748 + name = "make-cmd" 2749 + version = "0.1.0" 2750 + source = "registry+https://github.com/rust-lang/crates.io-index" 2751 + checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3" 2752 + 2753 + [[package]] 2383 2754 name = "malloc_buf" 2384 2755 version = "0.0.6" 2385 2756 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2404 2775 checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 2405 2776 2406 2777 [[package]] 2778 + name = "maybe-rayon" 2779 + version = "0.1.1" 2780 + source = "registry+https://github.com/rust-lang/crates.io-index" 2781 + checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" 2782 + dependencies = [ 2783 + "cfg-if", 2784 + "rayon", 2785 + ] 2786 + 2787 + [[package]] 2407 2788 name = "memchr" 2408 2789 version = "2.7.6" 2409 2790 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2463 2844 2464 2845 [[package]] 2465 2846 name = "miniz_oxide" 2847 + version = "0.4.4" 2848 + source = "registry+https://github.com/rust-lang/crates.io-index" 2849 + checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" 2850 + dependencies = [ 2851 + "adler", 2852 + "autocfg", 2853 + ] 2854 + 2855 + [[package]] 2856 + name = "miniz_oxide" 2466 2857 version = "0.8.9" 2467 2858 source = "registry+https://github.com/rust-lang/crates.io-index" 2468 2859 checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 2469 2860 dependencies = [ 2470 2861 "adler2", 2862 + "simd-adler32", 2471 2863 ] 2472 2864 2473 2865 [[package]] ··· 2482 2874 ] 2483 2875 2484 2876 [[package]] 2877 + name = "moxcms" 2878 + version = "0.7.7" 2879 + source = "registry+https://github.com/rust-lang/crates.io-index" 2880 + checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" 2881 + dependencies = [ 2882 + "num-traits", 2883 + "pxfm", 2884 + ] 2885 + 2886 + [[package]] 2485 2887 name = "multibase" 2486 2888 version = "0.9.1" 2487 2889 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2514 2916 "log", 2515 2917 "mime", 2516 2918 "mime_guess", 2517 - "quick-error", 2919 + "quick-error 1.2.3", 2518 2920 "rand 0.8.5", 2519 2921 "safemem", 2520 2922 "tempfile", ··· 2549 2951 checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 2550 2952 2551 2953 [[package]] 2954 + name = "new_debug_unreachable" 2955 + version = "1.0.6" 2956 + source = "registry+https://github.com/rust-lang/crates.io-index" 2957 + checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 2958 + 2959 + [[package]] 2552 2960 name = "nom" 2553 2961 version = "7.1.3" 2554 2962 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2557 2965 "memchr", 2558 2966 "minimal-lexical", 2559 2967 ] 2968 + 2969 + [[package]] 2970 + name = "noop_proc_macro" 2971 + version = "0.3.0" 2972 + source = "registry+https://github.com/rust-lang/crates.io-index" 2973 + checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" 2560 2974 2561 2975 [[package]] 2562 2976 name = "nu-ansi-term" ··· 2622 3036 version = "0.1.0" 2623 3037 source = "registry+https://github.com/rust-lang/crates.io-index" 2624 3038 checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 3039 + 3040 + [[package]] 3041 + name = "num-derive" 3042 + version = "0.4.2" 3043 + source = "registry+https://github.com/rust-lang/crates.io-index" 3044 + checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 3045 + dependencies = [ 3046 + "proc-macro2", 3047 + "quote", 3048 + "syn 2.0.106", 3049 + ] 2625 3050 2626 3051 [[package]] 2627 3052 name = "num-integer" ··· 2795 3220 ] 2796 3221 2797 3222 [[package]] 3223 + name = "paste" 3224 + version = "1.0.15" 3225 + source = "registry+https://github.com/rust-lang/crates.io-index" 3226 + checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 3227 + 3228 + [[package]] 2798 3229 name = "pem-rfc7468" 2799 3230 version = "0.7.0" 2800 3231 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2860 3291 dependencies = [ 2861 3292 "der", 2862 3293 "spki", 3294 + ] 3295 + 3296 + [[package]] 3297 + name = "pkg-config" 3298 + version = "0.3.32" 3299 + source = "registry+https://github.com/rust-lang/crates.io-index" 3300 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 3301 + 3302 + [[package]] 3303 + name = "png" 3304 + version = "0.18.0" 3305 + source = "registry+https://github.com/rust-lang/crates.io-index" 3306 + checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" 3307 + dependencies = [ 3308 + "bitflags", 3309 + "crc32fast", 3310 + "fdeflate", 3311 + "flate2", 3312 + "miniz_oxide 0.8.9", 2863 3313 ] 2864 3314 2865 3315 [[package]] ··· 2994 3444 ] 2995 3445 2996 3446 [[package]] 3447 + name = "profiling" 3448 + version = "1.0.17" 3449 + source = "registry+https://github.com/rust-lang/crates.io-index" 3450 + checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" 3451 + dependencies = [ 3452 + "profiling-procmacros", 3453 + ] 3454 + 3455 + [[package]] 3456 + name = "profiling-procmacros" 3457 + version = "1.0.17" 3458 + source = "registry+https://github.com/rust-lang/crates.io-index" 3459 + checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" 3460 + dependencies = [ 3461 + "quote", 3462 + "syn 2.0.106", 3463 + ] 3464 + 3465 + [[package]] 3466 + name = "pxfm" 3467 + version = "0.1.25" 3468 + source = "registry+https://github.com/rust-lang/crates.io-index" 3469 + checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" 3470 + dependencies = [ 3471 + "num-traits", 3472 + ] 3473 + 3474 + [[package]] 3475 + name = "qoi" 3476 + version = "0.4.1" 3477 + source = "registry+https://github.com/rust-lang/crates.io-index" 3478 + checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" 3479 + dependencies = [ 3480 + "bytemuck", 3481 + ] 3482 + 3483 + [[package]] 2997 3484 name = "quick-error" 2998 3485 version = "1.2.3" 2999 3486 source = "registry+https://github.com/rust-lang/crates.io-index" 3000 3487 checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 3488 + 3489 + [[package]] 3490 + name = "quick-error" 3491 + version = "2.0.1" 3492 + source = "registry+https://github.com/rust-lang/crates.io-index" 3493 + checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 3001 3494 3002 3495 [[package]] 3003 3496 name = "quinn" ··· 3135 3628 checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" 3136 3629 3137 3630 [[package]] 3631 + name = "rav1e" 3632 + version = "0.7.1" 3633 + source = "registry+https://github.com/rust-lang/crates.io-index" 3634 + checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" 3635 + dependencies = [ 3636 + "arbitrary", 3637 + "arg_enum_proc_macro", 3638 + "arrayvec", 3639 + "av1-grain", 3640 + "bitstream-io", 3641 + "built", 3642 + "cfg-if", 3643 + "interpolate_name", 3644 + "itertools 0.12.1", 3645 + "libc", 3646 + "libfuzzer-sys", 3647 + "log", 3648 + "maybe-rayon", 3649 + "new_debug_unreachable", 3650 + "noop_proc_macro", 3651 + "num-derive", 3652 + "num-traits", 3653 + "once_cell", 3654 + "paste", 3655 + "profiling", 3656 + "rand 0.8.5", 3657 + "rand_chacha 0.3.1", 3658 + "simd_helpers", 3659 + "system-deps", 3660 + "thiserror 1.0.69", 3661 + "v_frame", 3662 + "wasm-bindgen", 3663 + ] 3664 + 3665 + [[package]] 3666 + name = "ravif" 3667 + version = "0.11.20" 3668 + source = "registry+https://github.com/rust-lang/crates.io-index" 3669 + checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" 3670 + dependencies = [ 3671 + "avif-serialize", 3672 + "imgref", 3673 + "loop9", 3674 + "quick-error 2.0.1", 3675 + "rav1e", 3676 + "rayon", 3677 + "rgb", 3678 + ] 3679 + 3680 + [[package]] 3138 3681 name = "raw-window-handle" 3139 3682 version = "0.5.2" 3140 3683 source = "registry+https://github.com/rust-lang/crates.io-index" 3141 3684 checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" 3142 3685 3143 3686 [[package]] 3687 + name = "rayon" 3688 + version = "1.11.0" 3689 + source = "registry+https://github.com/rust-lang/crates.io-index" 3690 + checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 3691 + dependencies = [ 3692 + "either", 3693 + "rayon-core", 3694 + ] 3695 + 3696 + [[package]] 3697 + name = "rayon-core" 3698 + version = "1.13.0" 3699 + source = "registry+https://github.com/rust-lang/crates.io-index" 3700 + checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 3701 + dependencies = [ 3702 + "crossbeam-deque", 3703 + "crossbeam-utils", 3704 + ] 3705 + 3706 + [[package]] 3144 3707 name = "redox_syscall" 3145 3708 version = "0.5.18" 3146 3709 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3269 3832 ] 3270 3833 3271 3834 [[package]] 3835 + name = "rgb" 3836 + version = "0.8.52" 3837 + source = "registry+https://github.com/rust-lang/crates.io-index" 3838 + checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" 3839 + dependencies = [ 3840 + "bytemuck", 3841 + ] 3842 + 3843 + [[package]] 3272 3844 name = "ring" 3273 3845 version = "0.17.14" 3274 3846 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3364 3936 3365 3937 [[package]] 3366 3938 name = "rustix" 3939 + version = "0.38.44" 3940 + source = "registry+https://github.com/rust-lang/crates.io-index" 3941 + checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 3942 + dependencies = [ 3943 + "bitflags", 3944 + "errno", 3945 + "libc", 3946 + "linux-raw-sys 0.4.15", 3947 + "windows-sys 0.59.0", 3948 + ] 3949 + 3950 + [[package]] 3951 + name = "rustix" 3367 3952 version = "1.1.2" 3368 3953 source = "registry+https://github.com/rust-lang/crates.io-index" 3369 3954 checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" ··· 3371 3956 "bitflags", 3372 3957 "errno", 3373 3958 "libc", 3374 - "linux-raw-sys", 3959 + "linux-raw-sys 0.11.0", 3375 3960 "windows-sys 0.60.2", 3376 3961 ] 3377 3962 ··· 3599 4184 ] 3600 4185 3601 4186 [[package]] 4187 + name = "serde_spanned" 4188 + version = "0.6.9" 4189 + source = "registry+https://github.com/rust-lang/crates.io-index" 4190 + checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" 4191 + dependencies = [ 4192 + "serde", 4193 + ] 4194 + 4195 + [[package]] 3602 4196 name = "serde_urlencoded" 3603 4197 version = "0.7.1" 3604 4198 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3705 4299 ] 3706 4300 3707 4301 [[package]] 4302 + name = "simd-adler32" 4303 + version = "0.3.7" 4304 + source = "registry+https://github.com/rust-lang/crates.io-index" 4305 + checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 4306 + 4307 + [[package]] 4308 + name = "simd_helpers" 4309 + version = "0.1.0" 4310 + source = "registry+https://github.com/rust-lang/crates.io-index" 4311 + checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" 4312 + dependencies = [ 4313 + "quote", 4314 + ] 4315 + 4316 + [[package]] 4317 + name = "sixel-rs" 4318 + version = "0.3.3" 4319 + source = "registry+https://github.com/rust-lang/crates.io-index" 4320 + checksum = "cfa95c014543113a192d906e5971d0c8d1e8b4cc1e61026539687a7016644ce5" 4321 + dependencies = [ 4322 + "sixel-sys", 4323 + ] 4324 + 4325 + [[package]] 4326 + name = "sixel-sys" 4327 + version = "0.3.1" 4328 + source = "registry+https://github.com/rust-lang/crates.io-index" 4329 + checksum = "fb46e0cd5569bf910390844174a5a99d52dd40681fff92228d221d9f8bf87dea" 4330 + dependencies = [ 4331 + "make-cmd", 4332 + ] 4333 + 4334 + [[package]] 3708 4335 name = "slab" 3709 4336 version = "0.4.11" 3710 4337 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3908 4535 ] 3909 4536 3910 4537 [[package]] 4538 + name = "system-deps" 4539 + version = "6.2.2" 4540 + source = "registry+https://github.com/rust-lang/crates.io-index" 4541 + checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" 4542 + dependencies = [ 4543 + "cfg-expr", 4544 + "heck 0.5.0", 4545 + "pkg-config", 4546 + "toml", 4547 + "version-compare", 4548 + ] 4549 + 4550 + [[package]] 4551 + name = "target-lexicon" 4552 + version = "0.12.16" 4553 + source = "registry+https://github.com/rust-lang/crates.io-index" 4554 + checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" 4555 + 4556 + [[package]] 3911 4557 name = "tempfile" 3912 4558 version = "3.23.0" 3913 4559 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3916 4562 "fastrand", 3917 4563 "getrandom 0.3.4", 3918 4564 "once_cell", 3919 - "rustix", 4565 + "rustix 1.1.2", 3920 4566 "windows-sys 0.60.2", 3921 4567 ] 3922 4568 3923 4569 [[package]] 4570 + name = "termcolor" 4571 + version = "1.4.1" 4572 + source = "registry+https://github.com/rust-lang/crates.io-index" 4573 + checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 4574 + dependencies = [ 4575 + "winapi-util", 4576 + ] 4577 + 4578 + [[package]] 3924 4579 name = "terminal_size" 3925 4580 version = "0.4.3" 3926 4581 source = "registry+https://github.com/rust-lang/crates.io-index" 3927 4582 checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" 3928 4583 dependencies = [ 3929 - "rustix", 4584 + "rustix 1.1.2", 3930 4585 "windows-sys 0.60.2", 3931 4586 ] 3932 4587 ··· 3999 4654 ] 4000 4655 4001 4656 [[package]] 4657 + name = "tiff" 4658 + version = "0.6.1" 4659 + source = "registry+https://github.com/rust-lang/crates.io-index" 4660 + checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" 4661 + dependencies = [ 4662 + "jpeg-decoder", 4663 + "miniz_oxide 0.4.4", 4664 + "weezl", 4665 + ] 4666 + 4667 + [[package]] 4668 + name = "tiff" 4669 + version = "0.10.3" 4670 + source = "registry+https://github.com/rust-lang/crates.io-index" 4671 + checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" 4672 + dependencies = [ 4673 + "fax", 4674 + "flate2", 4675 + "half", 4676 + "quick-error 2.0.1", 4677 + "weezl", 4678 + "zune-jpeg", 4679 + ] 4680 + 4681 + [[package]] 4002 4682 name = "time" 4003 4683 version = "0.3.44" 4004 4684 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4154 4834 ] 4155 4835 4156 4836 [[package]] 4837 + name = "toml" 4838 + version = "0.8.23" 4839 + source = "registry+https://github.com/rust-lang/crates.io-index" 4840 + checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" 4841 + dependencies = [ 4842 + "serde", 4843 + "serde_spanned", 4844 + "toml_datetime", 4845 + "toml_edit", 4846 + ] 4847 + 4848 + [[package]] 4849 + name = "toml_datetime" 4850 + version = "0.6.11" 4851 + source = "registry+https://github.com/rust-lang/crates.io-index" 4852 + checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 4853 + dependencies = [ 4854 + "serde", 4855 + ] 4856 + 4857 + [[package]] 4858 + name = "toml_edit" 4859 + version = "0.22.27" 4860 + source = "registry+https://github.com/rust-lang/crates.io-index" 4861 + checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 4862 + dependencies = [ 4863 + "indexmap 2.11.4", 4864 + "serde", 4865 + "serde_spanned", 4866 + "toml_datetime", 4867 + "winnow 0.7.13", 4868 + ] 4869 + 4870 + [[package]] 4157 4871 name = "tower" 4158 4872 version = "0.5.2" 4159 4873 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4438 5152 ] 4439 5153 4440 5154 [[package]] 5155 + name = "v_frame" 5156 + version = "0.3.9" 5157 + source = "registry+https://github.com/rust-lang/crates.io-index" 5158 + checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" 5159 + dependencies = [ 5160 + "aligned-vec", 5161 + "num-traits", 5162 + "wasm-bindgen", 5163 + ] 5164 + 5165 + [[package]] 4441 5166 name = "valuable" 4442 5167 version = "0.1.1" 4443 5168 source = "registry+https://github.com/rust-lang/crates.io-index" 4444 5169 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 5170 + 5171 + [[package]] 5172 + name = "version-compare" 5173 + version = "0.2.0" 5174 + source = "registry+https://github.com/rust-lang/crates.io-index" 5175 + checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" 4445 5176 4446 5177 [[package]] 4447 5178 name = "version_check" 4448 5179 version = "0.9.5" 4449 5180 source = "registry+https://github.com/rust-lang/crates.io-index" 4450 5181 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 5182 + 5183 + [[package]] 5184 + name = "viuer" 5185 + version = "0.9.2" 5186 + source = "registry+https://github.com/rust-lang/crates.io-index" 5187 + checksum = "0ae7c6870b98c838123f22cac9a594cbe2d74ea48d79271c08f8c9e680b40fac" 5188 + dependencies = [ 5189 + "ansi_colours", 5190 + "base64 0.22.1", 5191 + "console", 5192 + "crossterm", 5193 + "image", 5194 + "lazy_static", 5195 + "sixel-rs", 5196 + "tempfile", 5197 + "termcolor", 5198 + ] 4451 5199 4452 5200 [[package]] 4453 5201 name = "walkdir" ··· 4615 5363 ] 4616 5364 4617 5365 [[package]] 5366 + name = "weezl" 5367 + version = "0.1.10" 5368 + source = "registry+https://github.com/rust-lang/crates.io-index" 5369 + checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" 5370 + 5371 + [[package]] 4618 5372 name = "widestring" 4619 5373 version = "1.2.0" 4620 5374 source = "registry+https://github.com/rust-lang/crates.io-index" 4621 5375 checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" 4622 5376 4623 5377 [[package]] 5378 + name = "winapi" 5379 + version = "0.3.9" 5380 + source = "registry+https://github.com/rust-lang/crates.io-index" 5381 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 5382 + dependencies = [ 5383 + "winapi-i686-pc-windows-gnu", 5384 + "winapi-x86_64-pc-windows-gnu", 5385 + ] 5386 + 5387 + [[package]] 5388 + name = "winapi-i686-pc-windows-gnu" 5389 + version = "0.4.0" 5390 + source = "registry+https://github.com/rust-lang/crates.io-index" 5391 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 5392 + 5393 + [[package]] 4624 5394 name = "winapi-util" 4625 5395 version = "0.1.11" 4626 5396 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4628 5398 dependencies = [ 4629 5399 "windows-sys 0.60.2", 4630 5400 ] 5401 + 5402 + [[package]] 5403 + name = "winapi-x86_64-pc-windows-gnu" 5404 + version = "0.4.0" 5405 + source = "registry+https://github.com/rust-lang/crates.io-index" 5406 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 4631 5407 4632 5408 [[package]] 4633 5409 name = "windows" ··· 5086 5862 ] 5087 5863 5088 5864 [[package]] 5865 + name = "winnow" 5866 + version = "0.7.13" 5867 + source = "registry+https://github.com/rust-lang/crates.io-index" 5868 + checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 5869 + dependencies = [ 5870 + "memchr", 5871 + ] 5872 + 5873 + [[package]] 5089 5874 name = "winreg" 5090 5875 version = "0.50.0" 5091 5876 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5219 6004 "quote", 5220 6005 "syn 2.0.106", 5221 6006 ] 6007 + 6008 + [[package]] 6009 + name = "zune-core" 6010 + version = "0.4.12" 6011 + source = "registry+https://github.com/rust-lang/crates.io-index" 6012 + checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" 6013 + 6014 + [[package]] 6015 + name = "zune-inflate" 6016 + version = "0.2.54" 6017 + source = "registry+https://github.com/rust-lang/crates.io-index" 6018 + checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" 6019 + dependencies = [ 6020 + "simd-adler32", 6021 + ] 6022 + 6023 + [[package]] 6024 + name = "zune-jpeg" 6025 + version = "0.4.21" 6026 + source = "registry+https://github.com/rust-lang/crates.io-index" 6027 + checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" 6028 + dependencies = [ 6029 + "zune-core", 6030 + ]
+1
Cargo.toml
··· 61 61 62 62 # Async and runtimes 63 63 tokio = { version = "1", default-features = false } 64 + n0-future = "0.1" 64 65 65 66 # Observability 66 67 tracing = "0.1"
+1 -1
crates/jacquard-common/Cargo.toml
··· 42 42 tokio = { workspace = true, default-features = false, features = ["sync"] } 43 43 44 44 # Streaming support (optional) 45 - n0-future = { version = "0.1", optional = true } 45 + n0-future = { workspace = true, optional = true } 46 46 futures = { version = "0.3", optional = true } 47 47 tokio-tungstenite-wasm = { version = "0.4", optional = true } 48 48 genawaiter = { version = "0.99.1", features = ["futures03"] }
+44 -9
crates/jacquard-common/src/stream.rs
··· 49 49 pub type BoxError = Box<dyn Error + Send + Sync + 'static>; 50 50 51 51 /// Error type for streaming operations 52 - #[derive(Debug)] 52 + #[derive(Debug, thiserror::Error, miette::Diagnostic)] 53 53 pub struct StreamError { 54 54 kind: StreamErrorKind, 55 + #[source] 55 56 source: Option<BoxError>, 56 57 } 57 58 ··· 156 157 } 157 158 } 158 159 159 - impl Error for StreamError { 160 - fn source(&self) -> Option<&(dyn Error + 'static)> { 161 - self.source 162 - .as_ref() 163 - .map(|e| e.as_ref() as &(dyn Error + 'static)) 164 - } 165 - } 166 - 167 160 use bytes::Bytes; 168 161 use n0_future::stream::Boxed; 169 162 ··· 203 196 /// Convert into the inner boxed stream 204 197 pub fn into_inner(self) -> Boxed<Result<Bytes, StreamError>> { 205 198 self.inner 199 + } 200 + 201 + /// Split this stream into two streams that both receive all chunks 202 + /// 203 + /// Chunks are cloned (cheaply via Bytes rc). Spawns a forwarder task. 204 + /// Both returned streams will receive all chunks from the original stream. 205 + /// The forwarder continues as long as at least one stream is alive. 206 + /// If the underlying stream errors, both teed streams will end. 207 + pub fn tee(self) -> (ByteStream, ByteStream) { 208 + use futures::channel::mpsc; 209 + use n0_future::StreamExt as _; 210 + 211 + let (tx1, rx1) = mpsc::unbounded(); 212 + let (tx2, rx2) = mpsc::unbounded(); 213 + 214 + n0_future::task::spawn(async move { 215 + let mut stream = self.inner; 216 + while let Some(result) = stream.next().await { 217 + match result { 218 + Ok(chunk) => { 219 + // Clone chunk (cheap - Bytes is rc'd) 220 + let chunk2 = chunk.clone(); 221 + 222 + // Send to both channels, continue if at least one succeeds 223 + let send1 = tx1.unbounded_send(Ok(chunk)); 224 + let send2 = tx2.unbounded_send(Ok(chunk2)); 225 + 226 + // Only stop if both channels are closed 227 + if send1.is_err() && send2.is_err() { 228 + break; 229 + } 230 + } 231 + Err(_e) => { 232 + // Underlying stream errored, stop forwarding. 233 + // Both channels will close, ending both streams. 234 + break; 235 + } 236 + } 237 + } 238 + }); 239 + 240 + (ByteStream::new(rx1), ByteStream::new(rx2)) 206 241 } 207 242 } 208 243
+5 -2
crates/jacquard-common/src/types/blob.rs
··· 1 - use crate::{CowStr, IntoStatic, types::cid::Cid}; 1 + use crate::{ 2 + CowStr, IntoStatic, 3 + types::cid::{Cid, CidLink}, 4 + }; 2 5 #[allow(unused)] 3 6 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; 4 7 use smol_str::ToSmolStr; ··· 24 27 #[serde(rename_all = "camelCase")] 25 28 pub struct Blob<'b> { 26 29 /// CID (Content Identifier) reference to the blob data 27 - pub r#ref: Cid<'b>, 30 + pub r#ref: CidLink<'b>, 28 31 /// MIME type of the blob (e.g., "image/png", "video/mp4") 29 32 #[serde(borrow)] 30 33 pub mime_type: MimeType<'b>,
+2 -1
crates/jacquard-common/src/types/value/convert.rs
··· 1 1 use crate::IntoStatic; 2 + use crate::types::cid::CidLink; 2 3 use crate::types::{ 3 4 DataModelType, 4 5 cid::Cid, ··· 298 299 } 299 300 }; 300 301 return Ok(Data::Blob(crate::types::blob::Blob { 301 - r#ref: cid.clone(), 302 + r#ref: CidLink::str(cid).into_static(), 302 303 mime_type: crate::types::blob::MimeType::from(mime.clone()), 303 304 size: size_val, 304 305 }));
+4 -4
crates/jacquard-common/src/types/value/parsing.rs
··· 251 251 }); 252 252 if let (Some(mime_type), Some(size)) = (mime_type, size) { 253 253 return Some(Blob { 254 - r#ref: Cid::ipld(*value), 254 + r#ref: CidLink::ipld(*value), 255 255 mime_type: MimeType::raw(mime_type), 256 256 size: size as usize, 257 257 }); ··· 259 259 } else if let Some(Ipld::String(value)) = blob.get("cid") { 260 260 if let Some(mime_type) = mime_type { 261 261 return Some(Blob { 262 - r#ref: Cid::str(value), 262 + r#ref: CidLink::str(value), 263 263 mime_type: MimeType::raw(mime_type), 264 264 size: 0, 265 265 }); ··· 281 281 let size = blob.get("size").and_then(|v| v.as_u64()); 282 282 if let (Some(mime_type), Some(size)) = (mime_type, size) { 283 283 return Some(Blob { 284 - r#ref: Cid::str(value), 284 + r#ref: CidLink::str(value), 285 285 mime_type: MimeType::raw(mime_type), 286 286 size: size as usize, 287 287 }); ··· 290 290 } else if let Some(value) = blob.get("cid").and_then(|v| v.as_str()) { 291 291 if let Some(mime_type) = mime_type { 292 292 return Some(Blob { 293 - r#ref: Cid::str(value), 293 + r#ref: CidLink::str(value), 294 294 mime_type: MimeType::raw(mime_type), 295 295 size: 0, 296 296 });
+2 -2
crates/jacquard-common/src/types/value/serde_impl.rs
··· 320 320 321 321 if let (Some(ref_cid), Some(mime_cowstr), Some(size)) = (ref_cid, mime_type, size) { 322 322 return Ok(Data::Blob(Blob { 323 - r#ref: ref_cid, 323 + r#ref: CidLink::str(ref_cid.as_str()).into_static(), 324 324 mime_type: MimeType::from(mime_cowstr), 325 325 size, 326 326 })); ··· 749 749 750 750 if let (Some(ref_cid), Some(mime_cowstr), Some(size)) = (ref_cid, mime_type, size) { 751 751 return Ok(RawData::Blob(Blob { 752 - r#ref: ref_cid, 752 + r#ref: CidLink::str(ref_cid.as_str()).into_static(), 753 753 mime_type: MimeType::from(mime_cowstr), 754 754 size, 755 755 }));
+53 -7
crates/jacquard-common/src/xrpc.rs
··· 15 15 16 16 use ipld_core::ipld::Ipld; 17 17 #[cfg(feature = "streaming")] 18 - pub use streaming::StreamingResponse; 18 + pub use streaming::{ 19 + StreamingResponse, XrpcProcedureSend, XrpcProcedureStream, XrpcResponseStream, XrpcStreamResp, 20 + }; 19 21 20 22 #[cfg(feature = "websocket")] 21 23 pub mod subscription; ··· 44 46 use crate::{CowStr, error::XrpcResult}; 45 47 use crate::{IntoStatic, error::DecodeError}; 46 48 #[cfg(feature = "streaming")] 47 - use crate::{ 48 - StreamError, 49 - xrpc::streaming::{XrpcProcedureSend, XrpcProcedureStream, XrpcResponseStream, XrpcStreamResp}, 50 - }; 49 + use crate::StreamError; 51 50 use crate::{error::TransportError, types::value::RawData}; 52 51 53 52 /// Error type for encoding XRPC requests ··· 272 271 #[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] 273 272 pub trait XrpcClient: HttpClient { 274 273 /// Get the base URI for the client. 275 - fn base_uri(&self) -> Url; 274 + fn base_uri(&self) -> impl Future<Output = Url>; 276 275 277 276 /// Get the call options for the client. 278 277 fn opts(&self) -> impl Future<Output = CallOptions<'_>> { ··· 316 315 where 317 316 R: XrpcRequest + Send + Sync, 318 317 <R as XrpcRequest>::Response: Send + Sync; 318 + 319 + } 320 + 321 + /// Stateful XRPC streaming client trait 322 + #[cfg(feature = "streaming")] 323 + pub trait XrpcStreamingClient: XrpcClient + HttpClientExt { 324 + /// Send an XRPC request and stream the response 325 + #[cfg(not(target_arch = "wasm32"))] 326 + fn download<R>( 327 + &self, 328 + request: R, 329 + ) -> impl Future<Output = Result<StreamingResponse, StreamError>> + Send 330 + where 331 + R: XrpcRequest + Send + Sync, 332 + <R as XrpcRequest>::Response: Send + Sync, 333 + Self: Sync; 334 + 335 + /// Send an XRPC request and stream the response 336 + #[cfg(target_arch = "wasm32")] 337 + fn download<R>( 338 + &self, 339 + request: R, 340 + ) -> impl Future<Output = Result<StreamingResponse, StreamError>> 341 + where 342 + R: XrpcRequest + Send + Sync, 343 + <R as XrpcRequest>::Response: Send + Sync; 344 + 345 + /// Stream an XRPC procedure call and its response 346 + #[cfg(not(target_arch = "wasm32"))] 347 + fn stream<S>( 348 + &self, 349 + stream: XrpcProcedureSend<S::Frame<'static>>, 350 + ) -> impl Future<Output = Result<XrpcResponseStream<<<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>>, StreamError>> 351 + where 352 + S: XrpcProcedureStream + 'static, 353 + <<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>: XrpcStreamResp, 354 + Self: Sync; 355 + 356 + /// Stream an XRPC procedure call and its response 357 + #[cfg(target_arch = "wasm32")] 358 + fn stream<S>( 359 + &self, 360 + stream: XrpcProcedureSend<S::Frame<'static>>, 361 + ) -> impl Future<Output = Result<XrpcResponseStream<<<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>>, StreamError>> 362 + where 363 + S: XrpcProcedureStream + 'static, 364 + <<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>: XrpcStreamResp; 319 365 } 320 366 321 367 /// Stateless XRPC call builder. ··· 947 993 /// Stream an XRPC procedure call and its response 948 994 /// 949 995 /// Useful for streaming upload of large payloads, or for "pipe-through" operations 950 - /// where you processing a large payload. 996 + /// where you are processing a large payload. 951 997 pub async fn stream<S>( 952 998 self, 953 999 stream: XrpcProcedureSend<S::Frame<'static>>,
+1 -1
crates/jacquard-common/src/xrpc/streaming.rs
··· 208 208 } 209 209 } 210 210 211 - /// XRPC streaming response 211 + /// HTTP streaming response 212 212 /// 213 213 /// Similar to `Response<R>` but holds a streaming body instead of a buffer. 214 214 pub struct StreamingResponse {
+3 -3
crates/jacquard-common/src/xrpc/subscription.rs
··· 472 472 #[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] 473 473 pub trait SubscriptionClient: WebSocketClient { 474 474 /// Get the base URI for the client. 475 - fn base_uri(&self) -> Url; 475 + fn base_uri(&self) -> impl Future<Output = Url>; 476 476 477 477 /// Get the subscription options for the client. 478 478 fn subscription_opts(&self) -> impl Future<Output = SubscriptionOptions<'_>> { ··· 570 570 } 571 571 572 572 impl<W: WebSocketClient> SubscriptionClient for BasicSubscriptionClient<W> { 573 - fn base_uri(&self) -> Url { 573 + async fn base_uri(&self) -> Url { 574 574 self.base_uri.clone() 575 575 } 576 576 ··· 613 613 Sub: XrpcSubscription + Send + Sync, 614 614 Self: Sync, 615 615 { 616 - let base = self.base_uri(); 616 + let base = self.base_uri().await; 617 617 self.subscription(base) 618 618 .with_options(opts) 619 619 .subscribe(params)
+2 -1
crates/jacquard-identity/Cargo.toml
··· 15 15 [features] 16 16 dns = ["dep:hickory-resolver"] 17 17 tracing = ["dep:tracing"] 18 + streaming = ["jacquard-common/streaming", "dep:n0-future"] 18 19 19 20 [dependencies] 20 21 trait-variant.workspace = true ··· 33 34 serde_html_form.workspace = true 34 35 urlencoding.workspace = true 35 36 tracing = { workspace = true, optional = true } 36 - 37 + n0-future = { workspace = true, optional = true } 37 38 38 39 [target.'cfg(not(target_family = "wasm"))'.dependencies] 39 40 hickory-resolver = { optional = true, version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"]}
+31 -1
crates/jacquard-identity/src/lib.rs
··· 77 77 use bytes::Bytes; 78 78 use jacquard_api::com_atproto::identity::resolve_did; 79 79 use jacquard_api::com_atproto::identity::resolve_handle::ResolveHandle; 80 + #[cfg(feature = "streaming")] 81 + use jacquard_common::ByteStream; 80 82 use jacquard_common::error::TransportError; 81 83 use jacquard_common::http_client::HttpClient; 82 84 use jacquard_common::types::did::Did; ··· 89 91 use url::{ParseError, Url}; 90 92 91 93 #[cfg(all(feature = "dns", not(target_family = "wasm")))] 92 - use {hickory_resolver::{TokioAsyncResolver, config::ResolverConfig}, std::sync::Arc}; 94 + use { 95 + hickory_resolver::{TokioAsyncResolver, config::ResolverConfig}, 96 + std::sync::Arc, 97 + }; 93 98 94 99 /// Default resolver implementation with configurable fallback order. 95 100 #[derive(Clone)] ··· 499 504 } 500 505 501 506 type Error = reqwest::Error; 507 + } 508 + 509 + #[cfg(feature = "streaming")] 510 + impl jacquard_common::http_client::HttpClientExt for JacquardResolver { 511 + /// Send HTTP request and return streaming response 512 + fn send_http_streaming( 513 + &self, 514 + request: http::Request<Vec<u8>>, 515 + ) -> impl Future<Output = Result<http::Response<ByteStream>, Self::Error>> { 516 + self.http.send_http_streaming(request) 517 + } 518 + 519 + /// Send HTTP request with streaming body and receive streaming response 520 + fn send_http_bidirectional<S>( 521 + &self, 522 + parts: http::request::Parts, 523 + body: S, 524 + ) -> impl Future<Output = Result<http::Response<ByteStream>, Self::Error>> 525 + where 526 + S: n0_future::Stream<Item = Result<bytes::Bytes, jacquard_common::StreamError>> 527 + + Send 528 + + 'static, 529 + { 530 + self.http.send_http_bidirectional(parts, body) 531 + } 502 532 } 503 533 504 534 /// Warnings produced during identity checks that are not fatal
+2
crates/jacquard-oauth/Cargo.toml
··· 37 37 tokio = { workspace = true, default-features = false, features = ["sync"] } 38 38 reqwest.workspace = true 39 39 trait-variant.workspace = true 40 + n0-future = { workspace = true, optional = true } 40 41 webbrowser = { version = "0.8", optional = true } 41 42 tracing = { workspace = true, optional = true } 42 43 ··· 50 51 browser-open = ["dep:webbrowser"] 51 52 tracing = ["dep:tracing"] 52 53 websocket = ["jacquard-common/websocket"] 54 + streaming = ["jacquard-common/streaming", "dep:n0-future"]
+196 -16
crates/jacquard-oauth/src/client.rs
··· 29 29 resolver::{DidDocResponse, IdentityError, IdentityResolver, ResolverOptions}, 30 30 }; 31 31 use jose_jwk::JwkSet; 32 - use std::sync::Arc; 32 + use std::{future::Future, sync::Arc}; 33 33 use tokio::sync::RwLock; 34 34 use url::Url; 35 35 ··· 458 458 T: OAuthResolver + DpopExt + XrpcExt + Send + Sync + 'static, 459 459 W: Send + Sync, 460 460 { 461 - fn base_uri(&self) -> Url { 462 - // base_uri is a synchronous trait method; we must avoid async `.read().await`. 463 - // Use `block_in_place` under Tokio runtime to perform a blocking RwLock read safely. 464 - #[cfg(not(target_arch = "wasm32"))] 465 - if tokio::runtime::Handle::try_current().is_ok() { 466 - return tokio::task::block_in_place(|| self.data.blocking_read().host_url.clone()); 467 - } 468 - 469 - self.data.blocking_read().host_url.clone() 461 + async fn base_uri(&self) -> Url { 462 + self.data.read().await.host_url.clone() 470 463 } 471 464 472 465 async fn opts(&self) -> CallOptions<'_> { ··· 491 484 R: XrpcRequest + Send + Sync, 492 485 <R as XrpcRequest>::Response: Send + Sync, 493 486 { 494 - let base_uri = self.base_uri(); 487 + let base_uri = self.base_uri().await; 495 488 opts.auth = Some(self.access_token().await); 496 489 let guard = self.data.read().await; 497 490 let mut dpop = guard.dpop_data.clone(); ··· 524 517 } 525 518 } 526 519 520 + #[cfg(feature = "streaming")] 521 + impl<T, S, W> jacquard_common::http_client::HttpClientExt for OAuthSession<T, S, W> 522 + where 523 + S: ClientAuthStore + Send + Sync + 'static, 524 + T: OAuthResolver 525 + + DpopExt 526 + + XrpcExt 527 + + jacquard_common::http_client::HttpClientExt 528 + + Send 529 + + Sync 530 + + 'static, 531 + W: Send + Sync, 532 + { 533 + async fn send_http_streaming( 534 + &self, 535 + request: http::Request<Vec<u8>>, 536 + ) -> core::result::Result<http::Response<jacquard_common::stream::ByteStream>, Self::Error> 537 + { 538 + self.client.send_http_streaming(request).await 539 + } 540 + 541 + async fn send_http_bidirectional<Str>( 542 + &self, 543 + parts: http::request::Parts, 544 + body: Str, 545 + ) -> core::result::Result<http::Response<jacquard_common::stream::ByteStream>, Self::Error> 546 + where 547 + Str: n0_future::Stream< 548 + Item = core::result::Result<bytes::Bytes, jacquard_common::StreamError>, 549 + > + Send 550 + + 'static, 551 + { 552 + self.client.send_http_bidirectional(parts, body).await 553 + } 554 + } 555 + 556 + #[cfg(feature = "streaming")] 557 + impl<T, S, W> jacquard_common::xrpc::XrpcStreamingClient for OAuthSession<T, S, W> 558 + where 559 + S: ClientAuthStore + Send + Sync + 'static, 560 + T: OAuthResolver 561 + + DpopExt 562 + + XrpcExt 563 + + jacquard_common::http_client::HttpClientExt 564 + + Send 565 + + Sync 566 + + 'static, 567 + W: Send + Sync, 568 + { 569 + async fn download<R>( 570 + &self, 571 + request: R, 572 + ) -> core::result::Result<jacquard_common::xrpc::StreamingResponse, jacquard_common::StreamError> 573 + where 574 + R: XrpcRequest + Send + Sync, 575 + <R as XrpcRequest>::Response: Send + Sync, 576 + { 577 + use jacquard_common::StreamError; 578 + 579 + let base_uri = <Self as XrpcClient>::base_uri(self).await; 580 + let mut opts = self.options.read().await.clone(); 581 + opts.auth = Some(self.access_token().await); 582 + let http_request = build_http_request(&base_uri, &request, &opts) 583 + .map_err(|e| StreamError::protocol(e.to_string()))?; 584 + let guard = self.data.read().await; 585 + let mut dpop = guard.dpop_data.clone(); 586 + let result = self 587 + .client 588 + .dpop_call(&mut dpop) 589 + .send_streaming(http_request) 590 + .await; 591 + drop(guard); 592 + 593 + match result { 594 + Ok(response) => Ok(response), 595 + Err(_e) => { 596 + // Check if it's an auth error and retry 597 + opts.auth = Some( 598 + self.refresh() 599 + .await 600 + .map_err(|e| StreamError::transport(e))?, 601 + ); 602 + let http_request = build_http_request(&base_uri, &request, &opts) 603 + .map_err(|e| StreamError::protocol(e.to_string()))?; 604 + let guard = self.data.read().await; 605 + let mut dpop = guard.dpop_data.clone(); 606 + self.client 607 + .dpop_call(&mut dpop) 608 + .send_streaming(http_request) 609 + .await 610 + .map_err(StreamError::transport) 611 + } 612 + } 613 + } 614 + 615 + async fn stream<Str>( 616 + &self, 617 + stream: jacquard_common::xrpc::streaming::XrpcProcedureSend<Str::Frame<'static>>, 618 + ) -> core::result::Result< 619 + jacquard_common::xrpc::streaming::XrpcResponseStream< 620 + <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>, 621 + >, 622 + jacquard_common::StreamError, 623 + > 624 + where 625 + Str: jacquard_common::xrpc::streaming::XrpcProcedureStream + 'static, 626 + <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::streaming::XrpcStreamResp, 627 + { 628 + use jacquard_common::StreamError; 629 + use n0_future::{StreamExt, TryStreamExt}; 630 + 631 + let base_uri = self.base_uri().await; 632 + let mut opts = self.options.read().await.clone(); 633 + opts.auth = Some(self.access_token().await); 634 + 635 + let mut url = base_uri; 636 + let mut path = url.path().trim_end_matches('/').to_owned(); 637 + path.push_str("/xrpc/"); 638 + path.push_str(<Str::Request as jacquard_common::xrpc::XrpcRequest>::NSID); 639 + url.set_path(&path); 640 + 641 + let mut builder = http::Request::post(url.to_string()); 642 + 643 + if let Some(token) = &opts.auth { 644 + use jacquard_common::AuthorizationToken; 645 + let hv = match token { 646 + AuthorizationToken::Bearer(t) => { 647 + http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) 648 + } 649 + AuthorizationToken::Dpop(t) => { 650 + http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref())) 651 + } 652 + } 653 + .map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?; 654 + builder = builder.header(http::header::AUTHORIZATION, hv); 655 + } 656 + 657 + if let Some(proxy) = &opts.atproto_proxy { 658 + builder = builder.header("atproto-proxy", proxy.as_ref()); 659 + } 660 + if let Some(labelers) = &opts.atproto_accept_labelers { 661 + if !labelers.is_empty() { 662 + let joined = labelers 663 + .iter() 664 + .map(|s| s.as_ref()) 665 + .collect::<Vec<_>>() 666 + .join(", "); 667 + builder = builder.header("atproto-accept-labelers", joined); 668 + } 669 + } 670 + for (name, value) in &opts.extra_headers { 671 + builder = builder.header(name, value); 672 + } 673 + 674 + let (parts, _) = builder 675 + .body(()) 676 + .map_err(|e| StreamError::protocol(e.to_string()))? 677 + .into_parts(); 678 + 679 + let body_stream = 680 + jacquard_common::stream::ByteStream::new(stream.0.map_ok(|f| f.buffer).boxed()); 681 + 682 + let guard = self.data.read().await; 683 + let mut dpop = guard.dpop_data.clone(); 684 + let result = self 685 + .client 686 + .dpop_call(&mut dpop) 687 + .send_bidirectional(parts, body_stream) 688 + .await; 689 + drop(guard); 690 + 691 + match result { 692 + Ok(response) => { 693 + let (resp_parts, resp_body) = response.into_parts(); 694 + Ok( 695 + jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts( 696 + resp_parts, resp_body, 697 + ), 698 + ) 699 + } 700 + Err(e) => { 701 + // OAuth token refresh and retry is handled by dpop wrapper 702 + // If we get here, it's a real error 703 + Err(StreamError::transport(e)) 704 + } 705 + } 706 + } 707 + } 708 + 527 709 fn is_invalid_token_response<R: XrpcResp>(response: &XrpcResult<Response<R>>) -> bool { 528 710 match response { 529 711 Err(ClientError::Auth(AuthError::InvalidToken)) => true, ··· 592 774 T: OAuthResolver + Send + Sync + 'static, 593 775 W: WebSocketClient + Send + Sync, 594 776 { 595 - fn base_uri(&self) -> Url { 777 + async fn base_uri(&self) -> Url { 596 778 #[cfg(not(target_arch = "wasm32"))] 597 779 if tokio::runtime::Handle::try_current().is_ok() { 598 780 return tokio::task::block_in_place(|| self.data.blocking_read().host_url.clone()); ··· 608 790 AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_ref()), 609 791 AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_ref()), 610 792 }; 611 - opts.headers.push(( 612 - CowStr::from("Authorization"), 613 - CowStr::from(auth_value), 614 - )); 793 + opts.headers 794 + .push((CowStr::from("Authorization"), CowStr::from(auth_value))); 615 795 opts 616 796 } 617 797
+227 -24
crates/jacquard-oauth/src/dpop.rs
··· 109 109 ) 110 110 .await 111 111 } 112 + 113 + #[cfg(feature = "streaming")] 114 + pub async fn send_streaming( 115 + self, 116 + request: Request<Vec<u8>>, 117 + ) -> Result<jacquard_common::xrpc::StreamingResponse> 118 + where 119 + C: jacquard_common::http_client::HttpClientExt, 120 + { 121 + wrap_request_with_dpop_streaming( 122 + self.client, 123 + self.data_source, 124 + self.is_to_auth_server, 125 + request, 126 + ) 127 + .await 128 + } 129 + 130 + #[cfg(feature = "streaming")] 131 + pub async fn send_bidirectional( 132 + self, 133 + parts: http::request::Parts, 134 + body: jacquard_common::stream::ByteStream, 135 + ) -> Result<jacquard_common::xrpc::StreamingResponse> 136 + where 137 + C: jacquard_common::http_client::HttpClientExt, 138 + { 139 + wrap_request_with_dpop_bidirectional( 140 + self.client, 141 + self.data_source, 142 + self.is_to_auth_server, 143 + parts, 144 + body, 145 + ) 146 + .await 147 + } 148 + } 149 + 150 + /// Extract authorization hash from request headers 151 + fn extract_ath(headers: &http::HeaderMap) -> Option<CowStr<'static>> { 152 + headers 153 + .get("Authorization") 154 + .filter(|v| v.to_str().is_ok_and(|s| s.starts_with("DPoP "))) 155 + .map(|auth| { 156 + URL_SAFE_NO_PAD 157 + .encode(sha2::Sha256::digest(&auth.as_bytes()[5..])) 158 + .into() 159 + }) 160 + } 161 + 162 + /// Get nonce from data source based on target 163 + fn get_nonce<N: DpopDataSource>(data_source: &N, is_to_auth_server: bool) -> Option<CowStr<'_>> { 164 + if is_to_auth_server { 165 + data_source.authserver_nonce() 166 + } else { 167 + data_source.host_nonce() 168 + } 169 + } 170 + 171 + /// Store nonce in data source based on target 172 + fn store_nonce<N: DpopDataSource>( 173 + data_source: &mut N, 174 + is_to_auth_server: bool, 175 + nonce: CowStr<'static>, 176 + ) { 177 + if is_to_auth_server { 178 + data_source.set_authserver_nonce(nonce); 179 + } else { 180 + data_source.set_host_nonce(nonce); 181 + } 112 182 } 113 183 114 184 pub async fn wrap_request_with_dpop<T, N>( ··· 124 194 let uri = request.uri().clone(); 125 195 let method = request.method().to_cowstr().into_static(); 126 196 let uri = uri.to_cowstr(); 127 - // https://datatracker.ietf.org/doc/html/rfc9449#section-4.2 128 - let ath = request 129 - .headers() 130 - .get("Authorization") 131 - .filter(|v| v.to_str().is_ok_and(|s| s.starts_with("DPoP "))) 132 - .map(|auth| { 133 - URL_SAFE_NO_PAD 134 - .encode(sha2::Sha256::digest(&auth.as_bytes()[5..])) 135 - .into() 136 - }); 197 + let ath = extract_ath(request.headers()); 137 198 138 - let init_nonce = if is_to_auth_server { 139 - data_source.authserver_nonce() 140 - } else { 141 - data_source.host_nonce() 142 - }; 199 + let init_nonce = get_nonce(data_source, is_to_auth_server); 143 200 let init_proof = build_dpop_proof( 144 201 data_source.key(), 145 202 method.clone(), ··· 157 214 .headers() 158 215 .get("DPoP-Nonce") 159 216 .and_then(|v| v.to_str().ok()) 160 - .map(|c| c.to_cowstr()); 217 + .map(|c| CowStr::from(c.to_string())); 161 218 match &next_nonce { 162 219 Some(s) if next_nonce != init_nonce => { 163 - // Store the fresh nonce for future requests 164 - if is_to_auth_server { 165 - data_source.set_authserver_nonce(s.clone()); 166 - } else { 167 - data_source.set_host_nonce(s.clone()); 168 - } 220 + store_nonce(data_source, is_to_auth_server, s.clone()); 169 221 } 170 222 _ => { 171 - // No nonce was returned or it is the same as the one we sent. No need to 172 - // update the nonce store, or retry the request. 173 223 return Ok(response); 174 224 } 175 225 } ··· 184 234 .await 185 235 .map_err(|e| Error::Inner(e.into()))?; 186 236 Ok(response) 237 + } 238 + 239 + #[cfg(feature = "streaming")] 240 + pub async fn wrap_request_with_dpop_streaming<T, N>( 241 + client: &T, 242 + data_source: &mut N, 243 + is_to_auth_server: bool, 244 + mut request: Request<Vec<u8>>, 245 + ) -> Result<jacquard_common::xrpc::StreamingResponse> 246 + where 247 + T: jacquard_common::http_client::HttpClientExt, 248 + N: DpopDataSource, 249 + { 250 + use jacquard_common::xrpc::StreamingResponse; 251 + 252 + let uri = request.uri().clone(); 253 + let method = request.method().to_cowstr().into_static(); 254 + let uri = uri.to_cowstr(); 255 + let ath = extract_ath(request.headers()); 256 + 257 + let init_nonce = get_nonce(data_source, is_to_auth_server); 258 + let init_proof = build_dpop_proof( 259 + data_source.key(), 260 + method.clone(), 261 + uri.clone(), 262 + init_nonce.clone(), 263 + ath.clone(), 264 + )?; 265 + request.headers_mut().insert("DPoP", init_proof.parse()?); 266 + let http_response = client 267 + .send_http_streaming(request.clone()) 268 + .await 269 + .map_err(|e| Error::Inner(e.into()))?; 270 + 271 + let (parts, body) = http_response.into_parts(); 272 + let next_nonce = parts 273 + .headers 274 + .get("DPoP-Nonce") 275 + .and_then(|v| v.to_str().ok()) 276 + .map(|c| CowStr::from(c.to_string())); 277 + match &next_nonce { 278 + Some(s) if next_nonce != init_nonce => { 279 + store_nonce(data_source, is_to_auth_server, s.clone()); 280 + } 281 + _ => { 282 + return Ok(StreamingResponse::new(parts, body)); 283 + } 284 + } 285 + 286 + // For streaming responses, we can't easily check the body for use_dpop_nonce error 287 + // We check status code + headers only 288 + if !is_use_dpop_nonce_error_streaming(is_to_auth_server, parts.status, &parts.headers) { 289 + return Ok(StreamingResponse::new(parts, body)); 290 + } 291 + 292 + let next_proof = build_dpop_proof(data_source.key(), method, uri, next_nonce, ath)?; 293 + request.headers_mut().insert("DPoP", next_proof.parse()?); 294 + let http_response = client 295 + .send_http_streaming(request) 296 + .await 297 + .map_err(|e| Error::Inner(e.into()))?; 298 + let (parts, body) = http_response.into_parts(); 299 + Ok(StreamingResponse::new(parts, body)) 300 + } 301 + 302 + #[cfg(feature = "streaming")] 303 + pub async fn wrap_request_with_dpop_bidirectional<T, N>( 304 + client: &T, 305 + data_source: &mut N, 306 + is_to_auth_server: bool, 307 + mut parts: http::request::Parts, 308 + body: jacquard_common::stream::ByteStream, 309 + ) -> Result<jacquard_common::xrpc::StreamingResponse> 310 + where 311 + T: jacquard_common::http_client::HttpClientExt, 312 + N: DpopDataSource, 313 + { 314 + use jacquard_common::xrpc::StreamingResponse; 315 + 316 + let uri = parts.uri.clone(); 317 + let method = parts.method.to_cowstr().into_static(); 318 + let uri = uri.to_cowstr(); 319 + let ath = extract_ath(&parts.headers); 320 + 321 + let init_nonce = get_nonce(data_source, is_to_auth_server); 322 + let init_proof = build_dpop_proof( 323 + data_source.key(), 324 + method.clone(), 325 + uri.clone(), 326 + init_nonce.clone(), 327 + ath.clone(), 328 + )?; 329 + parts.headers.insert("DPoP", init_proof.parse()?); 330 + 331 + // Clone the stream for potential retry 332 + let (body1, body2) = body.tee(); 333 + 334 + let http_response = client 335 + .send_http_bidirectional(parts.clone(), body1.into_inner()) 336 + .await 337 + .map_err(|e| Error::Inner(e.into()))?; 338 + 339 + let (resp_parts, resp_body) = http_response.into_parts(); 340 + let next_nonce = resp_parts 341 + .headers 342 + .get("DPoP-Nonce") 343 + .and_then(|v| v.to_str().ok()) 344 + .map(|c| CowStr::from(c.to_string())); 345 + match &next_nonce { 346 + Some(s) if next_nonce != init_nonce => { 347 + store_nonce(data_source, is_to_auth_server, s.clone()); 348 + } 349 + _ => { 350 + return Ok(StreamingResponse::new(resp_parts, resp_body)); 351 + } 352 + } 353 + 354 + // For streaming responses, we can't easily check the body for use_dpop_nonce error 355 + // We check status code + headers only 356 + if !is_use_dpop_nonce_error_streaming(is_to_auth_server, resp_parts.status, &resp_parts.headers) 357 + { 358 + return Ok(StreamingResponse::new(resp_parts, resp_body)); 359 + } 360 + 361 + let next_proof = build_dpop_proof(data_source.key(), method, uri, next_nonce, ath)?; 362 + parts.headers.insert("DPoP", next_proof.parse()?); 363 + let http_response = client 364 + .send_http_bidirectional(parts, body2.into_inner()) 365 + .await 366 + .map_err(|e| Error::Inner(e.into()))?; 367 + let (parts, body) = http_response.into_parts(); 368 + Ok(StreamingResponse::new(parts, body)) 369 + } 370 + 371 + #[cfg(feature = "streaming")] 372 + fn is_use_dpop_nonce_error_streaming( 373 + is_to_auth_server: bool, 374 + status: http::StatusCode, 375 + headers: &http::HeaderMap, 376 + ) -> bool { 377 + if is_to_auth_server && status == 400 { 378 + // Can't check body for streaming, so we rely on DPoP-Nonce header presence 379 + return false; 380 + } 381 + if !is_to_auth_server && status == 401 { 382 + if let Some(www_auth) = headers 383 + .get("WWW-Authenticate") 384 + .and_then(|v| v.to_str().ok()) 385 + { 386 + return www_auth.starts_with("DPoP") && www_auth.contains(r#"error="use_dpop_nonce""#); 387 + } 388 + } 389 + false 187 390 } 188 391 189 392 #[inline]
+18 -2
crates/jacquard/Cargo.toml
··· 12 12 license.workspace = true 13 13 14 14 [features] 15 - default = ["api_full", "dns", "loopback", "derive"] 15 + default = ["api_full", "dns", "loopback", "derive", "streaming"] 16 16 derive = ["dep:jacquard-derive"] 17 17 # Minimal API bindings 18 18 api = ["jacquard-api/minimal"] ··· 38 38 "jacquard-identity/tracing", 39 39 ] 40 40 dns = ["jacquard-identity/dns"] 41 - streaming = ["jacquard-common/streaming"] 41 + streaming = [ 42 + "jacquard-common/streaming", 43 + "jacquard-oauth/streaming", 44 + "jacquard-identity/streaming", 45 + "dep:n0-future", 46 + "dep:futures" 47 + ] 42 48 websocket = ["jacquard-common/websocket"] 43 49 44 50 [[example]] ··· 74 80 path = "../../examples/read_tangled_repo.rs" 75 81 76 82 [[example]] 83 + name = "stream_get_blob" 84 + path = "../../examples/stream_get_blob.rs" 85 + required-features = ["api_bluesky", "streaming"] 86 + 87 + [[example]] 77 88 name = "resolve_did" 78 89 path = "../../examples/resolve_did.rs" 79 90 ··· 127 138 p256 = { workspace = true, features = ["ecdsa"] } 128 139 rand_core.workspace = true 129 140 tracing = { workspace = true, optional = true } 141 + n0-future = { workspace = true, optional = true } 142 + futures = { version = "0.3", optional = true } 130 143 131 144 [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 132 145 reqwest = { workspace = true, features = [ ··· 142 155 [dev-dependencies] 143 156 clap.workspace = true 144 157 miette = { workspace = true, features = ["fancy"] } 158 + viuer = { version = "0.9", features = ["print-file", "sixel"] } 159 + tiff = { version = "0.6.0-alpha" } 160 + image = { version = "0.25" } 145 161 146 162 [package.metadata.docs.rs] 147 163 features = ["api_all", "derive", "dns", "loopback"]
+151 -2
crates/jacquard/src/client.rs
··· 789 789 })?, 790 790 )); 791 791 let response = self.send_with_opts(request, opts).await?; 792 + let debug: serde_json::Value = serde_json::from_slice(response.buffer()).unwrap(); 793 + println!("json: {}", serde_json::to_string_pretty(&debug).unwrap()); 792 794 let output = response.into_output().map_err(|e| match e { 793 795 XrpcError::Auth(auth) => AgentError::Auth(auth), 794 796 XrpcError::Generic(g) => AgentError::Generic(g), ··· 912 914 } 913 915 } 914 916 917 + #[cfg(feature = "streaming")] 918 + impl<A> jacquard_common::http_client::HttpClientExt for Agent<A> 919 + where 920 + A: AgentSession + jacquard_common::http_client::HttpClientExt, 921 + { 922 + #[cfg(not(target_arch = "wasm32"))] 923 + fn send_http_streaming( 924 + &self, 925 + request: http::Request<Vec<u8>>, 926 + ) -> impl Future< 927 + Output = core::result::Result< 928 + http::Response<jacquard_common::stream::ByteStream>, 929 + Self::Error, 930 + >, 931 + > + Send { 932 + self.inner.send_http_streaming(request) 933 + } 934 + 935 + #[cfg(target_arch = "wasm32")] 936 + fn send_http_streaming( 937 + &self, 938 + request: http::Request<Vec<u8>>, 939 + ) -> impl Future< 940 + Output = core::result::Result< 941 + http::Response<jacquard_common::stream::ByteStream>, 942 + Self::Error, 943 + >, 944 + > { 945 + self.inner.send_http_streaming(request) 946 + } 947 + 948 + #[cfg(not(target_arch = "wasm32"))] 949 + fn send_http_bidirectional<Str>( 950 + &self, 951 + parts: http::request::Parts, 952 + body: Str, 953 + ) -> impl Future< 954 + Output = core::result::Result< 955 + http::Response<jacquard_common::stream::ByteStream>, 956 + Self::Error, 957 + >, 958 + > + Send 959 + where 960 + Str: n0_future::Stream< 961 + Item = core::result::Result<bytes::Bytes, jacquard_common::StreamError>, 962 + > + Send 963 + + 'static, 964 + { 965 + self.inner.send_http_bidirectional(parts, body) 966 + } 967 + 968 + #[cfg(target_arch = "wasm32")] 969 + fn send_http_bidirectional<Str>( 970 + &self, 971 + parts: http::request::Parts, 972 + body: Str, 973 + ) -> impl Future< 974 + Output = core::result::Result< 975 + http::Response<jacquard_common::stream::ByteStream>, 976 + Self::Error, 977 + >, 978 + > 979 + where 980 + Str: n0_future::Stream< 981 + Item = core::result::Result<bytes::Bytes, jacquard_common::StreamError>, 982 + > + 'static, 983 + { 984 + self.inner.send_http_bidirectional(parts, body) 985 + } 986 + } 987 + 915 988 impl<A: AgentSession> XrpcClient for Agent<A> { 916 - fn base_uri(&self) -> url::Url { 917 - self.inner.base_uri() 989 + async fn base_uri(&self) -> url::Url { 990 + self.inner.base_uri().await 918 991 } 919 992 fn opts(&self) -> impl Future<Output = CallOptions<'_>> { 920 993 self.inner.opts() ··· 940 1013 <R as XrpcRequest>::Response: Send + Sync, 941 1014 { 942 1015 self.inner.send_with_opts(request, opts).await 1016 + } 1017 + } 1018 + 1019 + #[cfg(feature = "streaming")] 1020 + impl<A> jacquard_common::xrpc::XrpcStreamingClient for Agent<A> 1021 + where 1022 + A: AgentSession + jacquard_common::xrpc::XrpcStreamingClient, 1023 + { 1024 + #[cfg(not(target_arch = "wasm32"))] 1025 + fn download<R>( 1026 + &self, 1027 + request: R, 1028 + ) -> impl Future< 1029 + Output = core::result::Result< 1030 + jacquard_common::xrpc::StreamingResponse, 1031 + jacquard_common::StreamError, 1032 + >, 1033 + > + Send 1034 + where 1035 + R: XrpcRequest + Send + Sync, 1036 + <R as XrpcRequest>::Response: Send + Sync, 1037 + Self: Sync, 1038 + { 1039 + self.inner.download(request) 1040 + } 1041 + 1042 + #[cfg(target_arch = "wasm32")] 1043 + fn download<R>( 1044 + &self, 1045 + request: R, 1046 + ) -> impl Future< 1047 + Output = core::result::Result< 1048 + jacquard_common::xrpc::StreamingResponse, 1049 + jacquard_common::StreamError, 1050 + >, 1051 + > 1052 + where 1053 + R: XrpcRequest + Send + Sync, 1054 + <R as XrpcRequest>::Response: Send + Sync, 1055 + { 1056 + self.inner.download(request) 1057 + } 1058 + 1059 + #[cfg(not(target_arch = "wasm32"))] 1060 + fn stream<S>( 1061 + &self, 1062 + stream: jacquard_common::xrpc::XrpcProcedureSend<S::Frame<'static>>, 1063 + ) -> impl Future< 1064 + Output = core::result::Result< 1065 + jacquard_common::xrpc::XrpcResponseStream<<<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>>, 1066 + jacquard_common::StreamError, 1067 + >, 1068 + > 1069 + where 1070 + S: jacquard_common::xrpc::XrpcProcedureStream + 'static, 1071 + <<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::XrpcStreamResp, 1072 + Self: Sync, 1073 + { 1074 + self.inner.stream::<S>(stream) 1075 + } 1076 + 1077 + #[cfg(target_arch = "wasm32")] 1078 + fn stream<S>( 1079 + &self, 1080 + stream: jacquard_common::xrpc::XrpcProcedureSend<S::Frame<'static>>, 1081 + ) -> impl Future< 1082 + Output = core::result::Result< 1083 + jacquard_common::xrpc::XrpcResponseStream<<<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>>, 1084 + jacquard_common::StreamError, 1085 + >, 1086 + > 1087 + where 1088 + S: jacquard_common::xrpc::XrpcProcedureStream + 'static, 1089 + <<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::XrpcStreamResp, 1090 + { 1091 + self.inner.stream::<S>(stream) 943 1092 } 944 1093 } 945 1094
+219 -28
crates/jacquard/src/client/credential_session.rs
··· 433 433 T: HttpClient + XrpcExt + Send + Sync + 'static, 434 434 W: Send + Sync, 435 435 { 436 - fn base_uri(&self) -> Url { 437 - // base_uri is a synchronous trait method; avoid `.await` here. 438 - // Under Tokio, use `block_in_place` to make a blocking RwLock read safe. 439 - #[cfg(not(target_arch = "wasm32"))] 440 - if tokio::runtime::Handle::try_current().is_ok() { 441 - tokio::task::block_in_place(|| { 442 - self.endpoint.blocking_read().clone().unwrap_or( 443 - Url::parse("https://public.bsky.app") 444 - .expect("public appview should be valid url"), 445 - ) 446 - }) 447 - } else { 448 - self.endpoint.blocking_read().clone().unwrap_or( 449 - Url::parse("https://public.bsky.app").expect("public appview should be valid url"), 450 - ) 451 - } 452 - 453 - #[cfg(target_arch = "wasm32")] 454 - { 455 - self.endpoint.blocking_read().clone().unwrap_or( 456 - Url::parse("https://public.bsky.app").expect("public appview should be valid url"), 457 - ) 458 - } 436 + async fn base_uri(&self) -> Url { 437 + self.endpoint.read().await.clone().unwrap_or( 438 + Url::parse("https://public.bsky.app").expect("public appview should be valid url"), 439 + ) 459 440 } 460 441 461 442 async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>> ··· 476 457 R: XrpcRequest + Send + Sync, 477 458 <R as XrpcRequest>::Response: Send + Sync, 478 459 { 479 - let base_uri = self.base_uri(); 460 + let base_uri = self.base_uri().await; 480 461 let auth = self.access_token().await; 481 462 opts.auth = auth; 482 463 let resp = self ··· 512 493 } 513 494 } 514 495 496 + #[cfg(feature = "streaming")] 497 + impl<S, T, W> jacquard_common::http_client::HttpClientExt for CredentialSession<S, T, W> 498 + where 499 + S: SessionStore<SessionKey, AtpSession> + Send + Sync + 'static, 500 + T: HttpClient + XrpcExt + jacquard_common::http_client::HttpClientExt + Send + Sync + 'static, 501 + W: Send + Sync, 502 + { 503 + async fn send_http_streaming( 504 + &self, 505 + request: http::Request<Vec<u8>>, 506 + ) -> core::result::Result<http::Response<jacquard_common::stream::ByteStream>, Self::Error> { 507 + self.client.send_http_streaming(request).await 508 + } 509 + 510 + async fn send_http_bidirectional<Str>( 511 + &self, 512 + parts: http::request::Parts, 513 + body: Str, 514 + ) -> core::result::Result<http::Response<jacquard_common::stream::ByteStream>, Self::Error> 515 + where 516 + Str: n0_future::Stream<Item = core::result::Result<bytes::Bytes, jacquard_common::StreamError>> 517 + + Send 518 + + 'static, 519 + { 520 + self.client.send_http_bidirectional(parts, body).await 521 + } 522 + } 523 + 524 + #[cfg(feature = "streaming")] 525 + impl<S, T, W> jacquard_common::xrpc::XrpcStreamingClient for CredentialSession<S, T, W> 526 + where 527 + S: SessionStore<SessionKey, AtpSession> + Send + Sync + 'static, 528 + T: HttpClient + XrpcExt + jacquard_common::http_client::HttpClientExt + Send + Sync + 'static, 529 + W: Send + Sync, 530 + { 531 + async fn download<R>( 532 + &self, 533 + request: R, 534 + ) -> core::result::Result<jacquard_common::xrpc::StreamingResponse, jacquard_common::StreamError> 535 + where 536 + R: XrpcRequest + Send + Sync, 537 + <R as XrpcRequest>::Response: Send + Sync, 538 + { 539 + use jacquard_common::{StreamError, xrpc::build_http_request}; 540 + 541 + let base_uri = <Self as XrpcClient>::base_uri(self).await; 542 + let mut opts = self.options.read().await.clone(); 543 + opts.auth = self.access_token().await; 544 + 545 + let http_request = build_http_request(&base_uri, &request, &opts) 546 + .map_err(|e| StreamError::protocol(e.to_string()))?; 547 + 548 + let response = self 549 + .client 550 + .send_http_streaming(http_request.clone()) 551 + .await 552 + .map_err(StreamError::transport)?; 553 + 554 + let (parts, body) = response.into_parts(); 555 + let status = parts.status; 556 + 557 + // Check if expired based on status code 558 + if status == http::StatusCode::UNAUTHORIZED || status == http::StatusCode::BAD_REQUEST { 559 + // Try to refresh 560 + let auth = self.refresh().await.map_err(StreamError::transport)?; 561 + opts.auth = Some(auth); 562 + 563 + let http_request = build_http_request(&base_uri, &request, &opts) 564 + .map_err(|e| StreamError::protocol(e.to_string()))?; 565 + 566 + let response = self 567 + .client 568 + .send_http_streaming(http_request) 569 + .await 570 + .map_err(StreamError::transport)?; 571 + let (parts, body) = response.into_parts(); 572 + Ok(jacquard_common::xrpc::StreamingResponse::new(parts, body)) 573 + } else { 574 + Ok(jacquard_common::xrpc::StreamingResponse::new(parts, body)) 575 + } 576 + } 577 + 578 + async fn stream<Str>( 579 + &self, 580 + stream: jacquard_common::xrpc::streaming::XrpcProcedureSend<Str::Frame<'static>>, 581 + ) -> core::result::Result< 582 + jacquard_common::xrpc::streaming::XrpcResponseStream< 583 + <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>, 584 + >, 585 + jacquard_common::StreamError, 586 + > 587 + where 588 + Str: jacquard_common::xrpc::streaming::XrpcProcedureStream + 'static, 589 + <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::streaming::XrpcStreamResp, 590 + { 591 + use jacquard_common::StreamError; 592 + use n0_future::{StreamExt, TryStreamExt}; 593 + 594 + let base_uri = self.base_uri().await; 595 + let mut opts = self.options.read().await.clone(); 596 + opts.auth = self.access_token().await; 597 + 598 + let mut url = base_uri; 599 + let mut path = url.path().trim_end_matches('/').to_owned(); 600 + path.push_str("/xrpc/"); 601 + path.push_str(<Str::Request as jacquard_common::xrpc::XrpcRequest>::NSID); 602 + url.set_path(&path); 603 + 604 + let mut builder = http::Request::post(url.to_string()); 605 + 606 + if let Some(token) = &opts.auth { 607 + use jacquard_common::AuthorizationToken; 608 + let hv = match token { 609 + AuthorizationToken::Bearer(t) => { 610 + http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) 611 + } 612 + AuthorizationToken::Dpop(t) => { 613 + http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref())) 614 + } 615 + } 616 + .map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?; 617 + builder = builder.header(http::header::AUTHORIZATION, hv); 618 + } 619 + 620 + if let Some(proxy) = &opts.atproto_proxy { 621 + builder = builder.header("atproto-proxy", proxy.as_ref()); 622 + } 623 + if let Some(labelers) = &opts.atproto_accept_labelers { 624 + if !labelers.is_empty() { 625 + let joined = labelers 626 + .iter() 627 + .map(|s| s.as_ref()) 628 + .collect::<Vec<_>>() 629 + .join(", "); 630 + builder = builder.header("atproto-accept-labelers", joined); 631 + } 632 + } 633 + for (name, value) in &opts.extra_headers { 634 + builder = builder.header(name, value); 635 + } 636 + 637 + let (parts, _) = builder 638 + .body(()) 639 + .map_err(|e| StreamError::protocol(e.to_string()))? 640 + .into_parts(); 641 + 642 + let body_stream = 643 + jacquard_common::stream::ByteStream::new(stream.0.map_ok(|f| f.buffer).boxed()); 644 + 645 + let response = self 646 + .client 647 + .send_http_bidirectional(parts.clone(), body_stream.into_inner()) 648 + .await 649 + .map_err(StreamError::transport)?; 650 + 651 + let (resp_parts, resp_body) = response.into_parts(); 652 + let status = resp_parts.status; 653 + 654 + // Check if expired 655 + if status == http::StatusCode::UNAUTHORIZED || status == http::StatusCode::BAD_REQUEST { 656 + // Try to refresh 657 + let auth = self.refresh().await.map_err(StreamError::transport)?; 658 + opts.auth = Some(auth); 659 + 660 + // Rebuild request with new auth 661 + let mut builder = http::Request::post(url.to_string()); 662 + if let Some(token) = &opts.auth { 663 + use jacquard_common::AuthorizationToken; 664 + let hv = match token { 665 + AuthorizationToken::Bearer(t) => { 666 + http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) 667 + } 668 + AuthorizationToken::Dpop(t) => { 669 + http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref())) 670 + } 671 + } 672 + .map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?; 673 + builder = builder.header(http::header::AUTHORIZATION, hv); 674 + } 675 + if let Some(proxy) = &opts.atproto_proxy { 676 + builder = builder.header("atproto-proxy", proxy.as_ref()); 677 + } 678 + if let Some(labelers) = &opts.atproto_accept_labelers { 679 + if !labelers.is_empty() { 680 + let joined = labelers 681 + .iter() 682 + .map(|s| s.as_ref()) 683 + .collect::<Vec<_>>() 684 + .join(", "); 685 + builder = builder.header("atproto-accept-labelers", joined); 686 + } 687 + } 688 + for (name, value) in &opts.extra_headers { 689 + builder = builder.header(name, value); 690 + } 691 + 692 + let (parts, _) = builder 693 + .body(()) 694 + .map_err(|e| StreamError::protocol(e.to_string()))? 695 + .into_parts(); 696 + 697 + // Can't retry with the same stream - it's been consumed 698 + // This is a limitation of streaming upload with auth refresh 699 + return Err(StreamError::protocol("Authentication failed on streaming upload and stream cannot be retried".to_string())); 700 + } 701 + 702 + Ok(jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts( 703 + resp_parts, resp_body, 704 + )) 705 + } 706 + } 707 + 515 708 impl<S, T, W> IdentityResolver for CredentialSession<S, T, W> 516 709 where 517 710 S: SessionStore<SessionKey, AtpSession> + Send + Sync + 'static, ··· 596 789 AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_ref()), 597 790 AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_ref()), 598 791 }; 599 - opts.headers.push(( 600 - CowStr::from("Authorization"), 601 - CowStr::from(auth_value), 602 - )); 792 + opts.headers 793 + .push((CowStr::from("Authorization"), CowStr::from(auth_value))); 603 794 } 604 795 opts 605 796 }
+3
crates/jacquard/src/lib.rs
··· 219 219 220 220 pub mod client; 221 221 222 + #[cfg(feature = "streaming")] 223 + pub mod streaming; 224 + 222 225 pub use common::*; 223 226 #[cfg(feature = "api")] 224 227 pub use jacquard_api as api;
+3
crates/jacquard/src/streaming.rs
··· 1 + pub mod blob; 2 + pub mod repo; 3 + pub mod video;
+61
crates/jacquard/src/streaming/blob.rs
··· 1 + //! Streaming support for blob uploads 2 + 3 + use bytes::Bytes; 4 + use jacquard_api::com_atproto::repo::upload_blob::{UploadBlob, UploadBlobOutput}; 5 + use jacquard_common::{ 6 + StreamError, 7 + xrpc::streaming::{XrpcProcedureStream, XrpcStreamResp}, 8 + }; 9 + use serde::{Deserialize, Serialize}; 10 + 11 + /// Streaming implementation for com.atproto.repo.uploadBlob 12 + pub struct UploadBlobStream; 13 + 14 + impl XrpcProcedureStream for UploadBlobStream { 15 + const NSID: &'static str = "com.atproto.repo.uploadBlob"; 16 + const ENCODING: &'static str = "*/*"; 17 + 18 + type Frame<'de> = Bytes; 19 + type Request = UploadBlob; 20 + type Response = UploadBlobStreamResponse; 21 + 22 + fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 23 + where 24 + Self::Frame<'de>: Serialize, 25 + { 26 + Ok(data) 27 + } 28 + 29 + fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 30 + where 31 + Self::Frame<'de>: Deserialize<'de>, 32 + { 33 + Ok(Bytes::copy_from_slice(frame)) 34 + } 35 + } 36 + 37 + /// Response marker for streaming uploadBlob 38 + pub struct UploadBlobStreamResponse; 39 + 40 + impl XrpcStreamResp for UploadBlobStreamResponse { 41 + const NSID: &'static str = "com.atproto.repo.uploadBlob"; 42 + const ENCODING: &'static str = "application/json"; 43 + 44 + type Frame<'de> = UploadBlobOutput<'de>; 45 + 46 + fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 47 + where 48 + Self::Frame<'de>: Serialize, 49 + { 50 + Ok(Bytes::from_owner( 51 + serde_json::to_vec(&data).map_err(StreamError::encode)?, 52 + )) 53 + } 54 + 55 + fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 56 + where 57 + Self::Frame<'de>: Deserialize<'de>, 58 + { 59 + Ok(serde_json::from_slice(frame).map_err(StreamError::decode)?) 60 + } 61 + }
+83
crates/jacquard/src/streaming/repo.rs
··· 1 + //! Streaming support for repository operations 2 + 3 + use bytes::Bytes; 4 + use jacquard_api::com_atproto::repo::import_repo::ImportRepo; 5 + use jacquard_common::{ 6 + xrpc::streaming::{XrpcProcedureStream, XrpcStreamResp}, 7 + StreamError, 8 + }; 9 + use serde::{Deserialize, Serialize}; 10 + 11 + /// Streaming implementation for com.atproto.repo.importRepo 12 + pub struct ImportRepoStream; 13 + 14 + impl XrpcProcedureStream for ImportRepoStream { 15 + const NSID: &'static str = "com.atproto.repo.importRepo"; 16 + const ENCODING: &'static str = "application/vnd.ipld.car"; 17 + 18 + type Frame<'de> = Bytes; 19 + type Request = ImportRepo; 20 + type Response = ImportRepoStreamResponse; 21 + 22 + fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 23 + where 24 + Self::Frame<'de>: Serialize, 25 + { 26 + Ok(data) 27 + } 28 + 29 + fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 30 + where 31 + Self::Frame<'de>: Deserialize<'de>, 32 + { 33 + Ok(Bytes::copy_from_slice(frame)) 34 + } 35 + } 36 + 37 + /// Response marker for streaming importRepo 38 + pub struct ImportRepoStreamResponse; 39 + 40 + impl XrpcStreamResp for ImportRepoStreamResponse { 41 + const NSID: &'static str = "com.atproto.repo.importRepo"; 42 + const ENCODING: &'static str = "application/json"; 43 + 44 + type Frame<'de> = (); 45 + 46 + fn encode_frame<'de>(_data: Self::Frame<'de>) -> Result<Bytes, StreamError> 47 + where 48 + Self::Frame<'de>: Serialize, 49 + { 50 + Ok(Bytes::new()) 51 + } 52 + 53 + fn decode_frame<'de>(_frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 54 + where 55 + Self::Frame<'de>: Deserialize<'de>, 56 + { 57 + Ok(()) 58 + } 59 + } 60 + 61 + /// Streaming implementation for com.atproto.sync.getRepo 62 + pub struct GetRepoStream; 63 + 64 + impl XrpcStreamResp for GetRepoStream { 65 + const NSID: &'static str = "com.atproto.sync.getRepo"; 66 + const ENCODING: &'static str = "application/vnd.ipld.car"; 67 + 68 + type Frame<'de> = Bytes; 69 + 70 + fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 71 + where 72 + Self::Frame<'de>: Serialize, 73 + { 74 + Ok(data) 75 + } 76 + 77 + fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 78 + where 79 + Self::Frame<'de>: Deserialize<'de>, 80 + { 81 + Ok(Bytes::copy_from_slice(frame)) 82 + } 83 + }
+61
crates/jacquard/src/streaming/video.rs
··· 1 + //! Streaming support for video uploads 2 + 3 + use bytes::Bytes; 4 + use jacquard_api::app_bsky::video::upload_video::{UploadVideo, UploadVideoOutput}; 5 + use jacquard_common::{ 6 + xrpc::streaming::{XrpcProcedureStream, XrpcStreamResp}, 7 + StreamError, 8 + }; 9 + use serde::{Deserialize, Serialize}; 10 + 11 + /// Streaming implementation for app.bsky.video.uploadVideo 12 + pub struct UploadVideoStream; 13 + 14 + impl XrpcProcedureStream for UploadVideoStream { 15 + const NSID: &'static str = "app.bsky.video.uploadVideo"; 16 + const ENCODING: &'static str = "video/mp4"; 17 + 18 + type Frame<'de> = Bytes; 19 + type Request = UploadVideo; 20 + type Response = UploadVideoStreamResponse; 21 + 22 + fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 23 + where 24 + Self::Frame<'de>: Serialize, 25 + { 26 + Ok(data) 27 + } 28 + 29 + fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 30 + where 31 + Self::Frame<'de>: Deserialize<'de>, 32 + { 33 + Ok(Bytes::copy_from_slice(frame)) 34 + } 35 + } 36 + 37 + /// Response marker for streaming uploadVideo 38 + pub struct UploadVideoStreamResponse; 39 + 40 + impl XrpcStreamResp for UploadVideoStreamResponse { 41 + const NSID: &'static str = "app.bsky.video.uploadVideo"; 42 + const ENCODING: &'static str = "application/json"; 43 + 44 + type Frame<'de> = UploadVideoOutput<'de>; 45 + 46 + fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 47 + where 48 + Self::Frame<'de>: Serialize, 49 + { 50 + Ok(Bytes::from_owner( 51 + serde_json::to_vec(&data).map_err(StreamError::encode)?, 52 + )) 53 + } 54 + 55 + fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 56 + where 57 + Self::Frame<'de>: Deserialize<'de>, 58 + { 59 + Ok(serde_json::from_slice(frame).map_err(StreamError::decode)?) 60 + } 61 + }
+61
examples/stream_get_blob.rs
··· 1 + use clap::Parser; 2 + use jacquard::StreamingResponse; 3 + use jacquard::api::com_atproto::sync::get_blob::GetBlob; 4 + use jacquard::client::Agent; 5 + use jacquard::types::cid::Cid; 6 + use jacquard::types::did::Did; 7 + use jacquard::xrpc::XrpcStreamingClient; 8 + use jacquard_oauth::authstore::MemoryAuthStore; 9 + use jacquard_oauth::client::OAuthClient; 10 + use jacquard_oauth::loopback::LoopbackConfig; 11 + use n0_future::StreamExt; 12 + 13 + #[derive(Parser, Debug)] 14 + #[command( 15 + author, 16 + version, 17 + about = "Download a blob from a PDS and stream the response, then display it, if it's an image" 18 + )] 19 + struct Args { 20 + input: String, 21 + #[arg(short, long)] 22 + did: String, 23 + #[arg(short, long)] 24 + cid: String, 25 + } 26 + 27 + #[tokio::main] 28 + async fn main() -> miette::Result<()> { 29 + let args = Args::parse(); 30 + 31 + let oauth = OAuthClient::with_default_config(MemoryAuthStore::new()); 32 + let session = oauth 33 + .login_with_local_server(args.input, Default::default(), LoopbackConfig::default()) 34 + .await?; 35 + 36 + let agent: Agent<_> = Agent::from(session); 37 + // Use the streaming `.download()` method with the generated API parameter struct 38 + let output: StreamingResponse = agent 39 + .download(GetBlob { 40 + did: Did::new_owned(args.did)?, 41 + cid: Cid::str(&args.cid), 42 + }) 43 + .await?; 44 + 45 + let (parts, body_stream) = output.into_parts(); 46 + 47 + println!("Parts: {:?}", parts); 48 + 49 + let mut buf: Vec<u8> = Vec::new(); 50 + let mut stream = body_stream.into_inner(); 51 + 52 + while let Some(Ok(chunk)) = stream.as_mut().next().await { 53 + buf.append(&mut chunk.to_vec()); 54 + } 55 + 56 + if let Ok(img) = image::load_from_memory(&buf) { 57 + viuer::print(&img, &viuer::Config::default()).expect("Image printing failed."); 58 + } 59 + 60 + Ok(()) 61 + }