multer-2.1.0/.cargo_vcs_info.json0000644000000001360000000000100123440ustar { "git": { "sha1": "d30e0833d1ae5748d7a2fb459d9d9b28e8b2ddb8" }, "path_in_vcs": "" }multer-2.1.0/.github/workflows/test.yml000064400000000000000000000016431046102023000162370ustar 00000000000000name: CI on: pull_request: push: branches: - master - develop env: RUSTFLAGS: -Dwarnings jobs: build_and_test: name: Build and test runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] rust: [stable, nightly] steps: - uses: actions/checkout@master - name: Install ${{ matrix.rust }} uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - name: check run: cargo check --all --bins --examples --all-features - name: tests run: cargo test --all --all-features check_fmt_and_docs: name: Checking fmt, clippy, and docs runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: clippy run: cargo clippy --tests --examples --bins -- -D warnings - name: fmt run: cargo fmt --all -- --check - name: Docs run: cargo doc --no-deps multer-2.1.0/.gitignore000064400000000000000000000017041046102023000131260ustar 00000000000000# Created by https://www.gitignore.io/api/rust,macos # Edit at https://www.gitignore.io/?templates=rust,macos ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Rust ### # Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk ### Others ### .halt.releez.yml /.idea /multer-rs.iml # End of https://www.gitignore.io/api/rust,macos multer-2.1.0/Cargo.lock0000644000000342730000000000100103300ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bytes" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "encoding_rs" version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" dependencies = [ "cfg-if", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures-channel" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" [[package]] name = "futures-sink" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" [[package]] name = "futures-task" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" [[package]] name = "futures-util" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ "futures-core", "futures-task", "pin-project-lite", "pin-utils", ] [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "http" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", "pin-project-lite", ] [[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" version = "0.14.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "socket2", "tokio", "tower-service", "tracing", "want", ] [[package]] name = "itoa" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "libc" version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "lock_api" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mime" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mio" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", "wasi", "windows-sys", ] [[package]] name = "multer" version = "2.1.0" dependencies = [ "bytes", "encoding_rs", "futures-util", "http", "httparse", "hyper", "log", "memchr", "mime", "serde", "serde_json", "spin", "tokio", "tokio-util", "version_check", ] [[package]] name = "num_cpus" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "once_cell" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-sys", ] [[package]] name = "pin-project-lite" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "ryu" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "signal-hook-registry" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" dependencies = [ "libc", ] [[package]] name = "smallvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "socket2" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", ] [[package]] name = "spin" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" [[package]] name = "syn" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tokio" version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" dependencies = [ "autocfg", "bytes", "libc", "memchr", "mio", "num_cpus", "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "winapi", ] [[package]] name = "tokio-macros" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-util" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "tower-service" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "unicode-ident" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "want" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" dependencies = [ "log", "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" multer-2.1.0/Cargo.toml0000644000000043400000000000100103430ustar # 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 = "2018" name = "multer" version = "2.1.0" authors = ["Rousan Ali "] description = "An async parser for `multipart/form-data` content-type in Rust." homepage = "https://github.com/rousan/multer-rs" readme = "README.md" keywords = [ "multipart", "multipart-formdata", "multipart-uploads", "async", "formdata", ] categories = [ "asynchronous", "web-programming", ] license = "MIT" repository = "https://github.com/rousan/multer-rs" [package.metadata.docs.rs] all-features = true [package.metadata.playground] features = ["all"] [[example]] name = "parse_async_read" path = "examples/parse_async_read.rs" required-features = ["tokio-io"] [dependencies.bytes] version = "1.0" [dependencies.encoding_rs] version = "0.8" [dependencies.futures-util] version = "0.3" default-features = false [dependencies.http] version = "0.2" [dependencies.httparse] version = "1.3" [dependencies.log] version = "0.4" [dependencies.memchr] version = "2.4" [dependencies.mime] version = "0.3" [dependencies.serde] version = "1.0" optional = true [dependencies.serde_json] version = "1.0" optional = true [dependencies.spin] version = "0.9" features = ["spin_mutex"] default-features = false [dependencies.tokio] version = "1.0" features = [] optional = true [dependencies.tokio-util] version = "0.7" features = ["io"] optional = true [dev-dependencies.hyper] version = "0.14" features = [ "server", "http1", "stream", "tcp", ] [dev-dependencies.serde] version = "1.0" features = ["derive"] [dev-dependencies.tokio] version = "1.0" features = ["full"] [build-dependencies.version_check] version = "0.9" [features] all = ["json"] default = [] json = [ "serde", "serde_json", ] tokio-io = [ "tokio", "tokio-util", ] multer-2.1.0/Cargo.toml.orig000064400000000000000000000027371046102023000140340ustar 00000000000000[package] name = "multer" version = "2.1.0" description = "An async parser for `multipart/form-data` content-type in Rust." homepage = "https://github.com/rousan/multer-rs" repository = "https://github.com/rousan/multer-rs" keywords = ["multipart", "multipart-formdata", "multipart-uploads", "async", "formdata"] categories = ["asynchronous", "web-programming"] authors = ["Rousan Ali "] readme = "README.md" license = "MIT" edition = "2018" [package.metadata.docs.rs] all-features = true [package.metadata.playground] features = ["all"] [features] default = [] all = ["json"] json = ["serde", "serde_json"] tokio-io = ["tokio", "tokio-util"] [dependencies] log = "0.4" bytes = "1.0" futures-util = { version = "0.3", default-features = false } memchr = "2.4" http = "0.2" httparse = "1.3" mime = "0.3" encoding_rs = "0.8" spin = { version = "0.9", default-features = false, features = ["spin_mutex"] } serde = { version = "1.0", optional = true } serde_json = { version = "1.0", optional = true } tokio = { version = "1.0", features = [], optional = true } tokio-util = { version = "0.7", features = ["io"], optional = true } [dev-dependencies] serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.0", features = ["full"] } hyper = { version = "0.14", features = ["server", "http1", "stream", "tcp"] } [build-dependencies] version_check = "0.9" [[example]] name = "parse_async_read" path = "examples/parse_async_read.rs" required-features = ["tokio-io"] multer-2.1.0/LICENSE000064400000000000000000000020531046102023000121410ustar 00000000000000MIT License Copyright (c) 2020 Rousan Ali 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. multer-2.1.0/README.md000064400000000000000000000103171046102023000124150ustar 00000000000000[![Github Actions Status](https://github.com/rousan/multer-rs/workflows/Test/badge.svg)](https://github.com/rousan/multer-rs/actions) [![crates.io](https://img.shields.io/crates/v/multer.svg)](https://crates.io/crates/multer) [![Documentation](https://docs.rs/multer/badge.svg)](https://docs.rs/multer) [![MIT](https://img.shields.io/crates/l/multer.svg)](./LICENSE) # multer-rs An async parser for `multipart/form-data` content-type in Rust. It accepts a [`Stream`](https://docs.rs/futures/0.3/futures/stream/trait.Stream.html) of [`Bytes`](https://docs.rs/bytes/1/bytes/struct.Bytes.html) as a source, so that It can be plugged into any async Rust environment e.g. any async server. [Docs](https://docs.rs/multer) ## Install Add this to your `Cargo.toml`: ```toml [dependencies] multer = "2.0" ``` # Basic Example ```rust use bytes::Bytes; use futures::stream::Stream; // Import multer types. use multer::Multipart; use std::convert::Infallible; use futures::stream::once; #[tokio::main] async fn main() -> Result<(), Box> { // Generate a byte stream and the boundary from somewhere e.g. server request body. let (stream, boundary) = get_byte_stream_from_somewhere().await; // Create a `Multipart` instance from that byte stream and the boundary. let mut multipart = Multipart::new(stream, boundary); // Iterate over the fields, use `next_field()` to get the next field. while let Some(mut field) = multipart.next_field().await? { // Get field name. let name = field.name(); // Get the field's filename if provided in "Content-Disposition" header. let file_name = field.file_name(); println!("Name: {:?}, File Name: {:?}", name, file_name); // Process the field data chunks e.g. store them in a file. while let Some(chunk) = field.chunk().await? { // Do something with field chunk. println!("Chunk: {:?}", chunk); } } Ok(()) } // Generate a byte stream and the boundary from somewhere e.g. server request body. async fn get_byte_stream_from_somewhere() -> (impl Stream>, &'static str) { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; let stream = once(async move { Result::::Ok(Bytes::from(data)) }); (stream, "X-BOUNDARY") } ``` ## Prevent Denial of Service (DoS) Attacks This crate also provides some APIs to prevent potential DoS attacks with fine grained control. It's recommended to add some constraints on field (specially text field) size to prevent DoS attacks exhausting the server's memory. An example: ```rust use multer::{Multipart, Constraints, SizeLimit}; #[tokio::main] async fn main() -> Result<(), Box> { // Create some constraints to be applied to the fields to prevent DoS attack. let constraints = Constraints::new() // We only accept `my_text_field` and `my_file_field` fields, // For any unknown field, we will throw an error. .allowed_fields(vec!["my_text_field", "my_file_field"]) .size_limit( SizeLimit::new() // Set 15mb as size limit for the whole stream body. .whole_stream(15 * 1024 * 1024) // Set 10mb as size limit for all fields. .per_field(10 * 1024 * 1024) // Set 30kb as size limit for our text field only. .for_field("my_text_field", 30 * 1024), ); // Create a `Multipart` instance from a stream and the constraints. let mut multipart = Multipart::with_constraints(some_stream, "X-BOUNDARY", constraints); while let Some(field) = multipart.next_field().await.unwrap() { let content = field.text().await.unwrap(); assert_eq!(content, "abcd"); } Ok(()) } ``` ## Usage with [hyper.rs](https://hyper.rs/) server An [example](https://github.com/rousan/multer-rs/blob/master/examples/hyper_server_example.rs) showing usage with [hyper.rs](https://hyper.rs/). For more examples, please visit [examples](https://github.com/rousan/multer-rs/tree/master/examples). ## Contributing Your PRs and suggestions are always welcome. multer-2.1.0/build.rs000064400000000000000000000002011046102023000125720ustar 00000000000000fn main() { if let Some(true) = version_check::is_feature_flaggable() { println!("cargo:rustc-cfg=nightly"); } } multer-2.1.0/examples/README.md000064400000000000000000000013611046102023000142320ustar 00000000000000# Examples of using multer-rs These examples show of how to do common tasks using `multer-rs`. Please visit: [Docs](https://docs.rs/multer) for the documentation. Run an example: ```sh cargo run --example example_name ``` * [`simple_example`](simple_example.rs) - A basic example using `multer`. * [`hyper_server_example`](hyper_server_example.rs) - Shows how to use this crate with Rust HTTP server [hyper](https://hyper.rs/). * [`parse_async_read`](parse_async_read.rs) - Shows how to parse `multipart/form-data` from an [`AsyncRead`](https://docs.rs/tokio/1/tokio/io/trait.AsyncRead.html). * [`prevent_dos_attack`](prevent_dos_attack.rs) - Shows how to apply some rules to prevent potential DoS attacks while handling `multipart/form-data`. multer-2.1.0/examples/hyper_server_example.rs000064400000000000000000000054261046102023000175570ustar 00000000000000use std::{convert::Infallible, net::SocketAddr}; use hyper::server::Server; use hyper::service::{make_service_fn, service_fn}; use hyper::{header::CONTENT_TYPE, Body, Request, Response, StatusCode}; // Import the multer types. use multer::Multipart; // A handler for incoming requests. async fn handle(req: Request) -> Result, Infallible> { // Extract the `multipart/form-data` boundary from the headers. let boundary = req .headers() .get(CONTENT_TYPE) .and_then(|ct| ct.to_str().ok()) .and_then(|ct| multer::parse_boundary(ct).ok()); // Send `BAD_REQUEST` status if the content-type is not multipart/form-data. if boundary.is_none() { return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::from("BAD REQUEST")) .unwrap()); } // Process the multipart e.g. you can store them in files. if let Err(err) = process_multipart(req.into_body(), boundary.unwrap()).await { return Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::from(format!("INTERNAL SERVER ERROR: {}", err))) .unwrap()); } Ok(Response::new(Body::from("Success"))) } // Process the request body as multipart/form-data. async fn process_multipart(body: Body, boundary: String) -> multer::Result<()> { // Create a Multipart instance from the request body. let mut multipart = Multipart::new(body, boundary); // Iterate over the fields, `next_field` method will return the next field if // available. while let Some(mut field) = multipart.next_field().await? { // Get the field name. let name = field.name(); // Get the field's filename if provided in "Content-Disposition" header. let file_name = field.file_name(); // Get the "Content-Type" header as `mime::Mime` type. let content_type = field.content_type(); println!( "Name: {:?}, FileName: {:?}, Content-Type: {:?}", name, file_name, content_type ); // Process the field data chunks e.g. store them in a file. let mut field_bytes_len = 0; while let Some(field_chunk) = field.chunk().await? { // Do something with field chunk. field_bytes_len += field_chunk.len(); } println!("Field Bytes Length: {:?}", field_bytes_len); } Ok(()) } #[tokio::main] async fn main() { let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(handle)) }); let server = Server::bind(&addr).serve(make_svc); println!("Server running at: {}", addr); if let Err(e) = server.await { eprintln!("server error: {}", e); } } multer-2.1.0/examples/parse_async_read.rs000064400000000000000000000032271046102023000166260ustar 00000000000000use multer::Multipart; use tokio::io::AsyncRead; #[tokio::main] async fn main() -> Result<(), Box> { // Generate an `AsyncRead` and the boundary from somewhere e.g. server request // body. let (reader, boundary) = get_async_reader_from_somewhere().await; // Create a `Multipart` instance from that async reader and the boundary. let mut multipart = Multipart::with_reader(reader, boundary); // Iterate over the fields, use `next_field()` to get the next field. while let Some(mut field) = multipart.next_field().await? { // Get field name. let name = field.name(); // Get the field's filename if provided in "Content-Disposition" header. let file_name = field.file_name(); println!("Name: {:?}, File Name: {:?}", name, file_name); // Process the field data chunks e.g. store them in a file. let mut field_bytes_len = 0; while let Some(field_chunk) = field.chunk().await? { // Do something with field chunk. field_bytes_len += field_chunk.len(); } println!("Field Bytes Length: {:?}", field_bytes_len); } Ok(()) } // Generate an `AsyncRead` and the boundary from somewhere e.g. server request // body. async fn get_async_reader_from_somewhere() -> (impl AsyncRead, &'static str) { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; (data.as_bytes(), "X-BOUNDARY") } multer-2.1.0/examples/prevent_dos_attack.rs000064400000000000000000000047301046102023000172030ustar 00000000000000use std::convert::Infallible; use bytes::Bytes; use futures_util::stream::Stream; // Import multer types. use multer::{Constraints, Multipart, SizeLimit}; #[tokio::main] async fn main() -> Result<(), Box> { // Generate a byte stream and the boundary from somewhere e.g. server request // body. let (stream, boundary) = get_byte_stream_from_somewhere().await; // Create some constraints to be applied to the fields to prevent DoS attacks. let constraints = Constraints::new() // We only accept `my_text_field` and `my_file_field` fields, // For any unknown field, we will throw an error. .allowed_fields(vec!["my_text_field", "my_file_field"]) .size_limit( SizeLimit::new() // Set 15mb as size limit for the whole stream body. .whole_stream(15 * 1024 * 1024) // Set 10mb as size limit for all fields. .per_field(10 * 1024 * 1024) // Set 30kb as size limit for our text field only. .for_field("my_text_field", 30 * 1024), ); // Create a `Multipart` instance from that byte stream and the constraints. let mut multipart = Multipart::with_constraints(stream, boundary, constraints); // Iterate over the fields, use `next_field()` to get the next field. while let Some(field) = multipart.next_field().await? { // Get field name. let name = field.name(); // Get the field's filename if provided in "Content-Disposition" header. let file_name = field.file_name(); println!("Name: {:?}, File Name: {:?}", name, file_name); // Read field content as text. let content = field.text().await?; println!("Content: {:?}", content); } Ok(()) } // Generate a byte stream and the boundary from somewhere e.g. server request // body. async fn get_byte_stream_from_somewhere() -> (impl Stream>, &'static str) { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = futures_util::stream::iter( data.chars() .map(|ch| ch.to_string()) .map(|part| Ok(Bytes::copy_from_slice(part.as_bytes()))), ); (stream, "X-BOUNDARY") } multer-2.1.0/examples/simple_example.rs000064400000000000000000000033201046102023000163220ustar 00000000000000use std::convert::Infallible; use bytes::Bytes; use futures_util::stream::Stream; // Import multer types. use multer::Multipart; #[tokio::main] async fn main() -> Result<(), Box> { // Generate a byte stream and the boundary from somewhere e.g. server request // body. let (stream, boundary) = get_byte_stream_from_somewhere().await; // Create a `Multipart` instance from that byte stream and the boundary. let mut multipart = Multipart::new(stream, boundary); // Iterate over the fields, use `next_field()` to get the next field. while let Some(field) = multipart.next_field().await? { // Get field name. let name = field.name(); // Get the field's filename if provided in "Content-Disposition" header. let file_name = field.file_name(); println!("Name: {:?}, File Name: {:?}", name, file_name); // Read field content as text. let content = field.text().await?; println!("Content: {:?}", content); } Ok(()) } // Generate a byte stream and the boundary from somewhere e.g. server request // body. async fn get_byte_stream_from_somewhere() -> (impl Stream>, &'static str) { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = futures_util::stream::iter( data.chars() .map(|ch| ch.to_string()) .map(|part| Ok(Bytes::copy_from_slice(part.as_bytes()))), ); (stream, "X-BOUNDARY") } multer-2.1.0/releez.yml000064400000000000000000000023151046102023000131460ustar 00000000000000version: 1.0.0 checklist: - name: Checkout master and sync with remote type: auto run: - git checkout master - git pull - name: Check syntax type: auto run: - cargo check --release --features="all" - name: Run tests type: auto run: - cargo test --release --features="all" - name: Make sure code is formatted type: auto run: - cargo fmt - name: Bump version type: manual instructions: - Please update version with ${VERSION} in Cargo.toml file. - Please update version with ${VERSION} in README.md file if needed. - name: Commit changes type: auto run: - git add --all && git commit -m "Bump version" - name: Create a release tag type: auto run: - git tag "v${VERSION}" -a - name: Push branches and tags to Github type: auto run: - git push origin master - git push --tags - name: Edit tag on Github type: manual instructions: - Tag is pushed to Github(https://github.com/rousan/multer-rs/releases). Edit it there and make it a release. - name: Publish to crates.io type: auto confirm: Are you sure to publish it to crates.io? run: - cargo publish multer-2.1.0/rustfmt.toml000064400000000000000000000004071046102023000135360ustar 00000000000000max_width = 120 tab_spaces = 4 wrap_comments = true condense_wildcard_suffixes = true format_code_in_doc_comments = true newline_style = "Unix" normalize_comments = true reorder_impl_items = true group_imports = "StdExternalCrate" use_field_init_shorthand = true multer-2.1.0/src/buffer.rs000064400000000000000000000130671046102023000135510ustar 00000000000000use std::fmt; use std::pin::Pin; use std::task::{Context, Poll}; use bytes::{Buf, Bytes, BytesMut}; use futures_util::stream::Stream; use crate::constants; pub(crate) struct StreamBuffer<'r> { pub(crate) eof: bool, pub(crate) buf: BytesMut, pub(crate) stream: Pin> + Send + 'r>>, pub(crate) whole_stream_size_limit: u64, pub(crate) stream_size_counter: u64, } impl<'r> StreamBuffer<'r> { pub fn new(stream: S, whole_stream_size_limit: u64) -> Self where S: Stream> + Send + 'r, { StreamBuffer { eof: false, buf: BytesMut::new(), stream: Box::pin(stream), whole_stream_size_limit, stream_size_counter: 0, } } pub fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), crate::Error> { if self.eof { return Ok(()); } loop { match self.stream.as_mut().poll_next(cx) { Poll::Ready(Some(Ok(data))) => { self.stream_size_counter += data.len() as u64; if self.stream_size_counter > self.whole_stream_size_limit { return Err(crate::Error::StreamSizeExceeded { limit: self.whole_stream_size_limit, }); } self.buf.extend_from_slice(&data) } Poll::Ready(Some(Err(err))) => return Err(err), Poll::Ready(None) => { self.eof = true; return Ok(()); } Poll::Pending => return Ok(()), } } } pub fn read_exact(&mut self, size: usize) -> Option { if size <= self.buf.len() { Some(self.buf.split_to(size).freeze()) } else { None } } pub fn peek_exact(&mut self, size: usize) -> Option<&[u8]> { self.buf.get(..size) } pub fn read_until(&mut self, pattern: &[u8]) -> Option { memchr::memmem::find(&self.buf, pattern).map(|idx| self.buf.split_to(idx + pattern.len()).freeze()) } pub fn read_to(&mut self, pattern: &[u8]) -> Option { memchr::memmem::find(&self.buf, pattern).map(|idx| self.buf.split_to(idx).freeze()) } pub fn advance_past_transport_padding(&mut self) -> bool { match self.buf.iter().position(|b| *b != b' ' && *b != b'\t') { Some(pos) => { self.buf.advance(pos); true } None => { self.buf.clear(); false } } } pub fn read_field_data( &mut self, boundary: &str, field_name: Option<&str>, ) -> crate::Result> { log::trace!("finding next field: {:?}", field_name); if self.buf.is_empty() && self.eof { log::trace!("empty buffer && EOF"); return Err(crate::Error::IncompleteFieldData { field_name: field_name.map(|s| s.to_owned()), }); } else if self.buf.is_empty() { return Ok(None); } let boundary_deriv = format!("{}{}{}", constants::CRLF, constants::BOUNDARY_EXT, boundary); let b_len = boundary_deriv.len(); match memchr::memmem::find(&self.buf, boundary_deriv.as_bytes()) { Some(idx) => { log::trace!("new field found at {}", idx); let bytes = self.buf.split_to(idx).freeze(); // discard \r\n. self.buf.advance(constants::CRLF.len()); Ok(Some((true, bytes))) } None if self.eof => { log::trace!("no new field found: EOF. terminating"); Err(crate::Error::IncompleteFieldData { field_name: field_name.map(|s| s.to_owned()), }) } None => { let buf_len = self.buf.len(); let rem_boundary_part_max_len = b_len - 1; let rem_boundary_part_idx = if buf_len >= rem_boundary_part_max_len { buf_len - rem_boundary_part_max_len } else { 0 }; log::trace!("no new field found, not EOF, checking close"); let bytes = &self.buf[rem_boundary_part_idx..]; match memchr::memmem::rfind(bytes, constants::CR.as_bytes()) { Some(rel_idx) => { let idx = rel_idx + rem_boundary_part_idx; match memchr::memmem::find(boundary_deriv.as_bytes(), &self.buf[idx..]) { Some(_) => { let bytes = self.buf.split_to(idx).freeze(); match bytes.is_empty() { true => Ok(None), false => Ok(Some((false, bytes))), } } None => Ok(Some((false, self.read_full_buf()))), } } None => Ok(Some((false, self.read_full_buf()))), } } } } pub fn read_full_buf(&mut self) -> Bytes { self.buf.split_to(self.buf.len()).freeze() } } impl fmt::Debug for StreamBuffer<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("StreamBuffer").finish() } } multer-2.1.0/src/constants.rs000064400000000000000000000145131046102023000143110ustar 00000000000000pub(crate) const DEFAULT_WHOLE_STREAM_SIZE_LIMIT: u64 = std::u64::MAX; pub(crate) const DEFAULT_PER_FIELD_SIZE_LIMIT: u64 = std::u64::MAX; pub(crate) const MAX_HEADERS: usize = 32; pub(crate) const BOUNDARY_EXT: &str = "--"; pub(crate) const CR: &str = "\r"; #[allow(dead_code)] pub(crate) const LF: &str = "\n"; pub(crate) const CRLF: &str = "\r\n"; pub(crate) const CRLF_CRLF: &str = "\r\n\r\n"; #[derive(PartialEq)] pub(crate) enum ContentDispositionAttr { Name, FileName, } impl ContentDispositionAttr { /// Extract ContentDisposition Attribute from header. /// /// Some older clients may not quote the name or filename, so we allow them pub fn extract_from<'h>(&self, header: &'h [u8]) -> Option<&'h [u8]> { let prefix = match self { ContentDispositionAttr::Name => &b"name="[..], ContentDispositionAttr::FileName => &b"filename="[..], }; if let Some(i) = memchr::memmem::find(header, prefix) { // Check if this is malformed, with `filename` coming first. if *self == ContentDispositionAttr::Name && i > 0 && header[i - 1] == b'e' { return None; } // Handle quoted strings first, then unquoted string. // FIXME: According to RFC6266 4.1, a 'quoted-string' (RFC 2616 2.2) // can contain a 'quoted-pair', which can be used to escape a quote // character in a name with `\`. That is, "a\"b" is a valid name. // But this routine would truncate it to `a\`; this is wrong. let rest = &header[(i + prefix.len())..]; if rest.starts_with(b"\"") { let k = memchr::memchr(b'"', &rest[1..])?; return Some(&rest[1..(k + 1)]); } else { let j = memchr::memchr(b';', rest).unwrap_or(rest.len()); return Some(&rest[..j]); } } None } } #[cfg(test)] mod tests { use super::*; #[test] fn test_content_disposition_name_only() { let val = br#"form-data; name="my_field""#; let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(name.unwrap(), b"my_field"); assert!(filename.is_none()); } #[test] fn test_content_disposition_extraction() { let val = br#"form-data; name="my_field"; filename="file abc.txt""#; let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(name.unwrap(), b"my_field"); assert_eq!(filename.unwrap(), b"file abc.txt"); let val = "form-data; name=\"你好\"; filename=\"file abc.txt\"".as_bytes(); let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(name.unwrap(), "你好".as_bytes()); assert_eq!(filename.unwrap(), b"file abc.txt"); let val = "form-data; name=\"কখগ\"; filename=\"你好.txt\"".as_bytes(); let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(name.unwrap(), "কখগ".as_bytes()); assert_eq!(filename.unwrap(), "你好.txt".as_bytes()); } #[test] fn test_content_disposition_file_name_only() { // These are technically malformed, as RFC 7578 says the `name` // parameter _must_ be included. But okay. let val = br#"form-data; filename="file-name.txt""#; let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(filename.unwrap(), b"file-name.txt"); assert!(name.is_none()); let val = "form-data; filename=\"কখগ-你好.txt\"".as_bytes(); let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(filename.unwrap(), "কখগ-你好.txt".as_bytes()); assert!(name.is_none()); } #[test] fn test_content_disposition_name_unquoted() { let val = br#"form-data; name=my_field"#; let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(name.unwrap(), b"my_field"); assert!(filename.is_none()); let val = br#"form-data; name=my_field; filename=file-name.txt"#; let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(name.unwrap(), b"my_field"); assert_eq!(filename.unwrap(), b"file-name.txt"); } #[test] fn test_content_disposition_name_quoted() { let val = br#"form-data; name="my;f;ield""#; let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(name.unwrap(), b"my;f;ield"); assert!(filename.is_none()); let val = br#"form-data; name=my_field; filename="file;name.txt""#; let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(name.unwrap(), b"my_field"); assert_eq!(filename.unwrap(), b"file;name.txt"); let val = br#"form-data; name=; filename=filename.txt"#; let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(name.unwrap(), b""); assert_eq!(filename.unwrap(), b"filename.txt"); let val = br#"form-data; name=";"; filename=";""#; let name = ContentDispositionAttr::Name.extract_from(val); let filename = ContentDispositionAttr::FileName.extract_from(val); assert_eq!(name.unwrap(), b";"); assert_eq!(filename.unwrap(), b";"); } // FIXME: This test should pass. #[test] #[should_panic] fn test_content_disposition_name_escaped_quote() { let val = br#"form-data; name="my\"field\"name""#; let name = ContentDispositionAttr::Name.extract_from(val); assert_eq!(name.unwrap(), b"my\"field\"name"); } } multer-2.1.0/src/constraints.rs000064400000000000000000000064201046102023000146420ustar 00000000000000use crate::size_limit::SizeLimit; /// Represents some rules to be applied on the stream and field's content size /// to prevent DoS attacks. /// /// It's recommended to add some rules on field (specially text field) size to /// avoid potential DoS attacks from attackers running the server out of memory. /// This type provides some API to apply constraints on very granular level to /// make `multipart/form-data` safe. By default, it does not apply any /// constraint. /// /// # Examples /// /// ``` /// use multer::{Multipart, Constraints, SizeLimit}; /// # use bytes::Bytes; /// # use std::convert::Infallible; /// # use futures_util::stream::once; /// /// # async fn run() { /// # let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// # let some_stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// // Create some constraints to be applied to the fields to prevent DoS attack. /// let constraints = Constraints::new() /// // We only accept `my_text_field` and `my_file_field` fields, /// // For any unknown field, we will throw an error. /// .allowed_fields(vec!["my_text_field", "my_file_field"]) /// .size_limit( /// SizeLimit::new() /// // Set 15mb as size limit for the whole stream body. /// .whole_stream(15 * 1024 * 1024) /// // Set 10mb as size limit for all fields. /// .per_field(10 * 1024 * 1024) /// // Set 30kb as size limit for our text field only. /// .for_field("my_text_field", 30 * 1024), /// ); /// /// // Create a `Multipart` instance from a stream and the constraints. /// let mut multipart = Multipart::with_constraints(some_stream, "X-BOUNDARY", constraints); /// /// while let Some(field) = multipart.next_field().await.unwrap() { /// let content = field.text().await.unwrap(); /// assert_eq!(content, "abcd"); /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` #[derive(Debug, Default)] pub struct Constraints { pub(crate) size_limit: SizeLimit, pub(crate) allowed_fields: Option>, } impl Constraints { /// Creates a set of rules with default behaviour. pub fn new() -> Constraints { Constraints::default() } /// Applies rules on field's content length. pub fn size_limit(self, size_limit: SizeLimit) -> Constraints { Constraints { size_limit, allowed_fields: self.allowed_fields, } } /// Specify which fields should be allowed, for any unknown field, the /// [`next_field`](crate::Multipart::next_field) will throw an error. pub fn allowed_fields>(self, allowed_fields: Vec) -> Constraints { let allowed_fields = allowed_fields.into_iter().map(|item| item.into()).collect(); Constraints { size_limit: self.size_limit, allowed_fields: Some(allowed_fields), } } pub(crate) fn is_it_allowed(&self, field: Option<&str>) -> bool { if let Some(ref allowed_fields) = self.allowed_fields { field .map(|field| allowed_fields.iter().any(|item| item == field)) .unwrap_or(false) } else { true } } } multer-2.1.0/src/content_disposition.rs000064400000000000000000000016221046102023000163700ustar 00000000000000use http::header::{self, HeaderMap}; use crate::constants::ContentDispositionAttr; #[derive(Debug)] pub(crate) struct ContentDisposition { pub(crate) field_name: Option, pub(crate) file_name: Option, } impl ContentDisposition { pub fn parse(headers: &HeaderMap) -> ContentDisposition { let content_disposition = headers.get(header::CONTENT_DISPOSITION).map(|val| val.as_bytes()); let field_name = content_disposition .and_then(|val| ContentDispositionAttr::Name.extract_from(val)) .and_then(|attr| std::str::from_utf8(attr).ok()) .map(String::from); let file_name = content_disposition .and_then(|val| ContentDispositionAttr::FileName.extract_from(val)) .and_then(|attr| std::str::from_utf8(attr).ok()) .map(String::from); ContentDisposition { field_name, file_name } } } multer-2.1.0/src/error.rs000064400000000000000000000103161046102023000134230ustar 00000000000000use std::fmt::{self, Debug, Display, Formatter}; type BoxError = Box; /// A set of errors that can occur during parsing multipart stream and in other /// operations. #[non_exhaustive] pub enum Error { /// An unknown field is detected when multipart /// [`constraints`](crate::Constraints::allowed_fields) are added. UnknownField { field_name: Option }, /// The field data is found incomplete. IncompleteFieldData { field_name: Option }, /// Couldn't read the field headers completely. IncompleteHeaders, /// Failed to read headers. ReadHeaderFailed(httparse::Error), /// Failed to decode the field's raw header name to /// [`HeaderName`](http::header::HeaderName) type. DecodeHeaderName { name: String, cause: BoxError }, /// Failed to decode the field's raw header value to /// [`HeaderValue`](http::header::HeaderValue) type. DecodeHeaderValue { value: Vec, cause: BoxError }, /// Multipart stream is incomplete. IncompleteStream, /// The incoming field size exceeded the maximum limit. FieldSizeExceeded { limit: u64, field_name: Option }, /// The incoming stream size exceeded the maximum limit. StreamSizeExceeded { limit: u64 }, /// Stream read failed. StreamReadFailed(BoxError), /// Failed to lock the multipart shared state for any changes. LockFailure, /// The `Content-Type` header is not `multipart/form-data`. NoMultipart, /// Failed to convert the `Content-Type` to [`mime::Mime`] type. DecodeContentType(mime::FromStrError), /// No boundary found in `Content-Type` header. NoBoundary, /// Failed to decode the field data as `JSON` in /// [`field.json()`](crate::Field::json) method. #[cfg(feature = "json")] #[cfg_attr(nightly, doc(cfg(feature = "json")))] DecodeJson(serde_json::Error), } impl Debug for Error { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(self, f) } } impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Error::UnknownField { field_name } => { let name = field_name.as_deref().unwrap_or(""); write!(f, "unknown field received: {:?}", name) } Error::IncompleteFieldData { field_name } => { let name = field_name.as_deref().unwrap_or(""); write!(f, "field {:?} received with incomplete data", name) } Error::DecodeHeaderName { name, cause } => { write!(f, "failed to decode field's raw header name: {:?} {}", name, cause) } Error::DecodeHeaderValue { cause, .. } => { write!(f, "failed to decode field's raw header value: {}", cause) } Error::FieldSizeExceeded { limit, field_name } => { let name = field_name.as_deref().unwrap_or(""); write!(f, "field {:?} exceeded the size limit: {} bytes", name, limit) } Error::StreamSizeExceeded { limit } => { write!(f, "stream size exceeded limit: {} bytes", limit) } Error::ReadHeaderFailed(e) => write!(f, "failed to read headers: {}", e), Error::StreamReadFailed(e) => write!(f, "failed to read stream: {}", e), Error::DecodeContentType(e) => write!(f, "failed to decode Content-Type: {}", e), Error::IncompleteHeaders => write!(f, "failed to read field complete headers"), Error::IncompleteStream => write!(f, "incomplete multipart stream"), Error::LockFailure => write!(f, "failed to lock multipart state"), Error::NoMultipart => write!(f, "Content-Type is not multipart/form-data"), Error::NoBoundary => write!(f, "multipart boundary not found in Content-Type"), #[cfg(feature = "json")] Error::DecodeJson(e) => write!(f, "failed to decode field data as JSON: {}", e), } } } impl std::error::Error for Error {} impl PartialEq for Error { fn eq(&self, other: &Self) -> bool { self.to_string().eq(&other.to_string()) } } impl Eq for Error {} multer-2.1.0/src/field.rs000064400000000000000000000303471046102023000133630ustar 00000000000000use std::borrow::Cow; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use bytes::{Bytes, BytesMut}; use encoding_rs::{Encoding, UTF_8}; use futures_util::stream::{Stream, TryStreamExt}; use http::header::HeaderMap; #[cfg(feature = "json")] use serde::de::DeserializeOwned; use spin::mutex::spin::SpinMutex as Mutex; use crate::content_disposition::ContentDisposition; use crate::multipart::{MultipartState, StreamingStage}; use crate::{helpers, Error}; /// A single field in a multipart stream. /// /// Its content can be accessed via the [`Stream`] API or the methods defined in /// this type. /// /// # Lifetime /// /// The lifetime of the stream `'r` corresponds to the lifetime of the /// underlying `Stream`. If the underlying stream holds no references directly /// or transitively, then the lifetime can be `'static`. /// /// # Examples /// /// ``` /// use std::convert::Infallible; /// /// use bytes::Bytes; /// use futures_util::stream::once; /// use multer::Multipart; /// /// # async fn run() { /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; \ /// name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// /// while let Some(field) = multipart.next_field().await.unwrap() { /// let content = field.text().await.unwrap(); /// assert_eq!(content, "abcd"); /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` /// /// [`Multipart`]: crate::Multipart #[derive(Debug)] pub struct Field<'r> { state: Arc>>, done: bool, headers: HeaderMap, content_disposition: ContentDisposition, content_type: Option, idx: usize, } impl<'r> Field<'r> { pub(crate) fn new( state: Arc>>, headers: HeaderMap, idx: usize, content_disposition: ContentDisposition, ) -> Self { let content_type = helpers::parse_content_type(&headers); Field { state, headers, content_disposition, content_type, idx, done: false, } } /// The field name found in the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header. pub fn name(&self) -> Option<&str> { self.content_disposition.field_name.as_deref() } /// The file name found in the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header. pub fn file_name(&self) -> Option<&str> { self.content_disposition.file_name.as_deref() } /// Get the content type of the field. pub fn content_type(&self) -> Option<&mime::Mime> { self.content_type.as_ref() } /// Get a map of headers as [`HeaderMap`]. pub fn headers(&self) -> &HeaderMap { &self.headers } /// Get the full data of the field as [`Bytes`]. /// /// # Examples /// /// ``` /// use std::convert::Infallible; /// /// use bytes::Bytes; /// use futures_util::stream::once; /// use multer::Multipart; /// /// # async fn run() { /// let data = /// "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// /// while let Some(field) = multipart.next_field().await.unwrap() { /// let bytes = field.bytes().await.unwrap(); /// assert_eq!(bytes.len(), 4); /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` pub async fn bytes(self) -> crate::Result { let mut buf = BytesMut::new(); let mut this = self; while let Some(bytes) = this.chunk().await? { buf.extend_from_slice(&bytes); } Ok(buf.freeze()) } /// Stream a chunk of the field data. /// /// When the field data has been exhausted, this will return [`None`]. /// /// # Examples /// /// ``` /// use std::convert::Infallible; /// /// use bytes::Bytes; /// use futures_util::stream::once; /// use multer::Multipart; /// /// # async fn run() { /// let data = /// "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// /// while let Some(mut field) = multipart.next_field().await.unwrap() { /// while let Some(chunk) = field.chunk().await.unwrap() { /// println!("Chunk: {:?}", chunk); /// } /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` pub async fn chunk(&mut self) -> crate::Result> { self.try_next().await } /// Try to deserialize the field data as JSON. /// /// # Optional /// /// This requires the optional `json` feature to be enabled. /// /// # Examples /// /// ``` /// use multer::Multipart; /// use bytes::Bytes; /// use std::convert::Infallible; /// use futures_util::stream::once; /// use serde::Deserialize; /// /// // This `derive` requires the `serde` dependency. /// #[derive(Deserialize)] /// struct User { /// name: String /// } /// /// # async fn run() { /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\n{ \"name\": \"Alice\" }\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// /// while let Some(field) = multipart.next_field().await.unwrap() { /// let user = field.json::().await.unwrap(); /// println!("User Name: {}", user.name); /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` /// /// # Errors /// /// This method fails if the field data is not in JSON format /// or it cannot be properly deserialized to target type `T`. For more /// details please see [`serde_json::from_slice`]. #[cfg(feature = "json")] #[cfg_attr(nightly, doc(cfg(feature = "json")))] pub async fn json(self) -> crate::Result { serde_json::from_slice(&self.bytes().await?).map_err(crate::Error::DecodeJson) } /// Get the full field data as text. /// /// This method decodes the field data with `BOM sniffing` and with /// malformed sequences replaced with the `REPLACEMENT CHARACTER`. /// Encoding is determined from the `charset` parameter of `Content-Type` /// header, and defaults to `utf-8` if not presented. /// /// # Examples /// /// ``` /// use std::convert::Infallible; /// /// use bytes::Bytes; /// use futures_util::stream::once; /// use multer::Multipart; /// /// # async fn run() { /// let data = /// "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// /// while let Some(field) = multipart.next_field().await.unwrap() { /// let content = field.text().await.unwrap(); /// assert_eq!(content, "abcd"); /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` pub async fn text(self) -> crate::Result { self.text_with_charset("utf-8").await } /// Get the full field data as text given a specific encoding. /// /// This method decodes the field data with `BOM sniffing` and with /// malformed sequences replaced with the `REPLACEMENT CHARACTER`. /// You can provide a default encoding for decoding the raw message, while /// the `charset` parameter of `Content-Type` header is still prioritized. /// For more information about the possible encoding name, please go to /// [encoding_rs] docs. /// /// # Examples /// /// ``` /// use std::convert::Infallible; /// /// use bytes::Bytes; /// use futures_util::stream::once; /// use multer::Multipart; /// /// # async fn run() { /// let data = /// "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// /// while let Some(field) = multipart.next_field().await.unwrap() { /// let content = field.text_with_charset("utf-8").await.unwrap(); /// assert_eq!(content, "abcd"); /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` pub async fn text_with_charset(self, default_encoding: &str) -> crate::Result { let encoding_name = self .content_type() .and_then(|mime| mime.get_param(mime::CHARSET)) .map(|charset| charset.as_str()) .unwrap_or(default_encoding); let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8); let bytes = self.bytes().await?; let (text, ..) = encoding.decode(&bytes); match text { Cow::Owned(s) => Ok(s), Cow::Borrowed(s) => Ok(String::from(s)), } } /// Get the index of this field in order they appeared in the stream. /// /// # Examples /// /// ``` /// use std::convert::Infallible; /// /// use bytes::Bytes; /// use futures_util::stream::once; /// use multer::Multipart; /// /// # async fn run() { /// let data = /// "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// /// while let Some(field) = multipart.next_field().await.unwrap() { /// let idx = field.index(); /// println!("Field index: {}", idx); /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` pub fn index(&self) -> usize { self.idx } } impl Stream for Field<'_> { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if self.done { return Poll::Ready(None); } debug_assert!(self.state.try_lock().is_some(), "expected exlusive lock"); let state = self.state.clone(); let mut lock = match state.try_lock() { Some(lock) => lock, None => return Poll::Ready(Some(Err(Error::LockFailure))), }; let state = &mut *lock; if let Err(err) = state.buffer.poll_stream(cx) { return Poll::Ready(Some(Err(crate::Error::StreamReadFailed(err.into())))); } match state .buffer .read_field_data(&state.boundary, state.curr_field_name.as_deref()) { Ok(Some((done, bytes))) => { state.curr_field_size_counter += bytes.len() as u64; if state.curr_field_size_counter > state.curr_field_size_limit { return Poll::Ready(Some(Err(crate::Error::FieldSizeExceeded { limit: state.curr_field_size_limit, field_name: state.curr_field_name.clone(), }))); } if done { state.stage = StreamingStage::ReadingBoundary; self.done = true; } Poll::Ready(Some(Ok(bytes))) } Ok(None) => Poll::Pending, Err(err) => Poll::Ready(Some(Err(err))), } } } multer-2.1.0/src/helpers.rs000064400000000000000000000017601046102023000137370ustar 00000000000000use std::convert::TryFrom; use http::header::{self, HeaderMap, HeaderName, HeaderValue}; use httparse::Header; pub(crate) fn convert_raw_headers_to_header_map(raw_headers: &[Header<'_>]) -> crate::Result { let mut headers = HeaderMap::with_capacity(raw_headers.len()); for raw_header in raw_headers { let name = HeaderName::try_from(raw_header.name).map_err(|err| crate::Error::DecodeHeaderName { name: raw_header.name.to_owned(), cause: err.into(), })?; let value = HeaderValue::try_from(raw_header.value).map_err(|err| crate::Error::DecodeHeaderValue { value: raw_header.value.to_owned(), cause: err.into(), })?; headers.insert(name, value); } Ok(headers) } pub(crate) fn parse_content_type(headers: &HeaderMap) -> Option { headers .get(header::CONTENT_TYPE) .and_then(|val| val.to_str().ok()) .and_then(|val| val.parse::().ok()) } multer-2.1.0/src/lib.rs000064400000000000000000000147641046102023000130530ustar 00000000000000//! An async parser for `multipart/form-data` content-type in Rust. //! //! It accepts a [`Stream`](futures_util::stream::Stream) of //! [`Bytes`](bytes::Bytes), or with the `tokio-io` feature enabled, an //! `AsyncRead` reader as a source, so that it can be plugged into any async //! Rust environment e.g. any async server. //! //! # Examples //! //! ```no_run //! use std::convert::Infallible; //! //! use bytes::Bytes; //! // Import multer types. //! use futures_util::stream::once; //! use futures_util::stream::Stream; //! use multer::Multipart; //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { //! // Generate a byte stream and the boundary from somewhere e.g. server request body. //! let (stream, boundary) = get_byte_stream_from_somewhere().await; //! //! // Create a `Multipart` instance from that byte stream and the boundary. //! let mut multipart = Multipart::new(stream, boundary); //! //! // Iterate over the fields, use `next_field()` to get the next field. //! while let Some(mut field) = multipart.next_field().await? { //! // Get field name. //! let name = field.name(); //! // Get the field's filename if provided in "Content-Disposition" header. //! let file_name = field.file_name(); //! //! println!("Name: {:?}, File Name: {:?}", name, file_name); //! //! // Process the field data chunks e.g. store them in a file. //! while let Some(chunk) = field.chunk().await? { //! // Do something with field chunk. //! println!("Chunk: {:?}", chunk); //! } //! } //! //! Ok(()) //! } //! //! // Generate a byte stream and the boundary from somewhere e.g. server request body. //! async fn get_byte_stream_from_somewhere( //! ) -> (impl Stream>, &'static str) { //! let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; \ //! name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; //! //! let stream = once(async move { Result::::Ok(Bytes::from(data)) }); //! (stream, "X-BOUNDARY") //! } //! ``` //! //! ## Prevent Denial of Service (DoS) Attack //! //! This crate also provides some APIs to prevent potential DoS attacks with //! fine grained control. It's recommended to add some constraints //! on field (specially text field) size to avoid potential DoS attacks from //! attackers running the server out of memory. //! //! An example: //! //! ``` //! use multer::{Constraints, Multipart, SizeLimit}; //! # use bytes::Bytes; //! # use std::convert::Infallible; //! # use futures_util::stream::once; //! //! # async fn run() { //! # let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; \ //! # name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; //! # let some_stream = once(async move { Result::::Ok(Bytes::from(data)) }); //! // Create some constraints to be applied to the fields to prevent DoS attack. //! let constraints = Constraints::new() //! // We only accept `my_text_field` and `my_file_field` fields, //! // For any unknown field, we will throw an error. //! .allowed_fields(vec!["my_text_field", "my_file_field"]) //! .size_limit( //! SizeLimit::new() //! // Set 15mb as size limit for the whole stream body. //! .whole_stream(15 * 1024 * 1024) //! // Set 10mb as size limit for all fields. //! .per_field(10 * 1024 * 1024) //! // Set 30kb as size limit for our text field only. //! .for_field("my_text_field", 30 * 1024), //! ); //! //! // Create a `Multipart` instance from a stream and the constraints. //! let mut multipart = Multipart::with_constraints(some_stream, "X-BOUNDARY", constraints); //! //! while let Some(field) = multipart.next_field().await.unwrap() { //! let content = field.text().await.unwrap(); //! assert_eq!(content, "abcd"); //! } //! # } //! # tokio::runtime::Runtime::new().unwrap().block_on(run()); //! ``` //! //! Please refer [`Constraints`] for more info. //! //! ## Usage with [hyper.rs](https://hyper.rs/) server //! //! An [example](https://github.com/rousan/multer-rs/blob/master/examples/hyper_server_example.rs) showing usage with [hyper.rs](https://hyper.rs/). //! //! For more examples, please visit [examples](https://github.com/rousan/multer-rs/tree/master/examples). #![forbid(unsafe_code)] #![warn( missing_debug_implementations, rust_2018_idioms, trivial_casts, unused_qualifications )] #![cfg_attr(nightly, feature(doc_cfg))] #![doc(test(attr(deny(rust_2018_idioms, warnings))))] #![doc(test(attr(allow(unused_extern_crates, unused_variables))))] pub use bytes; pub use constraints::Constraints; pub use error::Error; pub use field::Field; pub use multipart::Multipart; pub use size_limit::SizeLimit; mod buffer; mod constants; mod constraints; mod content_disposition; mod error; mod field; mod helpers; mod multipart; mod size_limit; /// A Result type often returned from methods that can have `multer` errors. pub type Result = std::result::Result; /// Parses the `Content-Type` header to extract the boundary value. /// /// # Examples /// /// ``` /// # fn run(){ /// let content_type = "multipart/form-data; boundary=ABCDEFG"; /// /// assert_eq!( /// multer::parse_boundary(content_type), /// Ok("ABCDEFG".to_owned()) /// ); /// # } /// # run(); /// ``` pub fn parse_boundary>(content_type: T) -> Result { let m = content_type .as_ref() .parse::() .map_err(crate::Error::DecodeContentType)?; if !(m.type_() == mime::MULTIPART && m.subtype() == mime::FORM_DATA) { return Err(crate::Error::NoMultipart); } m.get_param(mime::BOUNDARY) .map(|name| name.as_str().to_owned()) .ok_or(crate::Error::NoBoundary) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_boundary() { let content_type = "multipart/form-data; boundary=ABCDEFG"; assert_eq!(parse_boundary(content_type), Ok("ABCDEFG".to_owned())); let content_type = "multipart/form-data; boundary=------ABCDEFG"; assert_eq!(parse_boundary(content_type), Ok("------ABCDEFG".to_owned())); let content_type = "boundary=------ABCDEFG"; assert!(parse_boundary(content_type).is_err()); let content_type = "text/plain"; assert!(parse_boundary(content_type).is_err()); let content_type = "text/plain; boundary=------ABCDEFG"; assert!(parse_boundary(content_type).is_err()); } } multer-2.1.0/src/multipart.rs000064400000000000000000000415451046102023000143230ustar 00000000000000use std::sync::Arc; use std::task::{Context, Poll}; use bytes::Bytes; use futures_util::future; use futures_util::stream::{Stream, TryStreamExt}; use spin::mutex::spin::SpinMutex as Mutex; #[cfg(feature = "tokio-io")] use {tokio::io::AsyncRead, tokio_util::io::ReaderStream}; use crate::buffer::StreamBuffer; use crate::constraints::Constraints; use crate::content_disposition::ContentDisposition; use crate::error::Error; use crate::field::Field; use crate::{constants, helpers, Result}; /// Represents the implementation of `multipart/form-data` formatted data. /// /// This will parse the source stream into [`Field`] instances via /// [`next_field()`](Self::next_field). /// /// # Field Exclusivity /// /// A `Field` represents a raw, self-decoding stream into multipart data. As /// such, only _one_ `Field` from a given `Multipart` instance may be live at /// once. That is, a `Field` emitted by `next_field()` must be dropped before /// calling `next_field()` again. Failure to do so will result in an error. /// /// ```rust /// use std::convert::Infallible; /// /// use bytes::Bytes; /// use futures_util::stream::once; /// use multer::Multipart; /// /// # async fn run() { /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; \ /// name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// /// let field1 = multipart.next_field().await; /// let field2 = multipart.next_field().await; /// /// assert!(field2.is_err()); /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` /// /// # Examples /// /// ``` /// use std::convert::Infallible; /// /// use bytes::Bytes; /// use futures_util::stream::once; /// use multer::Multipart; /// /// # async fn run() { /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; \ /// name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// /// while let Some(field) = multipart.next_field().await.unwrap() { /// println!("Field: {:?}", field.text().await) /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` #[derive(Debug)] pub struct Multipart<'r> { state: Arc>>, } #[derive(Debug)] pub(crate) struct MultipartState<'r> { pub(crate) buffer: StreamBuffer<'r>, pub(crate) boundary: String, pub(crate) stage: StreamingStage, pub(crate) next_field_idx: usize, pub(crate) curr_field_name: Option, pub(crate) curr_field_size_limit: u64, pub(crate) curr_field_size_counter: u64, pub(crate) constraints: Constraints, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum StreamingStage { FindingFirstBoundary, ReadingBoundary, DeterminingBoundaryType, ReadingTransportPadding, ReadingFieldHeaders, ReadingFieldData, Eof, } impl<'r> Multipart<'r> { /// Construct a new `Multipart` instance with the given [`Bytes`] stream and /// the boundary. pub fn new(stream: S, boundary: B) -> Self where S: Stream> + Send + 'r, O: Into + 'static, E: Into> + 'r, B: Into, { Multipart::with_constraints(stream, boundary, Constraints::default()) } /// Construct a new `Multipart` instance with the given [`Bytes`] stream and /// the boundary. pub fn with_constraints(stream: S, boundary: B, constraints: Constraints) -> Self where S: Stream> + Send + 'r, O: Into + 'static, E: Into> + 'r, B: Into, { let stream = stream .map_ok(|b| b.into()) .map_err(|err| crate::Error::StreamReadFailed(err.into())); Multipart { state: Arc::new(Mutex::new(MultipartState { buffer: StreamBuffer::new(stream, constraints.size_limit.whole_stream), boundary: boundary.into(), stage: StreamingStage::FindingFirstBoundary, next_field_idx: 0, curr_field_name: None, curr_field_size_limit: constraints.size_limit.per_field, curr_field_size_counter: 0, constraints, })), } } /// Construct a new `Multipart` instance with the given [`AsyncRead`] reader /// and the boundary. /// /// # Optional /// /// This requires the optional `tokio-io` feature to be enabled. /// /// # Examples /// /// ``` /// use multer::Multipart; /// /// # async fn run() { /// let data = /// "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let reader = data.as_bytes(); /// let mut multipart = Multipart::with_reader(reader, "X-BOUNDARY"); /// /// while let Some(mut field) = multipart.next_field().await.unwrap() { /// while let Some(chunk) = field.chunk().await.unwrap() { /// println!("Chunk: {:?}", chunk); /// } /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` #[cfg(feature = "tokio-io")] #[cfg_attr(nightly, doc(cfg(feature = "tokio-io")))] pub fn with_reader(reader: R, boundary: B) -> Self where R: AsyncRead + Unpin + Send + 'r, B: Into, { let stream = ReaderStream::new(reader); Multipart::new(stream, boundary) } /// Construct a new `Multipart` instance with the given [`AsyncRead`] reader /// and the boundary. /// /// # Optional /// /// This requires the optional `tokio-io` feature to be enabled. /// /// # Examples /// /// ``` /// use multer::Multipart; /// /// # async fn run() { /// let data = /// "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let reader = data.as_bytes(); /// let mut multipart = Multipart::with_reader(reader, "X-BOUNDARY"); /// /// while let Some(mut field) = multipart.next_field().await.unwrap() { /// while let Some(chunk) = field.chunk().await.unwrap() { /// println!("Chunk: {:?}", chunk); /// } /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` #[cfg(feature = "tokio-io")] #[cfg_attr(nightly, doc(cfg(feature = "tokio-io")))] pub fn with_reader_with_constraints(reader: R, boundary: B, constraints: Constraints) -> Self where R: AsyncRead + Unpin + Send + 'r, B: Into, { let stream = ReaderStream::new(reader); Multipart::with_constraints(stream, boundary, constraints) } /// Yields the next [`Field`] if available. /// /// Any previous `Field` returned by this method must be dropped before /// calling this method or [`Multipart::next_field_with_idx()`] again. See /// [field-exclusivity](#field-exclusivity) for details. pub async fn next_field(&mut self) -> Result>> { future::poll_fn(|cx| self.poll_next_field(cx)).await } /// Yields the next [`Field`] if available. /// /// Any previous `Field` returned by this method must be dropped before /// calling this method or [`Multipart::next_field_with_idx()`] again. See /// [field-exclusivity](#field-exclusivity) for details. /// /// This method is available since version 2.1.0. pub fn poll_next_field(&mut self, cx: &mut Context<'_>) -> Poll>>> { // This is consistent as we have an `&mut` and `Field` is not `Clone`. // Here, we are guaranteeing that the returned `Field` will be the // _only_ field with access to the multipart parsing state. This ensure // that lock failure can never occur. This is effectively a dynamic // version of passing an `&mut` of `self` to the `Field`. if Arc::strong_count(&self.state) != 1 { return Poll::Ready(Err(Error::LockFailure)); } debug_assert_eq!(Arc::strong_count(&self.state), 1); debug_assert!(self.state.try_lock().is_some(), "expected exlusive lock"); let mut lock = match self.state.try_lock() { Some(lock) => lock, None => return Poll::Ready(Err(Error::LockFailure)), }; let state = &mut *lock; if state.stage == StreamingStage::Eof { return Poll::Ready(Ok(None)); } if let Err(err) = state.buffer.poll_stream(cx) { return Poll::Ready(Err(crate::Error::StreamReadFailed(err.into()))); } if state.stage == StreamingStage::FindingFirstBoundary { let boundary = &state.boundary; let boundary_deriv = format!("{}{}", constants::BOUNDARY_EXT, boundary); match state.buffer.read_to(boundary_deriv.as_bytes()) { Some(_) => state.stage = StreamingStage::ReadingBoundary, None => { if let Err(err) = state.buffer.poll_stream(cx) { return Poll::Ready(Err(crate::Error::StreamReadFailed(err.into()))); } if state.buffer.eof { return Poll::Ready(Err(crate::Error::IncompleteStream)); } } } } // The previous field did not finish reading its data. if state.stage == StreamingStage::ReadingFieldData { match state .buffer .read_field_data(state.boundary.as_str(), state.curr_field_name.as_deref())? { Some((done, bytes)) => { state.curr_field_size_counter += bytes.len() as u64; if state.curr_field_size_counter > state.curr_field_size_limit { return Poll::Ready(Err(crate::Error::FieldSizeExceeded { limit: state.curr_field_size_limit, field_name: state.curr_field_name.clone(), })); } if done { state.stage = StreamingStage::ReadingBoundary; } else { return Poll::Pending; } } None => { return Poll::Pending; } } } if state.stage == StreamingStage::ReadingBoundary { let boundary = &state.boundary; let boundary_deriv_len = constants::BOUNDARY_EXT.len() + boundary.len(); let boundary_bytes = match state.buffer.read_exact(boundary_deriv_len) { Some(bytes) => bytes, None => { return if state.buffer.eof { Poll::Ready(Err(crate::Error::IncompleteStream)) } else { Poll::Pending }; } }; if &boundary_bytes[..] == format!("{}{}", constants::BOUNDARY_EXT, boundary).as_bytes() { state.stage = StreamingStage::DeterminingBoundaryType; } else { return Poll::Ready(Err(crate::Error::IncompleteStream)); } } if state.stage == StreamingStage::DeterminingBoundaryType { let ext_len = constants::BOUNDARY_EXT.len(); let next_bytes = match state.buffer.peek_exact(ext_len) { Some(bytes) => bytes, None => { return if state.buffer.eof { Poll::Ready(Err(crate::Error::IncompleteStream)) } else { Poll::Pending }; } }; if next_bytes == constants::BOUNDARY_EXT.as_bytes() { state.stage = StreamingStage::Eof; return Poll::Ready(Ok(None)); } else { state.stage = StreamingStage::ReadingTransportPadding; } } if state.stage == StreamingStage::ReadingTransportPadding { if !state.buffer.advance_past_transport_padding() { return if state.buffer.eof { Poll::Ready(Err(crate::Error::IncompleteStream)) } else { Poll::Pending }; } let crlf_len = constants::CRLF.len(); let crlf_bytes = match state.buffer.read_exact(crlf_len) { Some(bytes) => bytes, None => { return if state.buffer.eof { Poll::Ready(Err(crate::Error::IncompleteStream)) } else { Poll::Pending }; } }; if &crlf_bytes[..] == constants::CRLF.as_bytes() { state.stage = StreamingStage::ReadingFieldHeaders; } else { return Poll::Ready(Err(crate::Error::IncompleteStream)); } } if state.stage == StreamingStage::ReadingFieldHeaders { let header_bytes = match state.buffer.read_until(constants::CRLF_CRLF.as_bytes()) { Some(bytes) => bytes, None => { return if state.buffer.eof { return Poll::Ready(Err(crate::Error::IncompleteStream)); } else { Poll::Pending }; } }; let mut headers = [httparse::EMPTY_HEADER; constants::MAX_HEADERS]; let headers = match httparse::parse_headers(&header_bytes, &mut headers).map_err(crate::Error::ReadHeaderFailed)? { httparse::Status::Complete((_, raw_headers)) => { match helpers::convert_raw_headers_to_header_map(raw_headers) { Ok(headers) => headers, Err(err) => { return Poll::Ready(Err(err)); } } } httparse::Status::Partial => { return Poll::Ready(Err(crate::Error::IncompleteHeaders)); } }; state.stage = StreamingStage::ReadingFieldData; let field_idx = state.next_field_idx; state.next_field_idx += 1; let content_disposition = ContentDisposition::parse(&headers); let field_size_limit = state .constraints .size_limit .extract_size_limit_for(content_disposition.field_name.as_deref()); state.curr_field_name = content_disposition.field_name.clone(); state.curr_field_size_limit = field_size_limit; state.curr_field_size_counter = 0; let field_name = content_disposition.field_name.as_deref(); if !state.constraints.is_it_allowed(field_name) { return Poll::Ready(Err(crate::Error::UnknownField { field_name: field_name.map(str::to_owned), })); } drop(lock); // The lock will be dropped anyway, but let's be explicit. let field = Field::new(self.state.clone(), headers, field_idx, content_disposition); return Poll::Ready(Ok(Some(field))); } Poll::Pending } /// Yields the next [`Field`] with their positioning index as a tuple /// `(`[`usize`]`, `[`Field`]`)`. /// /// Any previous `Field` returned by this method must be dropped before /// calling this method or [`Multipart::next_field()`] again. See /// [field-exclusivity](#field-exclusivity) for details. /// /// # Examples /// /// ``` /// use std::convert::Infallible; /// /// use bytes::Bytes; /// use futures_util::stream::once; /// use multer::Multipart; /// /// # async fn run() { /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; \ /// name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// /// while let Some((idx, field)) = multipart.next_field_with_idx().await.unwrap() { /// println!("Index: {:?}, Content: {:?}", idx, field.text().await) /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` pub async fn next_field_with_idx(&mut self) -> Result)>> { self.next_field().await.map(|f| f.map(|field| (field.index(), field))) } } multer-2.1.0/src/size_limit.rs000064400000000000000000000034761046102023000144530ustar 00000000000000use std::collections::HashMap; use crate::constants; /// Represents size limit of the stream to prevent DoS attacks. /// /// Please refer [`Constraints`](crate::Constraints) for more info. #[derive(Debug)] pub struct SizeLimit { pub(crate) whole_stream: u64, pub(crate) per_field: u64, pub(crate) field_map: HashMap, } impl SizeLimit { /// Creates a default size limit which is [`u64::MAX`] for the whole stream /// and for each field. pub fn new() -> SizeLimit { SizeLimit::default() } /// Sets size limit for the whole stream. pub fn whole_stream(mut self, limit: u64) -> SizeLimit { self.whole_stream = limit; self } /// Sets size limit for each field. pub fn per_field(mut self, limit: u64) -> SizeLimit { self.per_field = limit; self } /// Sets size limit for a specific field, it overrides the /// [`per_field`](Self::per_field) value for this field. /// /// It is useful when you want to set a size limit on a textual field which /// will be stored in memory to avoid potential DoS attacks from /// attackers running the server out of memory. pub fn for_field>(mut self, field_name: N, limit: u64) -> SizeLimit { self.field_map.insert(field_name.into(), limit); self } pub(crate) fn extract_size_limit_for(&self, field: Option<&str>) -> u64 { field .and_then(|field| self.field_map.get(&field.to_owned())) .copied() .unwrap_or(self.per_field) } } impl Default for SizeLimit { fn default() -> Self { SizeLimit { whole_stream: constants::DEFAULT_WHOLE_STREAM_SIZE_LIMIT, per_field: constants::DEFAULT_PER_FIELD_SIZE_LIMIT, field_map: HashMap::default(), } } } multer-2.1.0/tests/integration.rs000064400000000000000000000301531046102023000151710ustar 00000000000000use bytes::Bytes; use futures_util::{stream, Stream}; use multer::{Constraints, Multipart, SizeLimit}; fn str_stream(string: &'static str) -> impl Stream> { stream::iter( string .chars() .map(|ch| ch.to_string()) .map(|part| Ok(Bytes::copy_from_slice(part.as_bytes()))), ) } #[tokio::test] async fn test_multipart_basic() { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = str_stream(data); let mut m = Multipart::new(stream, "X-BOUNDARY"); while let Some((idx, field)) = m.next_field_with_idx().await.unwrap() { if idx == 0 { assert_eq!(field.name(), Some("my_text_field")); assert_eq!(field.file_name(), None); assert_eq!(field.content_type(), None); assert_eq!(field.index(), 0); assert_eq!(field.text().await, Ok("abcd".to_owned())); } else if idx == 1 { assert_eq!(field.name(), Some("my_file_field")); assert_eq!(field.file_name(), Some("a-text-file.txt")); assert_eq!(field.content_type(), Some(&mime::TEXT_PLAIN)); assert_eq!(field.index(), 1); assert_eq!(field.text().await, Ok("Hello world\nHello\r\nWorld\rAgain".to_owned())); } } } #[tokio::test] async fn test_multipart_empty() { let data = "--X-BOUNDARY--\r\n"; let stream = str_stream(data); let mut m = Multipart::new(stream, "X-BOUNDARY"); assert!(m.next_field().await.unwrap().is_none()); assert!(m.next_field().await.unwrap().is_none()); } #[tokio::test] async fn test_multipart_clean_field() { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = str_stream(data); let mut m = Multipart::new(stream, "X-BOUNDARY"); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_none()); } #[tokio::test] async fn test_multipart_transport_padding() { let data = "--X-BOUNDARY \t \r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY \r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\t\t\t\t\t\r\n"; let stream = str_stream(data); let mut m = Multipart::new(stream, "X-BOUNDARY"); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_none()); let bad_data = "--X-BOUNDARY \t \r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARYzz \r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\t\t\t\t\t\r\n"; let bad_stream = str_stream(bad_data); let mut m = Multipart::new(bad_stream, "X-BOUNDARY"); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.is_err()); } #[tokio::test] async fn test_multipart_header() { let should_pass = [ "ignored header\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n", "\r\nignored header\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n", "\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n", "\r\n\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n", ]; for data in should_pass.iter() { let stream = str_stream(data); let mut m = Multipart::new(stream, "X-BOUNDARY"); assert_eq!( m.next_field().await.unwrap().unwrap().text().await.unwrap(), "abcd".to_owned() ); } } #[tokio::test] async fn test_multipart_constraint_allowed_fields_normal() { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = str_stream(data); let constraints = Constraints::new().allowed_fields(vec!["my_text_field", "my_file_field"]); let mut m = Multipart::with_constraints(stream, "X-BOUNDARY", constraints); assert_eq!( m.next_field().await.unwrap().unwrap().text().await.unwrap(), "abcd".to_owned() ); assert_eq!( m.next_field().await.unwrap().unwrap().text().await.unwrap(), "Hello world\nHello\r\nWorld\rAgain".to_owned() ); } #[tokio::test] #[should_panic] async fn test_multipart_constraint_allowed_fields_unknown_field() { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = str_stream(data); let constraints = Constraints::new().allowed_fields(vec!["my_text_field"]); let mut m = Multipart::with_constraints(stream, "X-BOUNDARY", constraints); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_none()); } #[tokio::test] async fn test_multipart_constraint_size_limit_whole_stream() { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = str_stream(data); let constraints = Constraints::new() .allowed_fields(vec!["my_text_field", "my_file_field"]) .size_limit(SizeLimit::new().whole_stream(248)); let mut m = Multipart::with_constraints(stream, "X-BOUNDARY", constraints); assert_eq!( m.next_field().await.unwrap().unwrap().text().await.unwrap(), "abcd".to_owned() ); assert_eq!( m.next_field().await.unwrap().unwrap().text().await.unwrap(), "Hello world\nHello\r\nWorld\rAgain".to_owned() ); } #[tokio::test] #[should_panic] async fn test_multipart_constraint_size_limit_whole_stream_size_exceeded() { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = str_stream(data); let constraints = Constraints::new() .allowed_fields(vec!["my_text_field", "my_file_field"]) .size_limit(SizeLimit::new().whole_stream(100)); let mut m = Multipart::with_constraints(stream, "X-BOUNDARY", constraints); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_none()); } #[tokio::test] async fn test_multipart_constraint_size_limit_per_field() { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = str_stream(data); let constraints = Constraints::new() .allowed_fields(vec!["my_text_field", "my_file_field"]) .size_limit(SizeLimit::new().whole_stream(248).per_field(100)); let mut m = Multipart::with_constraints(stream, "X-BOUNDARY", constraints); assert_eq!( m.next_field().await.unwrap().unwrap().text().await.unwrap(), "abcd".to_owned() ); assert_eq!( m.next_field().await.unwrap().unwrap().text().await.unwrap(), "Hello world\nHello\r\nWorld\rAgain".to_owned() ); } #[tokio::test] #[should_panic] async fn test_multipart_constraint_size_limit_per_field_size_exceeded() { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = str_stream(data); let constraints = Constraints::new() .allowed_fields(vec!["my_text_field", "my_file_field"]) .size_limit(SizeLimit::new().whole_stream(248).per_field(10)); let mut m = Multipart::with_constraints(stream, "X-BOUNDARY", constraints); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_none()); } #[tokio::test] async fn test_multipart_constraint_size_limit_for_field() { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = str_stream(data); let constraints = Constraints::new() .allowed_fields(vec!["my_text_field", "my_file_field"]) .size_limit( SizeLimit::new() .whole_stream(248) .per_field(100) .for_field("my_text_field", 4) .for_field("my_file_field", 30), ); let mut m = Multipart::with_constraints(stream, "X-BOUNDARY", constraints); assert_eq!( m.next_field().await.unwrap().unwrap().text().await.unwrap(), "abcd".to_owned() ); assert_eq!( m.next_field().await.unwrap().unwrap().text().await.unwrap(), "Hello world\nHello\r\nWorld\rAgain".to_owned() ); } #[tokio::test] #[should_panic] async fn test_multipart_constraint_size_limit_for_field_size_exceeded() { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = str_stream(data); let constraints = Constraints::new() .allowed_fields(vec!["my_text_field", "my_file_field"]) .size_limit( SizeLimit::new() .whole_stream(248) .per_field(100) .for_field("my_text_field", 4) .for_field("my_file_field", 10), ); let mut m = Multipart::with_constraints(stream, "X-BOUNDARY", constraints); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_none()); } #[tokio::test] async fn test_multiaccess_caught() { let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = str_stream(data); let mut m = Multipart::new(stream, "X-BOUNDARY"); let field1 = m.next_field().await; let field2 = m.next_field().await; assert!(matches!(field2.unwrap_err(), multer::Error::LockFailure)); assert!(field1.is_ok()); } multer-2.1.0/tusk.yml000064400000000000000000000010431046102023000126430ustar 00000000000000options: version: usage: The next release version short: v required: true tasks: setup: run: - command: cargo install cargo-watch - command: cargo install releez check:dev: run: - command: cargo watch --watch ./src -x 'check --features="all"' doc:dev: run: - command: cargo doc --open --features="all" - command: cargo watch -x 'doc --features="all"' test:dev: run: - command: cargo watch -x 'test --features="all"' release: run: - command: releez "${version}"