actix-multipart-0.7.2/.cargo_vcs_info.json0000644000000001550000000000100141720ustar { "git": { "sha1": "b01fbddba484a52309d3fd50dafd862e29684453" }, "path_in_vcs": "actix-multipart" }actix-multipart-0.7.2/CHANGES.md000064400000000000000000000075371046102023000143670ustar 00000000000000# Changes ## Unreleased ## 0.7.2 - Fix re-exported version of `actix-multipart-derive`. ## 0.7.1 - Expose `LimitExceeded` error type. ## 0.7.0 - Add `MultipartError::ContentTypeIncompatible` variant. - Add `MultipartError::ContentDispositionNameMissing` variant. - Add `Field::bytes()` method. - Rename `MultipartError::{NoContentDisposition => ContentDispositionMissing}` variant. - Rename `MultipartError::{NoContentType => ContentTypeMissing}` variant. - Rename `MultipartError::{ParseContentType => ContentTypeParse}` variant. - Rename `MultipartError::{Boundary => BoundaryMissing}` variant. - Rename `MultipartError::{UnsupportedField => UnknownField}` variant. - Remove top-level re-exports of `test` utilities. ## 0.6.2 - Add testing utilities under new module `test`. - Minimum supported Rust version (MSRV) is now 1.72. ## 0.6.1 - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. ## 0.6.0 - Added `MultipartForm` typed data extractor. [#2883] [#2883]: https://github.com/actix/actix-web/pull/2883 ## 0.5.0 - `Field::content_type()` now returns `Option<&mime::Mime>`. [#2885] - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. [#2885]: https://github.com/actix/actix-web/pull/2885 ## 0.4.0 - No significant changes since `0.4.0-beta.13`. ## 0.4.0-beta.13 - No significant changes since `0.4.0-beta.12`. ## 0.4.0-beta.12 - Minimum supported Rust version (MSRV) is now 1.54. ## 0.4.0-beta.11 - No significant changes since `0.4.0-beta.10`. ## 0.4.0-beta.10 - No significant changes since `0.4.0-beta.9`. ## 0.4.0-beta.9 - Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463] [#2463]: https://github.com/actix/actix-web/pull/2463 ## 0.4.0-beta.8 - Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] - Added `MultipartError::NoContentDisposition` variant. [#2451] - Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451] - Added `Field::name` method for getting the field name. [#2451] - `MultipartError` now marks variants with inner errors as the source. [#2451] - `MultipartError` is now marked as non-exhaustive. [#2451] [#2451]: https://github.com/actix/actix-web/pull/2451 ## 0.4.0-beta.7 - Minimum supported Rust version (MSRV) is now 1.52. ## 0.4.0-beta.6 - Minimum supported Rust version (MSRV) is now 1.51. ## 0.4.0-beta.5 - No notable changes. ## 0.4.0-beta.4 - No notable changes. ## 0.4.0-beta.3 - No notable changes. ## 0.4.0-beta.2 - No notable changes. ## 0.4.0-beta.1 - Fix multipart consuming payload before header checks. [#1513] - Update `bytes` to `1.0`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813 [#1513]: https://github.com/actix/actix-web/pull/1513 ## 0.3.0 - No significant changes from `0.3.0-beta.2`. ## 0.3.0-beta.2 - Update `actix-*` dependencies to latest versions. ## 0.3.0-beta.1 - Update `actix-web` to 3.0.0-beta.1 ## 0.3.0-alpha.1 - Update `actix-web` to 3.0.0-alpha.3 - Bump minimum supported Rust version to 1.40 - Minimize `futures` dependencies - Remove the unused `time` dependency - Fix missing `std::error::Error` implement for `MultipartError`. ## 0.2.0 - Release ## 0.2.0-alpha.4 - Multipart handling now handles Pending during read of boundary #1205 ## 0.2.0-alpha.2 - Migrate to `std::future` ## 0.1.4 - Multipart handling now parses requests which do not end in CRLF #1038 ## 0.1.3 - Fix ring dependency from actix-web default features for #741. ## 0.1.2 - Fix boundary parsing #876 ## 0.1.1 - Fix disconnect handling #834 ## 0.1.0 - Release ## 0.1.0-beta.4 - Handle cancellation of uploads #736 - Upgrade to actix-web 1.0.0-beta.4 ## 0.1.0-beta.1 - Do not support nested multipart - Split multipart support to separate crate - Optimize multipart handling #634, #769 actix-multipart-0.7.2/Cargo.lock0000644000001324170000000000100121540ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "actix-codec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ "bitflags", "bytes", "futures-core", "futures-sink", "memchr", "pin-project-lite", "tokio", "tokio-util", "tracing", ] [[package]] name = "actix-http" version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae682f693a9cd7b058f2b0b5d9a6d7728a8555779bedbbc35dd88528611d020" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", "ahash", "base64", "bitflags", "brotli", "bytes", "bytestring", "derive_more", "encoding_rs", "flate2", "futures-core", "h2", "http 0.2.12", "httparse", "httpdate", "itoa", "language-tags", "local-channel", "mime", "percent-encoding", "pin-project-lite", "rand", "sha1", "smallvec", "tokio", "tokio-util", "tracing", "zstd", ] [[package]] name = "actix-http-test" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061d27c2a6fea968fdaca0961ff429d23a4ec878c4f68f5d08626663ade69c80" dependencies = [ "actix-codec", "actix-rt", "actix-server", "actix-service", "actix-tls", "actix-utils", "awc", "bytes", "futures-core", "http 0.2.12", "log", "serde", "serde_json", "serde_urlencoded", "slab", "socket2", "tokio", ] [[package]] name = "actix-macros" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", "syn", ] [[package]] name = "actix-multipart" version = "0.7.2" dependencies = [ "actix-http", "actix-multipart-derive", "actix-multipart-rfc7578", "actix-rt", "actix-test", "actix-utils", "actix-web", "assert_matches", "awc", "derive_more", "env_logger", "futures-core", "futures-test", "futures-util", "httparse", "local-waker", "log", "memchr", "mime", "multer", "rand", "serde", "serde_json", "serde_plain", "tempfile", "tokio", "tokio-stream", ] [[package]] name = "actix-multipart-derive" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" dependencies = [ "darling", "parse-size", "proc-macro2", "quote", "syn", ] [[package]] name = "actix-multipart-rfc7578" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b79276c9ca6339b08d468b8165df55ce9ad361f68ac808eb755e23f30e19257" dependencies = [ "actix-http", "bytes", "common-multipart-rfc7578", "futures-core", "thiserror", ] [[package]] name = "actix-router" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", "cfg-if", "http 0.2.12", "regex", "regex-lite", "serde", "tracing", ] [[package]] name = "actix-rt" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" dependencies = [ "actix-macros", "futures-core", "tokio", ] [[package]] name = "actix-server" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", "futures-util", "mio", "socket2", "tokio", "tracing", ] [[package]] name = "actix-service" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" dependencies = [ "futures-core", "paste", "pin-project-lite", ] [[package]] name = "actix-test" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439022b5a7b5dac10798465029a9566e8e0cca7a6014541ed277b695691fac5f" dependencies = [ "actix-codec", "actix-http", "actix-http-test", "actix-rt", "actix-service", "actix-utils", "actix-web", "awc", "futures-core", "futures-util", "log", "serde", "serde_json", "serde_urlencoded", "tokio", ] [[package]] name = "actix-tls" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", "http 0.2.12", "http 1.1.0", "impl-more", "pin-project-lite", "tokio", "tokio-util", "tracing", ] [[package]] name = "actix-utils" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" dependencies = [ "local-waker", "pin-project-lite", ] [[package]] name = "actix-web" version = "4.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1988c02af8d2b718c05bc4aeb6a66395b7cdf32858c2c71131e5637a8c05a9ff" dependencies = [ "actix-codec", "actix-http", "actix-macros", "actix-router", "actix-rt", "actix-server", "actix-service", "actix-utils", "actix-web-codegen", "ahash", "bytes", "bytestring", "cfg-if", "cookie", "derive_more", "encoding_rs", "futures-core", "futures-util", "itoa", "language-tags", "log", "mime", "once_cell", "pin-project-lite", "regex", "regex-lite", "serde", "serde_json", "serde_urlencoded", "smallvec", "socket2", "time", "url", ] [[package]] name = "actix-web-codegen" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" dependencies = [ "actix-router", "proc-macro2", "quote", "syn", ] [[package]] name = "addr2line" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "alloc-no-stdlib" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ "alloc-no-stdlib", ] [[package]] name = "anstream" version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", ] [[package]] name = "assert_matches" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "awc" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe6b67e44fb95d1dc9467e3930383e115f9b4ed60ca689db41409284e967a12d" dependencies = [ "actix-codec", "actix-http", "actix-rt", "actix-service", "actix-tls", "actix-utils", "base64", "bytes", "cfg-if", "cookie", "derive_more", "futures-core", "futures-util", "h2", "http 0.2.12", "itoa", "log", "mime", "percent-encoding", "pin-project-lite", "rand", "serde", "serde_json", "serde_urlencoded", "tokio", ] [[package]] name = "backtrace" version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "brotli" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", "brotli-decompressor", ] [[package]] name = "brotli-decompressor" version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] name = "bytes" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bytestring" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" dependencies = [ "bytes", ] [[package]] name = "cc" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" dependencies = [ "jobserver", "libc", "once_cell", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "colorchoice" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "common-multipart-rfc7578" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baee326bc603965b0f26583e1ecd7c111c41b49bd92a344897476a352798869" dependencies = [ "bytes", "futures-core", "futures-util", "http 0.2.12", "mime", "mime_guess", "rand", "thiserror", ] [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "cookie" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "percent-encoding", "time", "version_check", ] [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "darling" version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", "syn", ] [[package]] name = "darling_macro" version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", "syn", ] [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] [[package]] name = "derive_more" version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", "syn", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "encoding_rs" version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] [[package]] name = "env_filter" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ "anstream", "anstyle", "env_filter", "humantime", "log", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fastrand" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "flate2" version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-test" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce388237b32ac42eca0df1ba55ed3bbda4eaf005d7d4b5dbc0b20ab962928ac9" dependencies = [ "futures-core", "futures-executor", "futures-io", "futures-macro", "futures-sink", "futures-task", "futures-util", "pin-project", "pin-utils", ] [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-io", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "h2" version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "http" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "httparse" version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "impl-more" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" [[package]] name = "indexmap" version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "is_terminal_polyfill" version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] [[package]] name = "language-tags" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "local-channel" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" dependencies = [ "futures-core", "futures-sink", "local-waker", ] [[package]] name = "local-waker" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", ] [[package]] name = "miniz_oxide" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi", "windows-sys 0.48.0", ] [[package]] name = "multer" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" dependencies = [ "bytes", "encoding_rs", "futures-util", "http 1.1.0", "httparse", "memchr", "mime", "spin", "version_check", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "object" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "parse-size" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-lite" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" [[package]] name = "regex-syntax" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ "semver", ] [[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_plain" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" dependencies = [ "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" version = "2.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys 0.52.0", ] [[package]] name = "thiserror" version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "time" version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tinyvec" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "windows-sys 0.48.0", ] [[package]] name = "tokio-stream" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-util" version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "url" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zstd" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", "pkg-config", ] actix-multipart-0.7.2/Cargo.toml0000644000000062100000000000100121660ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "actix-multipart" version = "0.7.2" authors = [ "Nikolay Kim ", "Jacob Halsey ", ] description = "Multipart form support for Actix Web" homepage = "https://actix.rs" readme = "README.md" keywords = [ "http", "web", "framework", "async", "futures", ] license = "MIT OR Apache-2.0" repository = "https://github.com/actix/actix-web" [package.metadata.cargo_check_external_types] allowed_external_types = [ "actix_http::*", "actix_multipart_derive::*", "actix_utils::*", "actix_web::*", "bytes::*", "futures_core::*", "mime::*", "serde_json::*", "serde_plain::*", "serde::*", "tempfile::*", ] [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [dependencies.actix-multipart-derive] version = "=0.7.0" optional = true [dependencies.actix-utils] version = "3" [dependencies.actix-web] version = "4" default-features = false [dependencies.derive_more] version = "0.99.5" [dependencies.futures-core] version = "0.3.17" features = ["alloc"] default-features = false [dependencies.futures-util] version = "0.3.17" features = ["alloc"] default-features = false [dependencies.httparse] version = "1.3" [dependencies.local-waker] version = "0.1" [dependencies.log] version = "0.4" [dependencies.memchr] version = "2.5" [dependencies.mime] version = "0.3" [dependencies.rand] version = "0.8" [dependencies.serde] version = "1" [dependencies.serde_json] version = "1" [dependencies.serde_plain] version = "1" [dependencies.tempfile] version = "3.4" optional = true [dependencies.tokio] version = "1.24.2" features = [ "sync", "io-util", ] [dev-dependencies.actix-http] version = "3" [dev-dependencies.actix-multipart-rfc7578] version = "0.10" [dev-dependencies.actix-rt] version = "2.2" [dev-dependencies.actix-test] version = "0.1" [dev-dependencies.actix-web] version = "4" [dev-dependencies.assert_matches] version = "1" [dev-dependencies.awc] version = "3" [dev-dependencies.env_logger] version = "0.11" [dev-dependencies.futures-test] version = "0.3" [dev-dependencies.futures-util] version = "0.3.17" features = ["alloc"] default-features = false [dev-dependencies.multer] version = "3" [dev-dependencies.tokio] version = "1.24.2" features = ["sync"] [dev-dependencies.tokio-stream] version = "0.1" [features] default = [ "tempfile", "derive", ] derive = ["actix-multipart-derive"] tempfile = [ "dep:tempfile", "tokio/fs", ] [lints.rust.future_incompatible] level = "deny" priority = 0 [lints.rust.nonstandard_style] level = "deny" priority = 0 [lints.rust.rust_2018_idioms] level = "deny" priority = 0 actix-multipart-0.7.2/Cargo.toml.orig000064400000000000000000000037101046102023000156510ustar 00000000000000[package] name = "actix-multipart" version = "0.7.2" authors = [ "Nikolay Kim ", "Jacob Halsey ", ] description = "Multipart form support for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web" license = "MIT OR Apache-2.0" edition = "2021" [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true [package.metadata.cargo_check_external_types] allowed_external_types = [ "actix_http::*", "actix_multipart_derive::*", "actix_utils::*", "actix_web::*", "bytes::*", "futures_core::*", "mime::*", "serde_json::*", "serde_plain::*", "serde::*", "tempfile::*", ] [features] default = ["tempfile", "derive"] derive = ["actix-multipart-derive"] tempfile = ["dep:tempfile", "tokio/fs"] [dependencies] actix-multipart-derive = { version = "=0.7.0", optional = true } actix-utils = "3" actix-web = { version = "4", default-features = false } derive_more = "0.99.5" futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } httparse = "1.3" local-waker = "0.1" log = "0.4" memchr = "2.5" mime = "0.3" rand = "0.8" serde = "1" serde_json = "1" serde_plain = "1" tempfile = { version = "3.4", optional = true } tokio = { version = "1.24.2", features = ["sync", "io-util"] } [dev-dependencies] actix-http = "3" actix-multipart-rfc7578 = "0.10" actix-rt = "2.2" actix-test = "0.1" actix-web = "4" assert_matches = "1" awc = "3" env_logger = "0.11" futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-test = "0.3" multer = "3" tokio = { version = "1.24.2", features = ["sync"] } tokio-stream = "0.1" [lints.rust] future_incompatible = { level = "deny" } rust_2018_idioms = { level = "deny" } nonstandard_style = { level = "deny" } actix-multipart-0.7.2/LICENSE-APACHE000064400000000000000000000261201046102023000147060ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017-NOW Actix Team Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. actix-multipart-0.7.2/LICENSE-MIT000064400000000000000000000020421046102023000144130ustar 00000000000000Copyright (c) 2017-NOW Actix Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. actix-multipart-0.7.2/README.md000064400000000000000000000037221046102023000142440ustar 00000000000000# `actix-multipart` [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.7.2)](https://docs.rs/actix-multipart/0.7.2) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
[![dependency status](https://deps.rs/crate/actix-multipart/0.7.2/status.svg)](https://deps.rs/crate/actix-multipart/0.7.2) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) Multipart form support for Actix Web. ## Examples ```rust use actix_web::{post, App, HttpServer, Responder}; use actix_multipart::form::{json::Json as MPJson, tempfile::TempFile, MultipartForm}; use serde::Deserialize; #[derive(Debug, Deserialize)] struct Metadata { name: String, } #[derive(Debug, MultipartForm)] struct UploadForm { #[multipart(limit = "100MB")] file: TempFile, json: MPJson, } #[post("/videos")] pub async fn post_video(MultipartForm(form): MultipartForm) -> impl Responder { format!( "Uploaded file {}, with size: {}", form.json.name, form.file.size ) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(move || App::new().service(post_video)) .bind(("127.0.0.1", 8080))? .run() .await } ``` cURL request: ```sh curl -v --request POST \ --url http://localhost:8080/videos \ -F 'json={"name": "Cargo.lock"};type=application/json' \ -F file=@./Cargo.lock ``` [More available in the examples repo →](https://github.com/actix/examples/tree/master/forms/multipart) actix-multipart-0.7.2/src/error.rs000064400000000000000000000075561046102023000152640ustar 00000000000000//! Error and Result module use actix_web::{ error::{ParseError, PayloadError}, http::StatusCode, ResponseError, }; use derive_more::{Display, Error, From}; /// A set of errors that can occur during parsing multipart streams. #[derive(Debug, Display, From, Error)] #[non_exhaustive] pub enum Error { /// Could not find Content-Type header. #[display(fmt = "Could not find Content-Type header")] ContentTypeMissing, /// Could not parse Content-Type header. #[display(fmt = "Could not parse Content-Type header")] ContentTypeParse, /// Parsed Content-Type did not have "multipart" top-level media type. /// /// Also raised when extracting a [`MultipartForm`] from a request that does not have the /// "multipart/form-data" media type. /// /// [`MultipartForm`]: struct@crate::form::MultipartForm #[display(fmt = "Parsed Content-Type did not have "multipart" top-level media type")] ContentTypeIncompatible, /// Multipart boundary is not found. #[display(fmt = "Multipart boundary is not found")] BoundaryMissing, /// Content-Disposition header was not found or not of disposition type "form-data" when parsing /// a "form-data" field. /// /// As per [RFC 7578 ยง4.2], a "multipart/form-data" field's Content-Disposition header must /// always be present and have a disposition type of "form-data". /// /// [RFC 7578 ยง4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 #[display(fmt = "Content-Disposition header was not found when parsing a \"form-data\" field")] ContentDispositionMissing, /// Content-Disposition name parameter was not found when parsing a "form-data" field. /// /// As per [RFC 7578 ยง4.2], a "multipart/form-data" field's Content-Disposition header must /// always include a "name" parameter. /// /// [RFC 7578 ยง4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 #[display(fmt = "Content-Disposition header was not found when parsing a \"form-data\" field")] ContentDispositionNameMissing, /// Nested multipart is not supported. #[display(fmt = "Nested multipart is not supported")] Nested, /// Multipart stream is incomplete. #[display(fmt = "Multipart stream is incomplete")] Incomplete, /// Field parsing failed. #[display(fmt = "Error during field parsing")] Parse(ParseError), /// HTTP payload error. #[display(fmt = "Payload error")] Payload(PayloadError), /// Stream is not consumed. #[display(fmt = "Stream is not consumed")] NotConsumed, /// Form field handler raised error. #[display(fmt = "An error occurred processing field: {name}")] Field { name: String, source: actix_web::Error, }, /// Duplicate field found (for structure that opted-in to denying duplicate fields). #[display(fmt = "Duplicate field found: {_0}")] #[from(ignore)] DuplicateField(#[error(not(source))] String), /// Required field is missing. #[display(fmt = "Required field is missing: {_0}")] #[from(ignore)] MissingField(#[error(not(source))] String), /// Unknown field (for structure that opted-in to denying unknown fields). #[display(fmt = "Unknown field: {_0}")] #[from(ignore)] UnknownField(#[error(not(source))] String), } /// Return `BadRequest` for `MultipartError`. impl ResponseError for Error { fn status_code(&self) -> StatusCode { match &self { Error::Field { source, .. } => source.as_response_error().status_code(), Error::ContentTypeIncompatible => StatusCode::UNSUPPORTED_MEDIA_TYPE, _ => StatusCode::BAD_REQUEST, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_multipart_error() { let resp = Error::BoundaryMissing.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } actix-multipart-0.7.2/src/extractor.rs000064400000000000000000000021641046102023000161340ustar 00000000000000use actix_utils::future::{ready, Ready}; use actix_web::{dev::Payload, Error, FromRequest, HttpRequest}; use crate::multipart::Multipart; /// Extract request's payload as multipart stream. /// /// Content-type: multipart/*; /// /// # Examples /// /// ``` /// use actix_web::{web, HttpResponse}; /// use actix_multipart::Multipart; /// use futures_util::StreamExt as _; /// /// async fn index(mut payload: Multipart) -> actix_web::Result { /// // iterate over multipart stream /// while let Some(item) = payload.next().await { /// let mut field = item?; /// /// // Field in turn is stream of *Bytes* object /// while let Some(chunk) = field.next().await { /// println!("-- CHUNK: \n{:?}", std::str::from_utf8(&chunk?)); /// } /// } /// /// Ok(HttpResponse::Ok().finish()) /// } /// ``` impl FromRequest for Multipart { type Error = Error; type Future = Ready>; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { ready(Ok(Multipart::from_req(req, payload))) } } actix-multipart-0.7.2/src/field.rs000064400000000000000000000403701046102023000152050ustar 00000000000000use std::{ cell::RefCell, cmp, fmt, future::poll_fn, mem, pin::Pin, rc::Rc, task::{ready, Context, Poll}, }; use actix_web::{ error::PayloadError, http::header::{self, ContentDisposition, HeaderMap}, web::{Bytes, BytesMut}, }; use derive_more::{Display, Error}; use futures_core::Stream; use mime::Mime; use crate::{ error::Error, payload::{PayloadBuffer, PayloadRef}, safety::Safety, }; /// Error type returned from [`Field::bytes()`] when field data is larger than limit. #[derive(Debug, Display, Error)] #[display(fmt = "size limit exceeded while collecting field data")] #[non_exhaustive] pub struct LimitExceeded; /// A single field in a multipart stream. pub struct Field { /// Field's Content-Type. content_type: Option, /// Field's Content-Disposition. content_disposition: Option, /// Form field name. /// /// A non-optional storage for form field names to avoid unwraps in `form` module. Will be an /// empty string in non-form contexts. /// // INVARIANT: always non-empty when request content-type is multipart/form-data. pub(crate) form_field_name: String, /// Field's header map. headers: HeaderMap, safety: Safety, inner: Rc>, } impl Field { pub(crate) fn new( content_type: Option, content_disposition: Option, form_field_name: Option, headers: HeaderMap, safety: Safety, inner: Rc>, ) -> Self { Field { content_type, content_disposition, form_field_name: form_field_name.unwrap_or_default(), headers, inner, safety, } } /// Returns a reference to the field's header map. pub fn headers(&self) -> &HeaderMap { &self.headers } /// Returns a reference to the field's content (mime) type, if it is supplied by the client. /// /// According to [RFC 7578](https://www.rfc-editor.org/rfc/rfc7578#section-4.4), if it is not /// present, it should default to "text/plain". Note it is the responsibility of the client to /// provide the appropriate content type, there is no attempt to validate this by the server. pub fn content_type(&self) -> Option<&Mime> { self.content_type.as_ref() } /// Returns this field's parsed Content-Disposition header, if set. /// /// # Validation /// /// Per [RFC 7578 ยง4.2], the parts of a multipart/form-data payload MUST contain a /// Content-Disposition header field where the disposition type is `form-data` and MUST also /// contain an additional parameter of `name` with its value being the original field name from /// the form. This requirement is enforced during extraction for multipart/form-data requests, /// but not other kinds of multipart requests (such as multipart/related). /// /// As such, it is safe to `.unwrap()` calls `.content_disposition()` if you've verified. /// /// The [`name()`](Self::name) method is also provided as a convenience for obtaining the /// aforementioned name parameter. /// /// [RFC 7578 ยง4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 pub fn content_disposition(&self) -> Option<&ContentDisposition> { self.content_disposition.as_ref() } /// Returns the field's name, if set. /// /// See [`content_disposition()`](Self::content_disposition) regarding guarantees on presence of /// the "name" field. pub fn name(&self) -> Option<&str> { self.content_disposition()?.get_name() } /// Collects the raw field data, up to `limit` bytes. /// /// # Errors /// /// Any errors produced by the data stream are returned as `Ok(Err(Error))` immediately. /// /// If the buffered data size would exceed `limit`, an `Err(LimitExceeded)` is returned. Note /// that, in this case, the full data stream is exhausted before returning the error so that /// subsequent fields can still be read. To better defend against malicious/infinite requests, /// it is advisable to also put a timeout on this call. pub async fn bytes(&mut self, limit: usize) -> Result, LimitExceeded> { /// Sensible default (2kB) for initial, bounded allocation when collecting body bytes. const INITIAL_ALLOC_BYTES: usize = 2 * 1024; let mut exceeded_limit = false; let mut buf = BytesMut::with_capacity(INITIAL_ALLOC_BYTES); let mut field = Pin::new(self); match poll_fn(|cx| loop { match ready!(field.as_mut().poll_next(cx)) { // if already over limit, discard chunk to advance multipart request Some(Ok(_chunk)) if exceeded_limit => {} // if limit is exceeded set flag to true and continue Some(Ok(chunk)) if buf.len() + chunk.len() > limit => { exceeded_limit = true; // eagerly de-allocate field data buffer let _ = mem::take(&mut buf); } Some(Ok(chunk)) => buf.extend_from_slice(&chunk), None => return Poll::Ready(Ok(())), Some(Err(err)) => return Poll::Ready(Err(err)), } }) .await { // propagate error returned from body poll Err(err) => Ok(Err(err)), // limit was exceeded while reading body Ok(()) if exceeded_limit => Err(LimitExceeded), // otherwise return body buffer Ok(()) => Ok(Ok(buf.freeze())), } } } impl Stream for Field { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); let mut inner = this.inner.borrow_mut(); if let Some(mut buffer) = inner .payload .as_ref() .expect("Field should not be polled after completion") .get_mut(&this.safety) { // check safety and poll read payload to buffer. buffer.poll_stream(cx)?; } else if !this.safety.is_clean() { // safety violation return Poll::Ready(Some(Err(Error::NotConsumed))); } else { return Poll::Pending; } inner.poll(&this.safety) } } impl fmt::Debug for Field { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(ct) = &self.content_type { writeln!(f, "\nField: {}", ct)?; } else { writeln!(f, "\nField:")?; } writeln!(f, " boundary: {}", self.inner.borrow().boundary)?; writeln!(f, " headers:")?; for (key, val) in self.headers.iter() { writeln!(f, " {:?}: {:?}", key, val)?; } Ok(()) } } pub(crate) struct InnerField { /// Payload is initialized as Some and is `take`n when the field stream finishes. payload: Option, boundary: String, eof: bool, length: Option, } impl InnerField { pub(crate) fn new_in_rc( payload: PayloadRef, boundary: String, headers: &HeaderMap, ) -> Result>, PayloadError> { Self::new(payload, boundary, headers).map(|this| Rc::new(RefCell::new(this))) } pub(crate) fn new( payload: PayloadRef, boundary: String, headers: &HeaderMap, ) -> Result { let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) { match len.to_str().ok().and_then(|len| len.parse::().ok()) { Some(len) => Some(len), None => return Err(PayloadError::Incomplete(None)), } } else { None }; Ok(InnerField { boundary, payload: Some(payload), eof: false, length: len, }) } /// Reads body part content chunk of the specified size. /// /// The body part must has `Content-Length` header with proper value. pub(crate) fn read_len( payload: &mut PayloadBuffer, size: &mut u64, ) -> Poll>> { if *size == 0 { Poll::Ready(None) } else { match payload.read_max(*size)? { Some(mut chunk) => { let len = cmp::min(chunk.len() as u64, *size); *size -= len; let ch = chunk.split_to(len as usize); if !chunk.is_empty() { payload.unprocessed(chunk); } Poll::Ready(Some(Ok(ch))) } None => { if payload.eof && (*size != 0) { Poll::Ready(Some(Err(Error::Incomplete))) } else { Poll::Pending } } } } } /// Reads content chunk of body part with unknown length. /// /// The `Content-Length` header for body part is not necessary. pub(crate) fn read_stream( payload: &mut PayloadBuffer, boundary: &str, ) -> Poll>> { let mut pos = 0; let len = payload.buf.len(); if len == 0 { return if payload.eof { Poll::Ready(Some(Err(Error::Incomplete))) } else { Poll::Pending }; } // check boundary if len > 4 && payload.buf[0] == b'\r' { let b_len = if &payload.buf[..2] == b"\r\n" && &payload.buf[2..4] == b"--" { Some(4) } else if &payload.buf[1..3] == b"--" { Some(3) } else { None }; if let Some(b_len) = b_len { let b_size = boundary.len() + b_len; if len < b_size { return Poll::Pending; } else if &payload.buf[b_len..b_size] == boundary.as_bytes() { // found boundary return Poll::Ready(None); } } } loop { return if let Some(idx) = memchr::memmem::find(&payload.buf[pos..], b"\r") { let cur = pos + idx; // check if we have enough data for boundary detection if cur + 4 > len { if cur > 0 { Poll::Ready(Some(Ok(payload.buf.split_to(cur).freeze()))) } else { Poll::Pending } } else { // check boundary if (&payload.buf[cur..cur + 2] == b"\r\n" && &payload.buf[cur + 2..cur + 4] == b"--") || (&payload.buf[cur..=cur] == b"\r" && &payload.buf[cur + 1..cur + 3] == b"--") { if cur != 0 { // return buffer Poll::Ready(Some(Ok(payload.buf.split_to(cur).freeze()))) } else { pos = cur + 1; continue; } } else { // not boundary pos = cur + 1; continue; } } } else { Poll::Ready(Some(Ok(payload.buf.split().freeze()))) }; } } pub(crate) fn poll(&mut self, safety: &Safety) -> Poll>> { if self.payload.is_none() { return Poll::Ready(None); } let result = if let Some(mut payload) = self .payload .as_ref() .expect("Field should not be polled after completion") .get_mut(safety) { if !self.eof { let res = if let Some(ref mut len) = self.length { InnerField::read_len(&mut payload, len) } else { InnerField::read_stream(&mut payload, &self.boundary) }; match res { Poll::Pending => return Poll::Pending, Poll::Ready(Some(Ok(bytes))) => return Poll::Ready(Some(Ok(bytes))), Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), Poll::Ready(None) => self.eof = true, } } match payload.readline() { Ok(None) => Poll::Pending, Ok(Some(line)) => { if line.as_ref() != b"\r\n" { log::warn!("multipart field did not read all the data or it is malformed"); } Poll::Ready(None) } Err(err) => Poll::Ready(Some(Err(err))), } } else { Poll::Pending }; if let Poll::Ready(None) = result { // drop payload buffer and make future un-poll-able let _ = self.payload.take(); } result } } #[cfg(test)] mod tests { use futures_util::{stream, StreamExt as _}; use super::*; use crate::Multipart; // TODO: use test utility when multi-file support is introduced fn create_double_request_with_header() -> (Bytes, HeaderMap) { let bytes = Bytes::from( "testasdadsad\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\n\ \r\n\ one+one+one\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\n\ \r\n\ two+two+two\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", ); let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, header::HeaderValue::from_static( "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", ), ); (bytes, headers) } #[actix_rt::test] async fn bytes_unlimited() { let (body, headers) = create_double_request_with_header(); let mut multipart = Multipart::new(&headers, stream::iter([Ok(body)])); let field = multipart .next() .await .expect("multipart should have two fields") .expect("multipart body should be well formatted") .bytes(usize::MAX) .await .expect("field data should not be size limited") .expect("reading field data should not error"); assert_eq!(field, "one+one+one"); let field = multipart .next() .await .expect("multipart should have two fields") .expect("multipart body should be well formatted") .bytes(usize::MAX) .await .expect("field data should not be size limited") .expect("reading field data should not error"); assert_eq!(field, "two+two+two"); } #[actix_rt::test] async fn bytes_limited() { let (body, headers) = create_double_request_with_header(); let mut multipart = Multipart::new(&headers, stream::iter([Ok(body)])); multipart .next() .await .expect("multipart should have two fields") .expect("multipart body should be well formatted") .bytes(8) // smaller than data size .await .expect_err("field data should be size limited"); // next field still readable let field = multipart .next() .await .expect("multipart should have two fields") .expect("multipart body should be well formatted") .bytes(usize::MAX) .await .expect("field data should not be size limited") .expect("reading field data should not error"); assert_eq!(field, "two+two+two"); } } actix-multipart-0.7.2/src/form/bytes.rs000064400000000000000000000026541046102023000162160ustar 00000000000000//! Reads a field into memory. use actix_web::{web::BytesMut, HttpRequest}; use futures_core::future::LocalBoxFuture; use futures_util::TryStreamExt as _; use mime::Mime; use crate::{ form::{FieldReader, Limits}, Field, MultipartError, }; /// Read the field into memory. #[derive(Debug)] pub struct Bytes { /// The data. pub data: actix_web::web::Bytes, /// The value of the `Content-Type` header. pub content_type: Option, /// The `filename` value in the `Content-Disposition` header. pub file_name: Option, } impl<'t> FieldReader<'t> for Bytes { type Future = LocalBoxFuture<'t, Result>; fn read_field(_: &'t HttpRequest, mut field: Field, limits: &'t mut Limits) -> Self::Future { Box::pin(async move { let mut buf = BytesMut::with_capacity(131_072); while let Some(chunk) = field.try_next().await? { limits.try_consume_limits(chunk.len(), true)?; buf.extend(chunk); } Ok(Bytes { data: buf.freeze(), content_type: field.content_type().map(ToOwned::to_owned), file_name: field .content_disposition() .expect("multipart form fields should have a content-disposition header") .get_filename() .map(ToOwned::to_owned), }) }) } } actix-multipart-0.7.2/src/form/json.rs000064400000000000000000000143631046102023000160410ustar 00000000000000//! Deserializes a field as JSON. use std::sync::Arc; use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError}; use derive_more::{Deref, DerefMut, Display, Error}; use futures_core::future::LocalBoxFuture; use serde::de::DeserializeOwned; use super::FieldErrorHandler; use crate::{ form::{bytes::Bytes, FieldReader, Limits}, Field, MultipartError, }; /// Deserialize from JSON. #[derive(Debug, Deref, DerefMut)] pub struct Json(pub T); impl Json { pub fn into_inner(self) -> T { self.0 } } impl<'t, T> FieldReader<'t> for Json where T: DeserializeOwned + 'static, { type Future = LocalBoxFuture<'t, Result>; fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future { Box::pin(async move { let config = JsonConfig::from_req(req); if config.validate_content_type { let valid = if let Some(mime) = field.content_type() { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) } else { false }; if !valid { return Err(MultipartError::Field { name: field.form_field_name, source: config.map_error(req, JsonFieldError::ContentType), }); } } let form_field_name = field.form_field_name.clone(); let bytes = Bytes::read_field(req, field, limits).await?; Ok(Json(serde_json::from_slice(bytes.data.as_ref()).map_err( |err| MultipartError::Field { name: form_field_name, source: config.map_error(req, JsonFieldError::Deserialize(err)), }, )?)) }) } } #[derive(Debug, Display, Error)] #[non_exhaustive] pub enum JsonFieldError { /// Deserialize error. #[display(fmt = "Json deserialize error: {}", _0)] Deserialize(serde_json::Error), /// Content type error. #[display(fmt = "Content type error")] ContentType, } impl ResponseError for JsonFieldError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } /// Configuration for the [`Json`] field reader. #[derive(Clone)] pub struct JsonConfig { err_handler: FieldErrorHandler, validate_content_type: bool, } const DEFAULT_CONFIG: JsonConfig = JsonConfig { err_handler: None, validate_content_type: true, }; impl JsonConfig { pub fn error_handler(mut self, f: F) -> Self where F: Fn(JsonFieldError, &HttpRequest) -> Error + Send + Sync + 'static, { self.err_handler = Some(Arc::new(f)); self } /// Extract payload config from app data. Check both `T` and `Data`, in that order, and fall /// back to the default payload config. fn from_req(req: &HttpRequest) -> &Self { req.app_data::() .or_else(|| req.app_data::>().map(|d| d.as_ref())) .unwrap_or(&DEFAULT_CONFIG) } fn map_error(&self, req: &HttpRequest, err: JsonFieldError) -> Error { if let Some(err_handler) = self.err_handler.as_ref() { (*err_handler)(err, req) } else { err.into() } } /// Sets whether or not the field must have a valid `Content-Type` header to be parsed. pub fn validate_content_type(mut self, validate_content_type: bool) -> Self { self.validate_content_type = validate_content_type; self } } impl Default for JsonConfig { fn default() -> Self { DEFAULT_CONFIG } } #[cfg(test)] mod tests { use std::collections::HashMap; use actix_web::{http::StatusCode, web, web::Bytes, App, HttpResponse, Responder}; use crate::form::{ json::{Json, JsonConfig}, MultipartForm, }; #[derive(MultipartForm)] struct JsonForm { json: Json>, } async fn test_json_route(form: MultipartForm) -> impl Responder { let mut expected = HashMap::new(); expected.insert("key1".to_owned(), "value1".to_owned()); expected.insert("key2".to_owned(), "value2".to_owned()); assert_eq!(&*form.json, &expected); HttpResponse::Ok().finish() } const TEST_JSON: &str = r#"{"key1": "value1", "key2": "value2"}"#; #[actix_rt::test] async fn test_json_without_content_type() { let srv = actix_test::start(|| { App::new() .route("/", web::post().to(test_json_route)) .app_data(JsonConfig::default().validate_content_type(false)) }); let (body, headers) = crate::test::create_form_data_payload_and_headers( "json", None, None, Bytes::from_static(TEST_JSON.as_bytes()), ); let mut req = srv.post("/"); *req.headers_mut() = headers; let res = req.send_body(body).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } #[actix_rt::test] async fn test_content_type_validation() { let srv = actix_test::start(|| { App::new() .route("/", web::post().to(test_json_route)) .app_data(JsonConfig::default().validate_content_type(true)) }); // Deny because wrong content type let (body, headers) = crate::test::create_form_data_payload_and_headers( "json", None, Some(mime::APPLICATION_OCTET_STREAM), Bytes::from_static(TEST_JSON.as_bytes()), ); let mut req = srv.post("/"); *req.headers_mut() = headers; let res = req.send_body(body).await.unwrap(); assert_eq!(res.status(), StatusCode::BAD_REQUEST); // Allow because correct content type let (body, headers) = crate::test::create_form_data_payload_and_headers( "json", None, Some(mime::APPLICATION_JSON), Bytes::from_static(TEST_JSON.as_bytes()), ); let mut req = srv.post("/"); *req.headers_mut() = headers; let res = req.send_body(body).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } } actix-multipart-0.7.2/src/form/mod.rs000064400000000000000000000670341046102023000156520ustar 00000000000000//! Process and extract typed data from a multipart stream. use std::{ any::Any, collections::HashMap, future::{ready, Future}, sync::Arc, }; use actix_web::{dev, error::PayloadError, web, Error, FromRequest, HttpRequest}; use derive_more::{Deref, DerefMut}; use futures_core::future::LocalBoxFuture; use futures_util::{TryFutureExt as _, TryStreamExt as _}; use crate::{Field, Multipart, MultipartError}; pub mod bytes; pub mod json; #[cfg(feature = "tempfile")] pub mod tempfile; pub mod text; #[cfg(feature = "derive")] pub use actix_multipart_derive::MultipartForm; type FieldErrorHandler = Option Error + Send + Sync>>; /// Trait that data types to be used in a multipart form struct should implement. /// /// It represents an asynchronous handler that processes a multipart field to produce `Self`. pub trait FieldReader<'t>: Sized + Any { /// Future that resolves to a `Self`. type Future: Future>; /// The form will call this function to handle the field. /// /// # Panics /// /// When reading the `field` payload using its `Stream` implementation, polling (manually or via /// `next()`/`try_next()`) may panic after the payload is exhausted. If this is a problem for /// your implementation of this method, you should [`fuse()`] the `Field` first. /// /// [`fuse()`]: futures_util::stream::StreamExt::fuse() fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future; } /// Used to accumulate the state of the loaded fields. #[doc(hidden)] #[derive(Default, Deref, DerefMut)] pub struct State(pub HashMap>); /// Trait that the field collection types implement, i.e. `Vec`, `Option`, or `T` itself. #[doc(hidden)] pub trait FieldGroupReader<'t>: Sized + Any { type Future: Future>; /// The form will call this function for each matching field. fn handle_field( req: &'t HttpRequest, field: Field, limits: &'t mut Limits, state: &'t mut State, duplicate_field: DuplicateField, ) -> Self::Future; /// Construct `Self` from the group of processed fields. fn from_state(name: &str, state: &'t mut State) -> Result; } impl<'t, T> FieldGroupReader<'t> for Option where T: FieldReader<'t>, { type Future = LocalBoxFuture<'t, Result<(), MultipartError>>; fn handle_field( req: &'t HttpRequest, field: Field, limits: &'t mut Limits, state: &'t mut State, duplicate_field: DuplicateField, ) -> Self::Future { if state.contains_key(&field.form_field_name) { match duplicate_field { DuplicateField::Ignore => return Box::pin(ready(Ok(()))), DuplicateField::Deny => { return Box::pin(ready(Err(MultipartError::DuplicateField( field.form_field_name, )))) } DuplicateField::Replace => {} } } Box::pin(async move { let field_name = field.form_field_name.clone(); let t = T::read_field(req, field, limits).await?; state.insert(field_name, Box::new(t)); Ok(()) }) } fn from_state(name: &str, state: &'t mut State) -> Result { Ok(state.remove(name).map(|m| *m.downcast::().unwrap())) } } impl<'t, T> FieldGroupReader<'t> for Vec where T: FieldReader<'t>, { type Future = LocalBoxFuture<'t, Result<(), MultipartError>>; fn handle_field( req: &'t HttpRequest, field: Field, limits: &'t mut Limits, state: &'t mut State, _duplicate_field: DuplicateField, ) -> Self::Future { Box::pin(async move { // Note: Vec GroupReader always allows duplicates let vec = state .entry(field.form_field_name.clone()) .or_insert_with(|| Box::>::default()) .downcast_mut::>() .unwrap(); let item = T::read_field(req, field, limits).await?; vec.push(item); Ok(()) }) } fn from_state(name: &str, state: &'t mut State) -> Result { Ok(state .remove(name) .map(|m| *m.downcast::>().unwrap()) .unwrap_or_default()) } } impl<'t, T> FieldGroupReader<'t> for T where T: FieldReader<'t>, { type Future = LocalBoxFuture<'t, Result<(), MultipartError>>; fn handle_field( req: &'t HttpRequest, field: Field, limits: &'t mut Limits, state: &'t mut State, duplicate_field: DuplicateField, ) -> Self::Future { if state.contains_key(&field.form_field_name) { match duplicate_field { DuplicateField::Ignore => return Box::pin(ready(Ok(()))), DuplicateField::Deny => { return Box::pin(ready(Err(MultipartError::DuplicateField( field.form_field_name, )))) } DuplicateField::Replace => {} } } Box::pin(async move { let field_name = field.form_field_name.clone(); let t = T::read_field(req, field, limits).await?; state.insert(field_name, Box::new(t)); Ok(()) }) } fn from_state(name: &str, state: &'t mut State) -> Result { state .remove(name) .map(|m| *m.downcast::().unwrap()) .ok_or_else(|| MultipartError::MissingField(name.to_owned())) } } /// Trait that allows a type to be used in the [`struct@MultipartForm`] extractor. /// /// You should use the [`macro@MultipartForm`] macro to derive this for your struct. pub trait MultipartCollect: Sized { /// An optional limit in bytes to be applied a given field name. Note this limit will be shared /// across all fields sharing the same name. fn limit(field_name: &str) -> Option; /// The extractor will call this function for each incoming field, the state can be updated /// with the processed field data. fn handle_field<'t>( req: &'t HttpRequest, field: Field, limits: &'t mut Limits, state: &'t mut State, ) -> LocalBoxFuture<'t, Result<(), MultipartError>>; /// Once all the fields have been processed and stored in the state, this is called /// to convert into the struct representation. fn from_state(state: State) -> Result; } #[doc(hidden)] pub enum DuplicateField { /// Additional fields are not processed. Ignore, /// An error will be raised. Deny, /// All fields will be processed, the last one will replace all previous. Replace, } /// Used to keep track of the remaining limits for the form and current field. pub struct Limits { pub total_limit_remaining: usize, pub memory_limit_remaining: usize, pub field_limit_remaining: Option, } impl Limits { pub fn new(total_limit: usize, memory_limit: usize) -> Self { Self { total_limit_remaining: total_limit, memory_limit_remaining: memory_limit, field_limit_remaining: None, } } /// This function should be called within a [`FieldReader`] when reading each chunk of a field /// to ensure that the form limits are not exceeded. /// /// # Arguments /// /// * `bytes` - The number of bytes being read from this chunk /// * `in_memory` - Whether to consume from the memory limits pub fn try_consume_limits( &mut self, bytes: usize, in_memory: bool, ) -> Result<(), MultipartError> { self.total_limit_remaining = self .total_limit_remaining .checked_sub(bytes) .ok_or(MultipartError::Payload(PayloadError::Overflow))?; if in_memory { self.memory_limit_remaining = self .memory_limit_remaining .checked_sub(bytes) .ok_or(MultipartError::Payload(PayloadError::Overflow))?; } if let Some(field_limit) = self.field_limit_remaining { self.field_limit_remaining = Some( field_limit .checked_sub(bytes) .ok_or(MultipartError::Payload(PayloadError::Overflow))?, ); } Ok(()) } } /// Typed `multipart/form-data` extractor. /// /// To extract typed data from a multipart stream, the inner type `T` must implement the /// [`MultipartCollect`] trait. You should use the [`macro@MultipartForm`] macro to derive this /// for your struct. /// /// Note that this extractor rejects requests with any other Content-Type such as `multipart/mixed`, /// `multipart/related`, or non-multipart media types. /// /// Add a [`MultipartFormConfig`] to your app data to configure extraction. #[derive(Deref, DerefMut)] pub struct MultipartForm(pub T); impl MultipartForm { /// Unwrap into inner `T` value. pub fn into_inner(self) -> T { self.0 } } impl FromRequest for MultipartForm where T: MultipartCollect + 'static, { type Error = Error; type Future = LocalBoxFuture<'static, Result>; #[inline] fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { let mut multipart = Multipart::from_req(req, payload); let content_type = match multipart.content_type_or_bail() { Ok(content_type) => content_type, Err(err) => return Box::pin(ready(Err(err.into()))), }; if content_type.subtype() != mime::FORM_DATA { // this extractor only supports multipart/form-data return Box::pin(ready(Err(MultipartError::ContentTypeIncompatible.into()))); }; let config = MultipartFormConfig::from_req(req); let mut limits = Limits::new(config.total_limit, config.memory_limit); let req = req.clone(); let req2 = req.clone(); let err_handler = config.err_handler.clone(); Box::pin( async move { let mut state = State::default(); // ensure limits are shared for all fields with this name let mut field_limits = HashMap::>::new(); while let Some(field) = multipart.try_next().await? { debug_assert!( !field.form_field_name.is_empty(), "multipart form fields should have names", ); // Retrieve the limit for this field let entry = field_limits .entry(field.form_field_name.clone()) .or_insert_with(|| T::limit(&field.form_field_name)); limits.field_limit_remaining.clone_from(entry); T::handle_field(&req, field, &mut limits, &mut state).await?; // Update the stored limit *entry = limits.field_limit_remaining; } let inner = T::from_state(state)?; Ok(MultipartForm(inner)) } .map_err(move |err| { if let Some(handler) = err_handler { (*handler)(err, &req2) } else { err.into() } }), ) } } type MultipartFormErrorHandler = Option Error + Send + Sync>>; /// [`struct@MultipartForm`] extractor configuration. /// /// Add to your app data to have it picked up by [`struct@MultipartForm`] extractors. #[derive(Clone)] pub struct MultipartFormConfig { total_limit: usize, memory_limit: usize, err_handler: MultipartFormErrorHandler, } impl MultipartFormConfig { /// Sets maximum accepted payload size for the entire form. By default this limit is 50MiB. pub fn total_limit(mut self, total_limit: usize) -> Self { self.total_limit = total_limit; self } /// Sets maximum accepted data that will be read into memory. By default this limit is 2MiB. pub fn memory_limit(mut self, memory_limit: usize) -> Self { self.memory_limit = memory_limit; self } /// Sets custom error handler. pub fn error_handler(mut self, f: F) -> Self where F: Fn(MultipartError, &HttpRequest) -> Error + Send + Sync + 'static, { self.err_handler = Some(Arc::new(f)); self } /// Extracts payload config from app data. Check both `T` and `Data`, in that order, and fall /// back to the default payload config. fn from_req(req: &HttpRequest) -> &Self { req.app_data::() .or_else(|| req.app_data::>().map(|d| d.as_ref())) .unwrap_or(&DEFAULT_CONFIG) } } const DEFAULT_CONFIG: MultipartFormConfig = MultipartFormConfig { total_limit: 52_428_800, // 50 MiB memory_limit: 2_097_152, // 2 MiB err_handler: None, }; impl Default for MultipartFormConfig { fn default() -> Self { DEFAULT_CONFIG } } #[cfg(test)] mod tests { use actix_http::encoding::Decoder; use actix_multipart_rfc7578::client::multipart; use actix_test::TestServer; use actix_web::{ dev::Payload, http::StatusCode, web, App, HttpRequest, HttpResponse, Resource, Responder, }; use awc::{Client, ClientResponse}; use futures_core::future::LocalBoxFuture; use futures_util::TryStreamExt as _; use super::MultipartForm; use crate::{ form::{ bytes::Bytes, tempfile::TempFile, text::Text, FieldReader, Limits, MultipartFormConfig, }, Field, MultipartError, }; pub async fn send_form( srv: &TestServer, form: multipart::Form<'static>, uri: &'static str, ) -> ClientResponse> { Client::default() .post(srv.url(uri)) .content_type(form.content_type()) .send_body(multipart::Body::from(form)) .await .unwrap() } /// Test `Option` fields. #[derive(MultipartForm)] struct TestOptions { field1: Option>, field2: Option>, } async fn test_options_route(form: MultipartForm) -> impl Responder { assert!(form.field1.is_some()); assert!(form.field2.is_none()); HttpResponse::Ok().finish() } #[actix_rt::test] async fn test_options() { let srv = actix_test::start(|| App::new().route("/", web::post().to(test_options_route))); let mut form = multipart::Form::default(); form.add_text("field1", "value"); let response = send_form(&srv, form, "/").await; assert_eq!(response.status(), StatusCode::OK); } /// Test `Vec` fields. #[derive(MultipartForm)] struct TestVec { list1: Vec>, list2: Vec>, } async fn test_vec_route(form: MultipartForm) -> impl Responder { let form = form.into_inner(); let strings = form .list1 .into_iter() .map(|s| s.into_inner()) .collect::>(); assert_eq!(strings, vec!["value1", "value2", "value3"]); assert_eq!(form.list2.len(), 0); HttpResponse::Ok().finish() } #[actix_rt::test] async fn test_vec() { let srv = actix_test::start(|| App::new().route("/", web::post().to(test_vec_route))); let mut form = multipart::Form::default(); form.add_text("list1", "value1"); form.add_text("list1", "value2"); form.add_text("list1", "value3"); let response = send_form(&srv, form, "/").await; assert_eq!(response.status(), StatusCode::OK); } /// Test the `rename` field attribute. #[derive(MultipartForm)] struct TestFieldRenaming { #[multipart(rename = "renamed")] field1: Text, #[multipart(rename = "field1")] field2: Text, field3: Text, } async fn test_field_renaming_route(form: MultipartForm) -> impl Responder { assert_eq!(&*form.field1, "renamed"); assert_eq!(&*form.field2, "field1"); assert_eq!(&*form.field3, "field3"); HttpResponse::Ok().finish() } #[actix_rt::test] async fn test_field_renaming() { let srv = actix_test::start(|| App::new().route("/", web::post().to(test_field_renaming_route))); let mut form = multipart::Form::default(); form.add_text("renamed", "renamed"); form.add_text("field1", "field1"); form.add_text("field3", "field3"); let response = send_form(&srv, form, "/").await; assert_eq!(response.status(), StatusCode::OK); } /// Test the `deny_unknown_fields` struct attribute. #[derive(MultipartForm)] #[multipart(deny_unknown_fields)] struct TestDenyUnknown {} #[derive(MultipartForm)] struct TestAllowUnknown {} async fn test_deny_unknown_route(_: MultipartForm) -> impl Responder { HttpResponse::Ok().finish() } async fn test_allow_unknown_route(_: MultipartForm) -> impl Responder { HttpResponse::Ok().finish() } #[actix_rt::test] async fn test_deny_unknown() { let srv = actix_test::start(|| { App::new() .route("/deny", web::post().to(test_deny_unknown_route)) .route("/allow", web::post().to(test_allow_unknown_route)) }); let mut form = multipart::Form::default(); form.add_text("unknown", "value"); let response = send_form(&srv, form, "/deny").await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); let mut form = multipart::Form::default(); form.add_text("unknown", "value"); let response = send_form(&srv, form, "/allow").await; assert_eq!(response.status(), StatusCode::OK); } /// Test the `duplicate_field` struct attribute. #[derive(MultipartForm)] #[multipart(duplicate_field = "deny")] struct TestDuplicateDeny { _field: Text, } #[derive(MultipartForm)] #[multipart(duplicate_field = "replace")] struct TestDuplicateReplace { field: Text, } #[derive(MultipartForm)] #[multipart(duplicate_field = "ignore")] struct TestDuplicateIgnore { field: Text, } async fn test_duplicate_deny_route(_: MultipartForm) -> impl Responder { HttpResponse::Ok().finish() } async fn test_duplicate_replace_route( form: MultipartForm, ) -> impl Responder { assert_eq!(&*form.field, "second_value"); HttpResponse::Ok().finish() } async fn test_duplicate_ignore_route( form: MultipartForm, ) -> impl Responder { assert_eq!(&*form.field, "first_value"); HttpResponse::Ok().finish() } #[actix_rt::test] async fn test_duplicate_field() { let srv = actix_test::start(|| { App::new() .route("/deny", web::post().to(test_duplicate_deny_route)) .route("/replace", web::post().to(test_duplicate_replace_route)) .route("/ignore", web::post().to(test_duplicate_ignore_route)) }); let mut form = multipart::Form::default(); form.add_text("_field", "first_value"); form.add_text("_field", "second_value"); let response = send_form(&srv, form, "/deny").await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); let mut form = multipart::Form::default(); form.add_text("field", "first_value"); form.add_text("field", "second_value"); let response = send_form(&srv, form, "/replace").await; assert_eq!(response.status(), StatusCode::OK); let mut form = multipart::Form::default(); form.add_text("field", "first_value"); form.add_text("field", "second_value"); let response = send_form(&srv, form, "/ignore").await; assert_eq!(response.status(), StatusCode::OK); } /// Test the Limits. #[derive(MultipartForm)] struct TestMemoryUploadLimits { field: Bytes, } #[derive(MultipartForm)] struct TestFileUploadLimits { field: TempFile, } async fn test_upload_limits_memory( form: MultipartForm, ) -> impl Responder { assert!(!form.field.data.is_empty()); HttpResponse::Ok().finish() } async fn test_upload_limits_file(form: MultipartForm) -> impl Responder { assert!(form.field.size > 0); HttpResponse::Ok().finish() } #[actix_rt::test] async fn test_memory_limits() { let srv = actix_test::start(|| { App::new() .route("/text", web::post().to(test_upload_limits_memory)) .route("/file", web::post().to(test_upload_limits_file)) .app_data( MultipartFormConfig::default() .memory_limit(20) .total_limit(usize::MAX), ) }); // Exceeds the 20 byte memory limit let mut form = multipart::Form::default(); form.add_text("field", "this string is 28 bytes long"); let response = send_form(&srv, form, "/text").await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); // Memory limit should not apply when the data is being streamed to disk let mut form = multipart::Form::default(); form.add_text("field", "this string is 28 bytes long"); let response = send_form(&srv, form, "/file").await; assert_eq!(response.status(), StatusCode::OK); } #[actix_rt::test] async fn test_total_limit() { let srv = actix_test::start(|| { App::new() .route("/text", web::post().to(test_upload_limits_memory)) .route("/file", web::post().to(test_upload_limits_file)) .app_data( MultipartFormConfig::default() .memory_limit(usize::MAX) .total_limit(20), ) }); // Within the 20 byte limit let mut form = multipart::Form::default(); form.add_text("field", "7 bytes"); let response = send_form(&srv, form, "/text").await; assert_eq!(response.status(), StatusCode::OK); // Exceeds the 20 byte overall limit let mut form = multipart::Form::default(); form.add_text("field", "this string is 28 bytes long"); let response = send_form(&srv, form, "/text").await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); // Exceeds the 20 byte overall limit let mut form = multipart::Form::default(); form.add_text("field", "this string is 28 bytes long"); let response = send_form(&srv, form, "/file").await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[derive(MultipartForm)] struct TestFieldLevelLimits { #[multipart(limit = "30B")] field: Vec, } async fn test_field_level_limits_route( form: MultipartForm, ) -> impl Responder { assert!(!form.field.is_empty()); HttpResponse::Ok().finish() } #[actix_rt::test] async fn test_field_level_limits() { let srv = actix_test::start(|| { App::new() .route("/", web::post().to(test_field_level_limits_route)) .app_data( MultipartFormConfig::default() .memory_limit(usize::MAX) .total_limit(usize::MAX), ) }); // Within the 30 byte limit let mut form = multipart::Form::default(); form.add_text("field", "this string is 28 bytes long"); let response = send_form(&srv, form, "/").await; assert_eq!(response.status(), StatusCode::OK); // Exceeds the the 30 byte limit let mut form = multipart::Form::default(); form.add_text("field", "this string is more than 30 bytes long"); let response = send_form(&srv, form, "/").await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); // Total of values (14 bytes) is within 30 byte limit for "field" let mut form = multipart::Form::default(); form.add_text("field", "7 bytes"); form.add_text("field", "7 bytes"); let response = send_form(&srv, form, "/").await; assert_eq!(response.status(), StatusCode::OK); // Total of values exceeds 30 byte limit for "field" let mut form = multipart::Form::default(); form.add_text("field", "this string is 28 bytes long"); form.add_text("field", "this string is 28 bytes long"); let response = send_form(&srv, form, "/").await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[actix_rt::test] async fn non_multipart_form_data() { #[derive(MultipartForm)] struct TestNonMultipartFormData { #[allow(unused)] #[multipart(limit = "30B")] foo: Text, } async fn non_multipart_form_data_route( _form: MultipartForm, ) -> String { unreachable!("request is sent with multipart/mixed"); } let srv = actix_test::start(|| { App::new().route("/", web::post().to(non_multipart_form_data_route)) }); let mut form = multipart::Form::default(); form.add_text("foo", "foo"); // mangle content-type, keeping the boundary let ct = form.content_type().replacen("/form-data", "/mixed", 1); let res = Client::default() .post(srv.url("/")) .content_type(ct) .send_body(multipart::Body::from(form)) .await .unwrap(); assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); } #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: Connect(Disconnected)")] #[actix_web::test] async fn field_try_next_panic() { #[derive(Debug)] struct NullSink; impl<'t> FieldReader<'t> for NullSink { type Future = LocalBoxFuture<'t, Result>; fn read_field( _: &'t HttpRequest, mut field: Field, _limits: &'t mut Limits, ) -> Self::Future { Box::pin(async move { // exhaust field stream while let Some(_chunk) = field.try_next().await? {} // poll again, crash let _post = field.try_next().await; Ok(Self) }) } } #[allow(dead_code)] #[derive(MultipartForm)] struct NullSinkForm { foo: NullSink, } async fn null_sink(_form: MultipartForm) -> impl Responder { "unreachable" } let srv = actix_test::start(|| App::new().service(Resource::new("/").post(null_sink))); let mut form = multipart::Form::default(); form.add_text("foo", "data is not important to this test"); // panics with Err(Connect(Disconnected)) due to form NullSink panic let _res = send_form(&srv, form, "/").await; } } actix-multipart-0.7.2/src/form/tempfile.rs000064400000000000000000000135631046102023000166760ustar 00000000000000//! Writes a field to a temporary file on disk. use std::{ io, path::{Path, PathBuf}, sync::Arc, }; use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError}; use derive_more::{Display, Error}; use futures_core::future::LocalBoxFuture; use futures_util::TryStreamExt as _; use mime::Mime; use tempfile::NamedTempFile; use tokio::io::AsyncWriteExt; use super::FieldErrorHandler; use crate::{ form::{FieldReader, Limits}, Field, MultipartError, }; /// Write the field to a temporary file on disk. #[derive(Debug)] pub struct TempFile { /// The temporary file on disk. pub file: NamedTempFile, /// The value of the `content-type` header. pub content_type: Option, /// The `filename` value in the `content-disposition` header. pub file_name: Option, /// The size in bytes of the file. pub size: usize, } impl<'t> FieldReader<'t> for TempFile { type Future = LocalBoxFuture<'t, Result>; fn read_field(req: &'t HttpRequest, mut field: Field, limits: &'t mut Limits) -> Self::Future { Box::pin(async move { let config = TempFileConfig::from_req(req); let mut size = 0; let file = config.create_tempfile().map_err(|err| { config.map_error(req, &field.form_field_name, TempFileError::FileIo(err)) })?; let mut file_async = tokio::fs::File::from_std(file.reopen().map_err(|err| { config.map_error(req, &field.form_field_name, TempFileError::FileIo(err)) })?); while let Some(chunk) = field.try_next().await? { limits.try_consume_limits(chunk.len(), false)?; size += chunk.len(); file_async.write_all(chunk.as_ref()).await.map_err(|err| { config.map_error(req, &field.form_field_name, TempFileError::FileIo(err)) })?; } file_async.flush().await.map_err(|err| { config.map_error(req, &field.form_field_name, TempFileError::FileIo(err)) })?; Ok(TempFile { file, content_type: field.content_type().map(ToOwned::to_owned), file_name: field .content_disposition() .expect("multipart form fields should have a content-disposition header") .get_filename() .map(ToOwned::to_owned), size, }) }) } } #[derive(Debug, Display, Error)] #[non_exhaustive] pub enum TempFileError { /// File I/O Error #[display(fmt = "File I/O error: {}", _0)] FileIo(std::io::Error), } impl ResponseError for TempFileError { fn status_code(&self) -> StatusCode { StatusCode::INTERNAL_SERVER_ERROR } } /// Configuration for the [`TempFile`] field reader. #[derive(Clone)] pub struct TempFileConfig { err_handler: FieldErrorHandler, directory: Option, } impl TempFileConfig { fn create_tempfile(&self) -> io::Result { if let Some(ref dir) = self.directory { NamedTempFile::new_in(dir) } else { NamedTempFile::new() } } } impl TempFileConfig { /// Sets custom error handler. pub fn error_handler(mut self, f: F) -> Self where F: Fn(TempFileError, &HttpRequest) -> Error + Send + Sync + 'static, { self.err_handler = Some(Arc::new(f)); self } /// Extracts payload config from app data. Check both `T` and `Data`, in that order, and fall /// back to the default payload config. fn from_req(req: &HttpRequest) -> &Self { req.app_data::() .or_else(|| req.app_data::>().map(|d| d.as_ref())) .unwrap_or(&DEFAULT_CONFIG) } fn map_error(&self, req: &HttpRequest, field_name: &str, err: TempFileError) -> MultipartError { let source = if let Some(ref err_handler) = self.err_handler { (err_handler)(err, req) } else { err.into() }; MultipartError::Field { name: field_name.to_owned(), source, } } /// Sets the directory that temp files will be created in. /// /// The default temporary file location is platform dependent. pub fn directory(mut self, dir: impl AsRef) -> Self { self.directory = Some(dir.as_ref().to_owned()); self } } const DEFAULT_CONFIG: TempFileConfig = TempFileConfig { err_handler: None, directory: None, }; impl Default for TempFileConfig { fn default() -> Self { DEFAULT_CONFIG } } #[cfg(test)] mod tests { use std::io::{Cursor, Read}; use actix_multipart_rfc7578::client::multipart; use actix_web::{http::StatusCode, web, App, HttpResponse, Responder}; use crate::form::{tempfile::TempFile, tests::send_form, MultipartForm}; #[derive(MultipartForm)] struct FileForm { file: TempFile, } async fn test_file_route(form: MultipartForm) -> impl Responder { let mut form = form.into_inner(); let mut contents = String::new(); form.file.file.read_to_string(&mut contents).unwrap(); assert_eq!(contents, "Hello, world!"); assert_eq!(form.file.file_name.unwrap(), "testfile.txt"); assert_eq!(form.file.content_type.unwrap(), mime::TEXT_PLAIN); HttpResponse::Ok().finish() } #[actix_rt::test] async fn test_file_upload() { let srv = actix_test::start(|| App::new().route("/", web::post().to(test_file_route))); let mut form = multipart::Form::default(); let bytes = Cursor::new("Hello, world!"); form.add_reader_file_with_mime("file", bytes, "testfile.txt", mime::TEXT_PLAIN); let response = send_form(&srv, form, "/").await; assert_eq!(response.status(), StatusCode::OK); } } actix-multipart-0.7.2/src/form/text.rs000064400000000000000000000137651046102023000160610ustar 00000000000000//! Deserializes a field from plain text. use std::{str, sync::Arc}; use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError}; use derive_more::{Deref, DerefMut, Display, Error}; use futures_core::future::LocalBoxFuture; use serde::de::DeserializeOwned; use super::FieldErrorHandler; use crate::{ form::{bytes::Bytes, FieldReader, Limits}, Field, MultipartError, }; /// Deserialize from plain text. /// /// Internally this uses [`serde_plain`] for deserialization, which supports primitive types /// including strings, numbers, and simple enums. #[derive(Debug, Deref, DerefMut)] pub struct Text(pub T); impl Text { /// Unwraps into inner value. pub fn into_inner(self) -> T { self.0 } } impl<'t, T> FieldReader<'t> for Text where T: DeserializeOwned + 'static, { type Future = LocalBoxFuture<'t, Result>; fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future { Box::pin(async move { let config = TextConfig::from_req(req); if config.validate_content_type { let valid = if let Some(mime) = field.content_type() { mime.subtype() == mime::PLAIN || mime.suffix() == Some(mime::PLAIN) } else { // https://datatracker.ietf.org/doc/html/rfc7578#section-4.4 // content type defaults to text/plain, so None should be considered valid true }; if !valid { return Err(MultipartError::Field { name: field.form_field_name, source: config.map_error(req, TextError::ContentType), }); } } let form_field_name = field.form_field_name.clone(); let bytes = Bytes::read_field(req, field, limits).await?; let text = str::from_utf8(&bytes.data).map_err(|err| MultipartError::Field { name: form_field_name.clone(), source: config.map_error(req, TextError::Utf8Error(err)), })?; Ok(Text(serde_plain::from_str(text).map_err(|err| { MultipartError::Field { name: form_field_name, source: config.map_error(req, TextError::Deserialize(err)), } })?)) }) } } #[derive(Debug, Display, Error)] #[non_exhaustive] pub enum TextError { /// UTF-8 decoding error. #[display(fmt = "UTF-8 decoding error: {}", _0)] Utf8Error(str::Utf8Error), /// Deserialize error. #[display(fmt = "Plain text deserialize error: {}", _0)] Deserialize(serde_plain::Error), /// Content type error. #[display(fmt = "Content type error")] ContentType, } impl ResponseError for TextError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } /// Configuration for the [`Text`] field reader. #[derive(Clone)] pub struct TextConfig { err_handler: FieldErrorHandler, validate_content_type: bool, } impl TextConfig { /// Sets custom error handler. pub fn error_handler(mut self, f: F) -> Self where F: Fn(TextError, &HttpRequest) -> Error + Send + Sync + 'static, { self.err_handler = Some(Arc::new(f)); self } /// Extracts payload config from app data. Check both `T` and `Data`, in that order, and fall /// back to the default payload config. fn from_req(req: &HttpRequest) -> &Self { req.app_data::() .or_else(|| req.app_data::>().map(|d| d.as_ref())) .unwrap_or(&DEFAULT_CONFIG) } fn map_error(&self, req: &HttpRequest, err: TextError) -> Error { if let Some(ref err_handler) = self.err_handler { (err_handler)(err, req) } else { err.into() } } /// Sets whether or not the field must have a valid `Content-Type` header to be parsed. /// /// Note that an empty `Content-Type` is also accepted, as the multipart specification defines /// `text/plain` as the default for text fields. pub fn validate_content_type(mut self, validate_content_type: bool) -> Self { self.validate_content_type = validate_content_type; self } } const DEFAULT_CONFIG: TextConfig = TextConfig { err_handler: None, validate_content_type: true, }; impl Default for TextConfig { fn default() -> Self { DEFAULT_CONFIG } } #[cfg(test)] mod tests { use std::io::Cursor; use actix_multipart_rfc7578::client::multipart; use actix_web::{http::StatusCode, web, App, HttpResponse, Responder}; use crate::form::{ tests::send_form, text::{Text, TextConfig}, MultipartForm, }; #[derive(MultipartForm)] struct TextForm { number: Text, } async fn test_text_route(form: MultipartForm) -> impl Responder { assert_eq!(*form.number, 1025); HttpResponse::Ok().finish() } #[actix_rt::test] async fn test_content_type_validation() { let srv = actix_test::start(|| { App::new() .route("/", web::post().to(test_text_route)) .app_data(TextConfig::default().validate_content_type(true)) }); // Deny because wrong content type let bytes = Cursor::new("1025"); let mut form = multipart::Form::default(); form.add_reader_file_with_mime("number", bytes, "", mime::APPLICATION_OCTET_STREAM); let response = send_form(&srv, form, "/").await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); // Allow because correct content type let bytes = Cursor::new("1025"); let mut form = multipart::Form::default(); form.add_reader_file_with_mime("number", bytes, "", mime::TEXT_PLAIN); let response = send_form(&srv, form, "/").await; assert_eq!(response.status(), StatusCode::OK); } } actix-multipart-0.7.2/src/lib.rs000064400000000000000000000033101046102023000146610ustar 00000000000000//! Multipart form support for Actix Web. //! //! # Examples //! //! ```no_run //! use actix_web::{post, App, HttpServer, Responder}; //! //! use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm}; //! use serde::Deserialize; //! //! #[derive(Debug, Deserialize)] //! struct Metadata { //! name: String, //! } //! //! #[derive(Debug, MultipartForm)] //! struct UploadForm { //! #[multipart(limit = "100MB")] //! file: TempFile, //! json: MpJson, //! } //! //! #[post("/videos")] //! pub async fn post_video(MultipartForm(form): MultipartForm) -> impl Responder { //! format!( //! "Uploaded file {}, with size: {}", //! form.json.name, form.file.size //! ) //! } //! //! #[actix_web::main] //! async fn main() -> std::io::Result<()> { //! HttpServer::new(move || App::new().service(post_video)) //! .bind(("127.0.0.1", 8080))? //! .run() //! .await //! } //! ``` //! //! cURL request: //! //! ```sh //! curl -v --request POST \ //! --url http://localhost:8080/videos \ //! -F 'json={"name": "Cargo.lock"};type=application/json' \ //! -F file=@./Cargo.lock //! ``` #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] // This allows us to use the actix_multipart_derive within this crate's tests #[cfg(test)] extern crate self as actix_multipart; mod error; mod extractor; pub(crate) mod field; pub mod form; mod multipart; pub(crate) mod payload; pub(crate) mod safety; pub mod test; pub use self::{ error::Error as MultipartError, field::{Field, LimitExceeded}, multipart::Multipart, }; actix-multipart-0.7.2/src/multipart.rs000064400000000000000000001004401046102023000161360ustar 00000000000000//! Multipart response payload support. use std::{ cell::RefCell, pin::Pin, rc::Rc, task::{Context, Poll}, }; use actix_web::{ dev, error::{ParseError, PayloadError}, http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}, web::Bytes, HttpRequest, }; use futures_core::stream::Stream; use mime::Mime; use crate::{ error::Error, field::InnerField, payload::{PayloadBuffer, PayloadRef}, safety::Safety, Field, }; const MAX_HEADERS: usize = 32; /// The server-side implementation of `multipart/form-data` requests. /// /// This will parse the incoming stream into `MultipartItem` instances via its `Stream` /// implementation. `MultipartItem::Field` contains multipart field. `MultipartItem::Multipart` is /// used for nested multipart streams. pub struct Multipart { flow: Flow, safety: Safety, } enum Flow { InFlight(Inner), /// Error container is Some until an error is returned out of the flow. Error(Option), } impl Multipart { /// Creates multipart instance from parts. pub fn new(headers: &HeaderMap, stream: S) -> Self where S: Stream> + 'static, { match Self::find_ct_and_boundary(headers) { Ok((ct, boundary)) => Self::from_ct_and_boundary(ct, boundary, stream), Err(err) => Self::from_error(err), } } /// Creates multipart instance from parts. pub(crate) fn from_req(req: &HttpRequest, payload: &mut dev::Payload) -> Self { match Self::find_ct_and_boundary(req.headers()) { Ok((ct, boundary)) => Self::from_ct_and_boundary(ct, boundary, payload.take()), Err(err) => Self::from_error(err), } } /// Extract Content-Type and boundary info from headers. pub(crate) fn find_ct_and_boundary(headers: &HeaderMap) -> Result<(Mime, String), Error> { let content_type = headers .get(&header::CONTENT_TYPE) .ok_or(Error::ContentTypeMissing)? .to_str() .ok() .and_then(|content_type| content_type.parse::().ok()) .ok_or(Error::ContentTypeParse)?; if content_type.type_() != mime::MULTIPART { return Err(Error::ContentTypeIncompatible); } let boundary = content_type .get_param(mime::BOUNDARY) .ok_or(Error::BoundaryMissing)? .as_str() .to_owned(); Ok((content_type, boundary)) } /// Constructs a new multipart reader from given Content-Type, boundary, and stream. pub(crate) fn from_ct_and_boundary(ct: Mime, boundary: String, stream: S) -> Multipart where S: Stream> + 'static, { Multipart { safety: Safety::new(), flow: Flow::InFlight(Inner { payload: PayloadRef::new(PayloadBuffer::new(stream)), content_type: ct, boundary, state: State::FirstBoundary, item: Item::None, }), } } /// Constructs a new multipart reader from given `MultipartError`. pub(crate) fn from_error(err: Error) -> Multipart { Multipart { flow: Flow::Error(Some(err)), safety: Safety::new(), } } /// Return requests parsed Content-Type or raise the stored error. pub(crate) fn content_type_or_bail(&mut self) -> Result { match self.flow { Flow::InFlight(ref inner) => Ok(inner.content_type.clone()), Flow::Error(ref mut err) => Err(err .take() .expect("error should not be taken after it was returned")), } } } impl Stream for Multipart { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); match this.flow { Flow::InFlight(ref mut inner) => { if let Some(mut buffer) = inner.payload.get_mut(&this.safety) { // check safety and poll read payload to buffer. buffer.poll_stream(cx)?; } else if !this.safety.is_clean() { // safety violation return Poll::Ready(Some(Err(Error::NotConsumed))); } else { return Poll::Pending; } inner.poll(&this.safety, cx) } Flow::Error(ref mut err) => Poll::Ready(Some(Err(err .take() .expect("Multipart polled after finish")))), } } } #[derive(PartialEq, Debug)] enum State { /// Skip data until first boundary. FirstBoundary, /// Reading boundary. Boundary, /// Reading Headers. Headers, /// Stream EOF. Eof, } enum Item { None, Field(Rc>), } struct Inner { /// Request's payload stream & buffer. payload: PayloadRef, /// Request's Content-Type. /// /// Guaranteed to have "multipart" top-level media type, i.e., `multipart/*`. content_type: Mime, /// Field boundary. boundary: String, state: State, item: Item, } impl Inner { fn read_field_headers(payload: &mut PayloadBuffer) -> Result, Error> { match payload.read_until(b"\r\n\r\n")? { None => { if payload.eof { Err(Error::Incomplete) } else { Ok(None) } } Some(bytes) => { let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS]; match httparse::parse_headers(&bytes, &mut hdrs).map_err(ParseError::from)? { httparse::Status::Complete((_, hdrs)) => { // convert headers let mut headers = HeaderMap::with_capacity(hdrs.len()); for h in hdrs { let name = HeaderName::try_from(h.name).map_err(|_| ParseError::Header)?; let value = HeaderValue::try_from(h.value).map_err(|_| ParseError::Header)?; headers.append(name, value); } Ok(Some(headers)) } httparse::Status::Partial => Err(ParseError::Header.into()), } } } } /// Reads a field boundary from the payload buffer (and discards it). /// /// Reads "in-between" and "final" boundaries. E.g. for boundary = "foo": /// /// ```plain /// --foo <-- in-between fields /// --foo-- <-- end of request body, should be followed by EOF /// ``` /// /// Returns: /// /// - `Ok(Some(true))` - final field boundary read (EOF) /// - `Ok(Some(false))` - field boundary read /// - `Ok(None)` - boundary not found, more data needs reading /// - `Err(BoundaryMissing)` - multipart boundary is missing fn read_boundary(payload: &mut PayloadBuffer, boundary: &str) -> Result, Error> { // TODO: need to read epilogue let chunk = match payload.readline_or_eof()? { // TODO: this might be okay as a let Some() else return Ok(None) None => return Ok(payload.eof.then_some(true)), Some(chunk) => chunk, }; const BOUNDARY_MARKER: &[u8] = b"--"; const LINE_BREAK: &[u8] = b"\r\n"; let boundary_len = boundary.len(); if chunk.len() < boundary_len + 2 + 2 || !chunk.starts_with(BOUNDARY_MARKER) || &chunk[2..boundary_len + 2] != boundary.as_bytes() { return Err(Error::BoundaryMissing); } // chunk facts: // - long enough to contain boundary + 2 markers or 1 marker and line-break // - starts with boundary marker // - chunk contains correct boundary if &chunk[boundary_len + 2..] == LINE_BREAK { // boundary is followed by line-break, indicating more fields to come return Ok(Some(false)); } // boundary is followed by marker if &chunk[boundary_len + 2..boundary_len + 4] == BOUNDARY_MARKER && ( // chunk is exactly boundary len + 2 markers chunk.len() == boundary_len + 2 + 2 // final boundary is allowed to end with a line-break || &chunk[boundary_len + 4..] == LINE_BREAK ) { return Ok(Some(true)); } Err(Error::BoundaryMissing) } fn skip_until_boundary( payload: &mut PayloadBuffer, boundary: &str, ) -> Result, Error> { let mut eof = false; loop { match payload.readline()? { Some(chunk) => { if chunk.is_empty() { return Err(Error::BoundaryMissing); } if chunk.len() < boundary.len() { continue; } if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes() { break; } else { if chunk.len() < boundary.len() + 2 { continue; } let b: &[u8] = boundary.as_ref(); if &chunk[..boundary.len()] == b && &chunk[boundary.len()..boundary.len() + 2] == b"--" { eof = true; break; } } } None => { return if payload.eof { Err(Error::Incomplete) } else { Ok(None) }; } } } Ok(Some(eof)) } fn poll(&mut self, safety: &Safety, cx: &Context<'_>) -> Poll>> { if self.state == State::Eof { Poll::Ready(None) } else { // release field loop { // Nested multipart streams of fields has to be consumed // before switching to next if safety.current() { let stop = match self.item { Item::Field(ref mut field) => match field.borrow_mut().poll(safety) { Poll::Pending => return Poll::Pending, Poll::Ready(Some(Ok(_))) => continue, Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), Poll::Ready(None) => true, }, Item::None => false, }; if stop { self.item = Item::None; } if let Item::None = self.item { break; } } } let field_headers = if let Some(mut payload) = self.payload.get_mut(safety) { match self.state { // read until first boundary State::FirstBoundary => { match Inner::skip_until_boundary(&mut payload, &self.boundary)? { None => return Poll::Pending, Some(eof) => { if eof { self.state = State::Eof; return Poll::Ready(None); } else { self.state = State::Headers; } } } } // read boundary State::Boundary => match Inner::read_boundary(&mut payload, &self.boundary)? { None => return Poll::Pending, Some(eof) => { if eof { self.state = State::Eof; return Poll::Ready(None); } else { self.state = State::Headers; } } }, _ => {} } // read field headers for next field if self.state == State::Headers { if let Some(headers) = Inner::read_field_headers(&mut payload)? { self.state = State::Boundary; headers } else { return Poll::Pending; } } else { unreachable!() } } else { log::debug!("NotReady: field is in flight"); return Poll::Pending; }; let field_content_disposition = field_headers .get(&header::CONTENT_DISPOSITION) .and_then(|cd| ContentDisposition::from_raw(cd).ok()) .filter(|content_disposition| { matches!( content_disposition.disposition, header::DispositionType::FormData, ) }); let form_field_name = if self.content_type.subtype() == mime::FORM_DATA { // According to RFC 7578 ยง4.2, which relates to "multipart/form-data" requests // specifically, fields must have a Content-Disposition header, its disposition // type must be set as "form-data", and it must have a name parameter. let Some(cd) = &field_content_disposition else { return Poll::Ready(Some(Err(Error::ContentDispositionMissing))); }; let Some(field_name) = cd.get_name() else { return Poll::Ready(Some(Err(Error::ContentDispositionNameMissing))); }; Some(field_name.to_owned()) } else { None }; // TODO: check out other multipart/* RFCs for specific requirements let field_content_type: Option = field_headers .get(&header::CONTENT_TYPE) .and_then(|ct| ct.to_str().ok()) .and_then(|ct| ct.parse().ok()); self.state = State::Boundary; // nested multipart stream is not supported if let Some(mime) = &field_content_type { if mime.type_() == mime::MULTIPART { return Poll::Ready(Some(Err(Error::Nested))); } } let field_inner = InnerField::new_in_rc(self.payload.clone(), self.boundary.clone(), &field_headers)?; self.item = Item::Field(Rc::clone(&field_inner)); Poll::Ready(Some(Ok(Field::new( field_content_type, field_content_disposition, form_field_name, field_headers, safety.clone(cx), field_inner, )))) } } } impl Drop for Inner { fn drop(&mut self) { // InnerMultipartItem::Field has to be dropped first because of Safety. self.item = Item::None; } } #[cfg(test)] mod tests { use std::time::Duration; use actix_http::h1; use actix_web::{ http::header::{DispositionParam, DispositionType}, rt, test::TestRequest, web::{BufMut as _, BytesMut}, FromRequest, }; use assert_matches::assert_matches; use futures_test::stream::StreamTestExt as _; use futures_util::{future::lazy, stream, StreamExt as _}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; use super::*; const BOUNDARY: &str = "abbc761f78ff4d7cb7573b5a23f96ef0"; #[actix_rt::test] async fn test_boundary() { let headers = HeaderMap::new(); match Multipart::find_ct_and_boundary(&headers) { Err(Error::ContentTypeMissing) => {} _ => unreachable!("should not happen"), } let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, header::HeaderValue::from_static("test"), ); match Multipart::find_ct_and_boundary(&headers) { Err(Error::ContentTypeParse) => {} _ => unreachable!("should not happen"), } let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, header::HeaderValue::from_static("multipart/mixed"), ); match Multipart::find_ct_and_boundary(&headers) { Err(Error::BoundaryMissing) => {} _ => unreachable!("should not happen"), } let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, header::HeaderValue::from_static( "multipart/mixed; boundary=\"5c02368e880e436dab70ed54e1c58209\"", ), ); assert_eq!( Multipart::find_ct_and_boundary(&headers).unwrap().1, "5c02368e880e436dab70ed54e1c58209", ); } fn create_stream() -> ( mpsc::UnboundedSender>, impl Stream>, ) { let (tx, rx) = mpsc::unbounded_channel(); ( tx, UnboundedReceiverStream::new(rx).map(|res| res.map_err(|_| panic!())), ) } fn create_simple_request_with_header() -> (Bytes, HeaderMap) { let (body, headers) = crate::test::create_form_data_payload_and_headers_with_boundary( BOUNDARY, "file", Some("fn.txt".to_owned()), Some(mime::TEXT_PLAIN_UTF_8), Bytes::from_static(b"data"), ); let mut buf = BytesMut::with_capacity(body.len() + 14); // add junk before form to test pre-boundary data rejection buf.put("testasdadsad\r\n".as_bytes()); buf.put(body); (buf.freeze(), headers) } // TODO: use test utility when multi-file support is introduced fn create_double_request_with_header() -> (Bytes, HeaderMap) { let bytes = Bytes::from( "testasdadsad\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ test\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ data\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", ); let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, header::HeaderValue::from_static( "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", ), ); (bytes, headers) } #[actix_rt::test] async fn test_multipart_no_end_crlf() { let (sender, payload) = create_stream(); let (mut bytes, headers) = create_double_request_with_header(); let bytes_stripped = bytes.split_to(bytes.len()); // strip crlf sender.send(Ok(bytes_stripped)).unwrap(); drop(sender); // eof let mut multipart = Multipart::new(&headers, payload); match multipart.next().await.unwrap() { Ok(_) => {} _ => unreachable!(), } match multipart.next().await.unwrap() { Ok(_) => {} _ => unreachable!(), } match multipart.next().await { None => {} _ => unreachable!(), } } #[actix_rt::test] async fn test_multipart() { let (sender, payload) = create_stream(); let (bytes, headers) = create_double_request_with_header(); sender.send(Ok(bytes)).unwrap(); let mut multipart = Multipart::new(&headers, payload); match multipart.next().await { Some(Ok(mut field)) => { let cd = field.content_disposition().unwrap(); assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); match field.next().await.unwrap() { Ok(chunk) => assert_eq!(chunk, "test"), _ => unreachable!(), } match field.next().await { None => {} _ => unreachable!(), } } _ => unreachable!(), } match multipart.next().await.unwrap() { Ok(mut field) => { assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); match field.next().await { Some(Ok(chunk)) => assert_eq!(chunk, "data"), _ => unreachable!(), } match field.next().await { None => {} _ => unreachable!(), } } _ => unreachable!(), } match multipart.next().await { None => {} _ => unreachable!(), } } // Loops, collecting all bytes until end-of-field async fn get_whole_field(field: &mut Field) -> BytesMut { let mut b = BytesMut::new(); loop { match field.next().await { Some(Ok(chunk)) => b.extend_from_slice(&chunk), None => return b, _ => unreachable!(), } } } #[actix_rt::test] async fn test_stream() { let (bytes, headers) = create_double_request_with_header(); let payload = stream::iter(bytes) .map(|byte| Ok(Bytes::copy_from_slice(&[byte]))) .interleave_pending(); let mut multipart = Multipart::new(&headers, payload); match multipart.next().await.unwrap() { Ok(mut field) => { let cd = field.content_disposition().unwrap(); assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); assert_eq!(get_whole_field(&mut field).await, "test"); } _ => unreachable!(), } match multipart.next().await { Some(Ok(mut field)) => { assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); assert_eq!(get_whole_field(&mut field).await, "data"); } _ => unreachable!(), } match multipart.next().await { None => {} _ => unreachable!(), } } #[actix_rt::test] async fn test_basic() { let (_, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(payload.buf.len(), 0); lazy(|cx| payload.poll_stream(cx)).await.unwrap(); assert_eq!(None, payload.read_max(1).unwrap()); } #[actix_rt::test] async fn test_eof() { let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_max(4).unwrap()); sender.feed_data(Bytes::from("data")); sender.feed_eof(); lazy(|cx| payload.poll_stream(cx)).await.unwrap(); assert_eq!(Some(Bytes::from("data")), payload.read_max(4).unwrap()); assert_eq!(payload.buf.len(), 0); assert!(payload.read_max(1).is_err()); assert!(payload.eof); } #[actix_rt::test] async fn test_err() { let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_max(1).unwrap()); sender.set_error(PayloadError::Incomplete(None)); lazy(|cx| payload.poll_stream(cx)).await.err().unwrap(); } #[actix_rt::test] async fn read_max() { let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); sender.feed_data(Bytes::from("line1")); sender.feed_data(Bytes::from("line2")); lazy(|cx| payload.poll_stream(cx)).await.unwrap(); assert_eq!(payload.buf.len(), 10); assert_eq!(Some(Bytes::from("line1")), payload.read_max(5).unwrap()); assert_eq!(payload.buf.len(), 5); assert_eq!(Some(Bytes::from("line2")), payload.read_max(5).unwrap()); assert_eq!(payload.buf.len(), 0); } #[actix_rt::test] async fn read_exactly() { let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_exact(2)); sender.feed_data(Bytes::from("line1")); sender.feed_data(Bytes::from("line2")); lazy(|cx| payload.poll_stream(cx)).await.unwrap(); assert_eq!(Some(Bytes::from_static(b"li")), payload.read_exact(2)); assert_eq!(payload.buf.len(), 8); assert_eq!(Some(Bytes::from_static(b"ne1l")), payload.read_exact(4)); assert_eq!(payload.buf.len(), 4); } #[actix_rt::test] async fn read_until() { let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_until(b"ne").unwrap()); sender.feed_data(Bytes::from("line1")); sender.feed_data(Bytes::from("line2")); lazy(|cx| payload.poll_stream(cx)).await.unwrap(); assert_eq!( Some(Bytes::from("line")), payload.read_until(b"ne").unwrap() ); assert_eq!(payload.buf.len(), 6); assert_eq!( Some(Bytes::from("1line2")), payload.read_until(b"2").unwrap() ); assert_eq!(payload.buf.len(), 0); } #[actix_rt::test] async fn test_multipart_from_error() { let err = Error::ContentTypeMissing; let mut multipart = Multipart::from_error(err); assert!(multipart.next().await.unwrap().is_err()) } #[actix_rt::test] async fn test_multipart_from_boundary() { let (_, payload) = create_stream(); let (_, headers) = create_simple_request_with_header(); let (ct, boundary) = Multipart::find_ct_and_boundary(&headers).unwrap(); let _ = Multipart::from_ct_and_boundary(ct, boundary, payload); } #[actix_rt::test] async fn test_multipart_payload_consumption() { // with sample payload and HttpRequest with no headers let (_, inner_payload) = h1::Payload::create(false); let mut payload = actix_web::dev::Payload::from(inner_payload); let req = TestRequest::default().to_http_request(); // multipart should generate an error let mut mp = Multipart::from_request(&req, &mut payload).await.unwrap(); assert!(mp.next().await.unwrap().is_err()); // and should not consume the payload match payload { actix_web::dev::Payload::H1 { .. } => {} //expected _ => unreachable!(), } } #[actix_rt::test] async fn no_content_disposition_form_data() { let bytes = Bytes::from( "testasdadsad\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ Content-Type: text/plain; charset=utf-8\r\n\ Content-Length: 4\r\n\ \r\n\ test\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", ); let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, header::HeaderValue::from_static( "multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", ), ); let payload = stream::iter(bytes) .map(|byte| Ok(Bytes::copy_from_slice(&[byte]))) .interleave_pending(); let mut multipart = Multipart::new(&headers, payload); let res = multipart.next().await.unwrap(); assert_matches!( res.expect_err( "according to RFC 7578, form-data fields require a content-disposition header" ), Error::ContentDispositionMissing ); } #[actix_rt::test] async fn no_content_disposition_non_form_data() { let bytes = Bytes::from( "testasdadsad\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ Content-Type: text/plain; charset=utf-8\r\n\ Content-Length: 4\r\n\ \r\n\ test\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", ); let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, header::HeaderValue::from_static( "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", ), ); let payload = stream::iter(bytes) .map(|byte| Ok(Bytes::copy_from_slice(&[byte]))) .interleave_pending(); let mut multipart = Multipart::new(&headers, payload); let res = multipart.next().await.unwrap(); res.unwrap(); } #[actix_rt::test] async fn no_name_in_form_data_content_disposition() { let bytes = Bytes::from( "testasdadsad\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ Content-Disposition: form-data; filename=\"fn.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\n\ Content-Length: 4\r\n\ \r\n\ test\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", ); let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, header::HeaderValue::from_static( "multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", ), ); let payload = stream::iter(bytes) .map(|byte| Ok(Bytes::copy_from_slice(&[byte]))) .interleave_pending(); let mut multipart = Multipart::new(&headers, payload); let res = multipart.next().await.unwrap(); assert_matches!( res.expect_err("according to RFC 7578, form-data fields require a name attribute"), Error::ContentDispositionNameMissing ); } #[actix_rt::test] async fn test_drop_multipart_dont_hang() { let (sender, payload) = create_stream(); let (bytes, headers) = create_simple_request_with_header(); sender.send(Ok(bytes)).unwrap(); drop(sender); // eof let mut multipart = Multipart::new(&headers, payload); let mut field = multipart.next().await.unwrap().unwrap(); drop(multipart); // should fail immediately match field.next().await { Some(Err(Error::NotConsumed)) => {} _ => panic!(), }; } #[actix_rt::test] async fn test_drop_field_awaken_multipart() { let (sender, payload) = create_stream(); let (bytes, headers) = create_double_request_with_header(); sender.send(Ok(bytes)).unwrap(); drop(sender); // eof let mut multipart = Multipart::new(&headers, payload); let mut field = multipart.next().await.unwrap().unwrap(); let task = rt::spawn(async move { rt::time::sleep(Duration::from_millis(500)).await; assert_eq!(field.next().await.unwrap().unwrap(), "test"); drop(field); }); // dropping field should awaken current task let _ = multipart.next().await.unwrap().unwrap(); task.await.unwrap(); } } actix-multipart-0.7.2/src/payload.rs000064400000000000000000000104251046102023000155510ustar 00000000000000use std::{ cell::{RefCell, RefMut}, cmp, mem, pin::Pin, rc::Rc, task::{Context, Poll}, }; use actix_web::{ error::PayloadError, web::{Bytes, BytesMut}, }; use futures_core::stream::{LocalBoxStream, Stream}; use crate::{error::Error, safety::Safety}; pub(crate) struct PayloadRef { payload: Rc>, } impl PayloadRef { pub(crate) fn new(payload: PayloadBuffer) -> PayloadRef { PayloadRef { payload: Rc::new(RefCell::new(payload)), } } pub(crate) fn get_mut(&self, safety: &Safety) -> Option> { if safety.current() { Some(self.payload.borrow_mut()) } else { None } } } impl Clone for PayloadRef { fn clone(&self) -> PayloadRef { PayloadRef { payload: Rc::clone(&self.payload), } } } /// Payload buffer. pub(crate) struct PayloadBuffer { pub(crate) stream: LocalBoxStream<'static, Result>, pub(crate) buf: BytesMut, /// EOF flag. If true, no more payload reads will be attempted. pub(crate) eof: bool, } impl PayloadBuffer { /// Constructs new payload buffer. pub(crate) fn new(stream: S) -> Self where S: Stream> + 'static, { PayloadBuffer { stream: Box::pin(stream), buf: BytesMut::with_capacity(1_024), // pre-allocate 1KiB eof: false, } } pub(crate) fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { loop { match Pin::new(&mut self.stream).poll_next(cx) { Poll::Ready(Some(Ok(data))) => { self.buf.extend_from_slice(&data); // try to read more data continue; } Poll::Ready(Some(Err(err))) => return Err(err), Poll::Ready(None) => { self.eof = true; return Ok(()); } Poll::Pending => return Ok(()), } } } /// Reads exact number of bytes. #[cfg(test)] pub(crate) fn read_exact(&mut self, size: usize) -> Option { if size <= self.buf.len() { Some(self.buf.split_to(size).freeze()) } else { None } } pub(crate) fn read_max(&mut self, size: u64) -> Result, Error> { if !self.buf.is_empty() { let size = cmp::min(self.buf.len() as u64, size) as usize; Ok(Some(self.buf.split_to(size).freeze())) } else if self.eof { Err(Error::Incomplete) } else { Ok(None) } } /// Reads until specified ending. /// /// Returns: /// /// - `Ok(Some(chunk))` - `needle` is found, with chunk ending after needle /// - `Err(Incomplete)` - `needle` is not found and we're at EOF /// - `Ok(None)` - `needle` is not found otherwise pub(crate) fn read_until(&mut self, needle: &[u8]) -> Result, Error> { match memchr::memmem::find(&self.buf, needle) { // buffer exhausted and EOF without finding needle None if self.eof => Err(Error::Incomplete), // needle not yet found None => Ok(None), // needle found, split chunk out of buf Some(idx) => Ok(Some(self.buf.split_to(idx + needle.len()).freeze())), } } /// Reads bytes until new line delimiter. #[inline] pub(crate) fn readline(&mut self) -> Result, Error> { self.read_until(b"\n") } /// Reads bytes until new line delimiter or until EOF. #[inline] pub(crate) fn readline_or_eof(&mut self) -> Result, Error> { match self.readline() { Err(Error::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())), line => line, } } /// Puts unprocessed data back to the buffer. pub(crate) fn unprocessed(&mut self, data: Bytes) { // TODO: use BytesMut::from when it's released, see https://github.com/tokio-rs/bytes/pull/710 let buf = BytesMut::from(&data[..]); let buf = mem::replace(&mut self.buf, buf); self.buf.extend_from_slice(&buf); } } actix-multipart-0.7.2/src/safety.rs000064400000000000000000000032711046102023000154140ustar 00000000000000use std::{cell::Cell, marker::PhantomData, rc::Rc, task}; use local_waker::LocalWaker; /// Counter. It tracks of number of clones of payloads and give access to payload only to top most. /// /// - When dropped, parent task is awakened. This is to support the case where `Field` is dropped in /// a separate task than `Multipart`. /// - Assumes that parent owners don't move to different tasks; only the top-most is allowed to. /// - If dropped and is not top most owner, is_clean flag is set to false. #[derive(Debug)] pub(crate) struct Safety { task: LocalWaker, level: usize, payload: Rc>, clean: Rc>, } impl Safety { pub(crate) fn new() -> Safety { let payload = Rc::new(PhantomData); Safety { task: LocalWaker::new(), level: Rc::strong_count(&payload), clean: Rc::new(Cell::new(true)), payload, } } pub(crate) fn current(&self) -> bool { Rc::strong_count(&self.payload) == self.level && self.clean.get() } pub(crate) fn is_clean(&self) -> bool { self.clean.get() } pub(crate) fn clone(&self, cx: &task::Context<'_>) -> Safety { let payload = Rc::clone(&self.payload); let s = Safety { task: LocalWaker::new(), level: Rc::strong_count(&payload), clean: self.clean.clone(), payload, }; s.task.register(cx.waker()); s } } impl Drop for Safety { fn drop(&mut self) { if Rc::strong_count(&self.payload) != self.level { // Multipart dropped leaving a Field self.clean.set(false); } self.task.wake(); } } actix-multipart-0.7.2/src/test.rs000064400000000000000000000141401046102023000150750ustar 00000000000000//! Multipart testing utilities. use actix_web::{ http::header::{self, HeaderMap}, web::{BufMut as _, Bytes, BytesMut}, }; use mime::Mime; use rand::{ distributions::{Alphanumeric, DistString as _}, thread_rng, }; const CRLF: &[u8] = b"\r\n"; const CRLF_CRLF: &[u8] = b"\r\n\r\n"; const HYPHENS: &[u8] = b"--"; const BOUNDARY_PREFIX: &str = "------------------------"; /// Constructs a `multipart/form-data` payload from bytes and metadata. /// /// Returned header map can be extended or merged with existing headers. /// /// Multipart boundary used is a random alphanumeric string. /// /// # Examples /// /// ``` /// use actix_multipart::test::create_form_data_payload_and_headers; /// use actix_web::{test::TestRequest, web::Bytes}; /// use memchr::memmem::find; /// /// let (body, headers) = create_form_data_payload_and_headers( /// "foo", /// Some("lorem.txt".to_owned()), /// Some(mime::TEXT_PLAIN_UTF_8), /// Bytes::from_static(b"Lorem ipsum."), /// ); /// /// assert!(find(&body, b"foo").is_some()); /// assert!(find(&body, b"lorem.txt").is_some()); /// assert!(find(&body, b"text/plain; charset=utf-8").is_some()); /// assert!(find(&body, b"Lorem ipsum.").is_some()); /// /// let req = TestRequest::default(); /// /// // merge header map into existing test request and set multipart body /// let req = headers /// .into_iter() /// .fold(req, |req, hdr| req.insert_header(hdr)) /// .set_payload(body) /// .to_http_request(); /// /// assert!( /// req.headers() /// .get("content-type") /// .unwrap() /// .to_str() /// .unwrap() /// .starts_with("multipart/form-data; boundary=\"") /// ); /// ``` pub fn create_form_data_payload_and_headers( name: &str, filename: Option, content_type: Option, file: Bytes, ) -> (Bytes, HeaderMap) { let boundary = Alphanumeric.sample_string(&mut thread_rng(), 32); create_form_data_payload_and_headers_with_boundary( &boundary, name, filename, content_type, file, ) } /// Constructs a `multipart/form-data` payload from bytes and metadata with a fixed boundary. /// /// See [`create_form_data_payload_and_headers`] for more details. pub fn create_form_data_payload_and_headers_with_boundary( boundary: &str, name: &str, filename: Option, content_type: Option, file: Bytes, ) -> (Bytes, HeaderMap) { let mut buf = BytesMut::with_capacity(file.len() + 128); let boundary_str = [BOUNDARY_PREFIX, boundary].concat(); let boundary = boundary_str.as_bytes(); buf.put(HYPHENS); buf.put(boundary); buf.put(CRLF); buf.put(format!("Content-Disposition: form-data; name=\"{name}\"").as_bytes()); if let Some(filename) = filename { buf.put(format!("; filename=\"{filename}\"").as_bytes()); } buf.put(CRLF); if let Some(ct) = content_type { buf.put(format!("Content-Type: {ct}").as_bytes()); buf.put(CRLF); } buf.put(format!("Content-Length: {}", file.len()).as_bytes()); buf.put(CRLF_CRLF); buf.put(file); buf.put(CRLF); buf.put(HYPHENS); buf.put(boundary); buf.put(HYPHENS); buf.put(CRLF); let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, format!("multipart/form-data; boundary=\"{boundary_str}\"") .parse() .unwrap(), ); (buf.freeze(), headers) } #[cfg(test)] mod tests { use std::convert::Infallible; use futures_util::stream; use super::*; fn find_boundary(headers: &HeaderMap) -> String { headers .get("content-type") .unwrap() .to_str() .unwrap() .parse::() .unwrap() .get_param(mime::BOUNDARY) .unwrap() .as_str() .to_owned() } #[test] fn wire_format() { let (pl, headers) = create_form_data_payload_and_headers_with_boundary( "qWeRtYuIoP", "foo", None, None, Bytes::from_static(b"Lorem ipsum dolor\nsit ame."), ); assert_eq!( find_boundary(&headers), "------------------------qWeRtYuIoP", ); assert_eq!( std::str::from_utf8(&pl).unwrap(), "--------------------------qWeRtYuIoP\r\n\ Content-Disposition: form-data; name=\"foo\"\r\n\ Content-Length: 26\r\n\ \r\n\ Lorem ipsum dolor\n\ sit ame.\r\n\ --------------------------qWeRtYuIoP--\r\n", ); let (pl, _headers) = create_form_data_payload_and_headers_with_boundary( "qWeRtYuIoP", "foo", Some("Lorem.txt".to_owned()), Some(mime::TEXT_PLAIN_UTF_8), Bytes::from_static(b"Lorem ipsum dolor\nsit ame."), ); assert_eq!( std::str::from_utf8(&pl).unwrap(), "--------------------------qWeRtYuIoP\r\n\ Content-Disposition: form-data; name=\"foo\"; filename=\"Lorem.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\n\ Content-Length: 26\r\n\ \r\n\ Lorem ipsum dolor\n\ sit ame.\r\n\ --------------------------qWeRtYuIoP--\r\n", ); } /// Test using an external library to prevent the two-wrongs-make-a-right class of errors. #[actix_web::test] async fn ecosystem_compat() { let (pl, headers) = create_form_data_payload_and_headers( "foo", None, None, Bytes::from_static(b"Lorem ipsum dolor\nsit ame."), ); let boundary = find_boundary(&headers); let pl = stream::once(async { Ok::<_, Infallible>(pl) }); let mut form = multer::Multipart::new(pl, boundary); let field = form.next_field().await.unwrap().unwrap(); assert_eq!(field.name().unwrap(), "foo"); assert_eq!(field.file_name(), None); assert_eq!(field.content_type(), None); assert!(field.bytes().await.unwrap().starts_with(b"Lorem")); } }