this repo has no description

confirmed fixed the blob bug. http download stream works.

Orual d45b5d1b 9714fd1f

Changed files
+2051 -112
crates
examples
+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 + }