From 3de77ddb116266dd68b17e78bfe8595cbaebbf16 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 14 Jun 2025 00:16:29 +0000 Subject: [PATCH 1/7] Merge https://github.com/abonander/multipart.git into master Squash merge the `multipart` repository into `rouille` so FCWs can be resolved and unneeded features stripped out. Original HEAD ref: f4fee608af ("Merge pull request #134 ...") --- multipart/.gitignore | 19 + multipart/.travis.yml | 28 + multipart/Cargo.toml | 90 ++ multipart/LICENSE | 22 + multipart/LICENSE-APACHE | 201 +++++ multipart/LICENSE-MIT | 25 + multipart/README.md | 90 ++ multipart/examples/README.md | 115 +++ multipart/examples/hyper_client.rs | 44 + multipart/examples/hyper_reqbuilder.rs | 19 + multipart/examples/hyper_server.rs | 51 ++ multipart/examples/iron.rs | 66 ++ multipart/examples/iron_intercept.rs | 25 + multipart/examples/nickel.rs | 63 ++ multipart/examples/rocket.rs | 85 ++ multipart/examples/tiny_http.rs | 78 ++ multipart/fuzz/.gitignore | 5 + multipart/fuzz/Cargo.lock | 206 +++++ multipart/fuzz/Cargo.toml | 27 + multipart/fuzz/fuzzer_dict | 7 + multipart/fuzz/fuzzers/logger.rs | 28 + multipart/fuzz/fuzzers/server_basic.rs | 57 ++ multipart/fuzz_server.sh | 3 + multipart/lorem_ipsum.txt | 7 + multipart/src/bin/form_test.rs | 33 + multipart/src/bin/read_file.rs | 75 ++ multipart/src/bin/test_form.html | 14 + multipart/src/client/hyper.rs | 83 ++ multipart/src/client/lazy.rs | 546 +++++++++++++ multipart/src/client/mod.rs | 315 +++++++ multipart/src/client/sized.rs | 90 ++ multipart/src/lib.rs | 131 +++ multipart/src/local_test.rs | 497 +++++++++++ multipart/src/mock.rs | 203 +++++ multipart/src/server/boundary.rs | 598 ++++++++++++++ multipart/src/server/field.rs | 603 ++++++++++++++ multipart/src/server/hyper.rs | 139 ++++ multipart/src/server/iron.rs | 284 +++++++ multipart/src/server/mod.rs | 290 +++++++ multipart/src/server/nickel.rs | 69 ++ multipart/src/server/save.rs | 1045 ++++++++++++++++++++++++ multipart/src/server/tiny_http.rs | 36 + 42 files changed, 6412 insertions(+) create mode 100644 multipart/.gitignore create mode 100644 multipart/.travis.yml create mode 100644 multipart/Cargo.toml create mode 100644 multipart/LICENSE create mode 100644 multipart/LICENSE-APACHE create mode 100644 multipart/LICENSE-MIT create mode 100644 multipart/README.md create mode 100644 multipart/examples/README.md create mode 100644 multipart/examples/hyper_client.rs create mode 100644 multipart/examples/hyper_reqbuilder.rs create mode 100644 multipart/examples/hyper_server.rs create mode 100644 multipart/examples/iron.rs create mode 100644 multipart/examples/iron_intercept.rs create mode 100644 multipart/examples/nickel.rs create mode 100644 multipart/examples/rocket.rs create mode 100644 multipart/examples/tiny_http.rs create mode 100644 multipart/fuzz/.gitignore create mode 100644 multipart/fuzz/Cargo.lock create mode 100644 multipart/fuzz/Cargo.toml create mode 100644 multipart/fuzz/fuzzer_dict create mode 100644 multipart/fuzz/fuzzers/logger.rs create mode 100644 multipart/fuzz/fuzzers/server_basic.rs create mode 100755 multipart/fuzz_server.sh create mode 100644 multipart/lorem_ipsum.txt create mode 100644 multipart/src/bin/form_test.rs create mode 100644 multipart/src/bin/read_file.rs create mode 100644 multipart/src/bin/test_form.html create mode 100644 multipart/src/client/hyper.rs create mode 100644 multipart/src/client/lazy.rs create mode 100644 multipart/src/client/mod.rs create mode 100644 multipart/src/client/sized.rs create mode 100644 multipart/src/lib.rs create mode 100644 multipart/src/local_test.rs create mode 100644 multipart/src/mock.rs create mode 100644 multipart/src/server/boundary.rs create mode 100644 multipart/src/server/field.rs create mode 100644 multipart/src/server/hyper.rs create mode 100644 multipart/src/server/iron.rs create mode 100644 multipart/src/server/mod.rs create mode 100644 multipart/src/server/nickel.rs create mode 100644 multipart/src/server/save.rs create mode 100644 multipart/src/server/tiny_http.rs diff --git a/multipart/.gitignore b/multipart/.gitignore new file mode 100644 index 000000000..cb8fc54a6 --- /dev/null +++ b/multipart/.gitignore @@ -0,0 +1,19 @@ +/target +/Cargo.lock +*.swp +*~ +# Compiled files +*.o +*.so +*.rlib +*.dll + +# Executables +*.exe + +# Generated by Cargo +target/ +.idea/ +*.iml + +dump.bin diff --git a/multipart/.travis.yml b/multipart/.travis.yml new file mode 100644 index 000000000..bf229ebb6 --- /dev/null +++ b/multipart/.travis.yml @@ -0,0 +1,28 @@ +language: rust +cache: cargo +branches: + except: + - fuzzing +rust: + - 1.33.0 + - stable + - beta + - nightly +os: + - linux + - osx +env: + global: + - RUST_LOG=multipart=trace RUST_BACKTRACE=1 ARGS= +matrix: + include: + - rust: stable + env: ARGS+=--no-default-features --features "nickel" + - rust: stable + env: ARGS+=--features "use_arc_str" + - rust: nightly + env: ARGS+=--features "nightly,rocket" +script: + - cargo build --verbose $ARGS; + - cargo test --verbose $ARGS -- --test-threads=1; + - cargo doc --verbose $ARGS; diff --git a/multipart/Cargo.toml b/multipart/Cargo.toml new file mode 100644 index 000000000..7516948cf --- /dev/null +++ b/multipart/Cargo.toml @@ -0,0 +1,90 @@ +[package] +name = "multipart" + +version = "0.18.0" + +authors = ["Austin Bonander "] + +description = "A backend-agnostic extension for HTTP libraries that provides support for POST multipart/form-data requests on both client and server." + +keywords = ["form-data", "hyper", "iron", "http", "upload"] + +repository = "http://github.com/abonander/multipart" + +documentation = "http://docs.rs/multipart/" + +license = "MIT OR Apache-2.0" + +readme = "README.md" + +[dependencies] +lazy_static = { version = "1.2.0", optional = true } +log = "0.4" +mime = "0.3.14" +mime_guess = "2.0.1" +rand = "0.8" +safemem = { version = "0.3", optional = true } +tempfile = "3" +clippy = { version = ">=0.0, <0.1", optional = true} + +#Server Dependencies +buf_redux = { version = "0.8", optional = true, default-features = false } +httparse = { version = "1.2", optional = true } +twoway = { version = "0.1", optional = true } +quick-error = { version = "1.2", optional = true } + +# Optional Integrations +hyper = { version = ">=0.9, <0.11", optional = true, default-features = false } +iron = { version = ">=0.4,<0.7", optional = true } +tiny_http = { version = "0.6", optional = true } +nickel = { version = ">=0.10.1", optional = true } + +# Only for Rocket example but dev-dependencies can't be optional +rocket = { version = "0.4", optional = true } + +[dev-dependencies] +env_logger = "0.5" + +[features] +client = [] +default = ["client", "hyper", "iron", "mock", "nickel", "server", "tiny_http"] +server = ["buf_redux", "httparse", "quick-error", "safemem", "twoway"] +mock = [] +nightly = [] +bench = [] + +[[example]] +name = "hyper_client" +required-features = ["client", "mock", "hyper"] + +[[example]] +name = "hyper_reqbuilder" +required-features = ["client", "mock", "hyper"] + +[[example]] +name = "hyper_server" +required-features = ["mock", "hyper", "server"] + +[[example]] +name = "iron" +required-features = ["mock", "iron", "server"] + +[[example]] +name = "iron_intercept" +required-features = ["mock", "iron", "server"] + +[[example]] +name = "nickel" +required-features = ["mock", "nickel", "server"] + +[[example]] +name = "tiny_http" +required-features = ["mock", "tiny_http", "server"] + +[[example]] +name = "rocket" +required-features = ["mock", "rocket", "server"] + +[[bin]] +name = "form_test" +required-features = ["mock", "hyper", "server"] diff --git a/multipart/LICENSE b/multipart/LICENSE new file mode 100644 index 000000000..f0b4b9e27 --- /dev/null +++ b/multipart/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Austin Bonander + +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. + diff --git a/multipart/LICENSE-APACHE b/multipart/LICENSE-APACHE new file mode 100644 index 000000000..16fe87b06 --- /dev/null +++ b/multipart/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/multipart/LICENSE-MIT b/multipart/LICENSE-MIT new file mode 100644 index 000000000..3145ad975 --- /dev/null +++ b/multipart/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2016 The `multipart` Crate Developers + +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. diff --git a/multipart/README.md b/multipart/README.md new file mode 100644 index 000000000..a97f80581 --- /dev/null +++ b/multipart/README.md @@ -0,0 +1,90 @@ +# Multipart [![Build Status](https://travis-ci.org/abonander/multipart.svg?branch=master)](https://travis-ci.org/abonander/multipart) [![On Crates.io](https://img.shields.io/crates/v/multipart.svg)](https://crates.io/crates/multipart) + +Client- and server-side abstractions for HTTP file uploads (POST requests with `Content-Type: multipart/form-data`). + +Supports several different (**sync**hronous API) HTTP crates. +**Async**hronous (i.e. `futures`-based) API support will be provided by [multipart-async]. + +##### Minimum supported Rust version: 1.36.0 + +##### Maintenance Status: Passive + +As the web ecosystem in Rust moves towards asynchronous APIs, the need for this crate in synchronous +API form becomes dubious. This crate in its current form is usable enough, so as of June 2020 it +is now in passive maintenance mode; bug reports will be addressed as time permits and PRs will be +accepted but otherwise no new development of the existing API is taking place. + +Look for a release of [multipart-async] soon which targets newer releases of Hyper. + +### [Documentation](http://docs.rs/multipart/) + +## Integrations + +Example files demonstrating how to use `multipart` with these crates are available under [`examples/`](examples). + +### [Hyper ![](https://img.shields.io/crates/v/hyper.svg)](https://crates.io/crates/hyper) +via the `hyper` feature (enabled by default). + +**Note: Hyper 0.9, 0.10 (synchronous API) only**; support for asynchronous APIs will be provided by [multipart-async]. + +Client integration includes support for regular `hyper::client::Request` objects via `multipart::client::Multipart`, as well +as integration with the new `hyper::Client` API via `multipart::client::lazy::Multipart` (new in 0.5). + +Server integration for `hyper::server::Request` via `multipart::server::Multipart`. + +### [Iron ![](https://img.shields.io/crates/v/iron.svg)](https://crates.io/crates/iron) +via the `iron` feature. + +Provides regular server-side integration with `iron::Request` via `multipart::server::Multipart`, +as well as a convenient `BeforeMiddleware` implementation in `multipart::server::iron::Intercept`. + +### [Nickel ![](https://img.shields.io/crates/v/nickel.svg)](https://crates.io/crates/nickel) returning to `multipart` in 0.14! +via the `nickel` feature. + +Provides server-side integration with `&mut nickel::Request` via `multipart::server::Multipart`. + +### [tiny_http ![](https://img.shields.io/crates/v/tiny_http.svg)](https://crates.io/crates/tiny_http) +via the `tiny_http` feature. + +Provides server-side integration with `tiny_http::Request` via `multipart::server::Multipart`. + +### [Rocket ![](https://img.shields.io/crates/v/rocket.svg)](https://crates.io/crates/rocket) + +Direct integration is not provided as the Rocket folks seem to want to handle `multipart/form-data` +behind the scenes which would supercede any integration with `multipart`. However, an example is available +showing how to use `multipart` on a Rocket server: [examples/rocket.rs](examples/rocket.rs) + +## ⚡ Powered By ⚡ + +### [buf_redux ![](https://img.shields.io/crates/v/buf_redux.svg)](https://crates.io/crates/buf_redux) + +Customizable drop-in `std::io::BufReader` replacement, created to be used in this crate. +Needed because it can read more bytes into the buffer without the buffer being empty, necessary +when a boundary falls across two reads. (It was easier to author a new crate than try to get this added +to `std::io::BufReader`.) + +### [httparse ![](https://img.shields.io/crates/v/httparse.svg)](https://crates.io/crates/httparse) + +Fast, zero-copy HTTP header parsing, used to read field headers in `multipart/form-data` request bodies. + +### [twoway ![](https://img.shields.io/crates/v/twoway.svg)](https://crates.io/crates/twoway) + +Fast string and byte-string search. Used to find boundaries in the request body. Uses SIMD acceleration +when possible. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any +additional terms or conditions. + +[multipart-async]: https://github.com/abonander/multipart-async diff --git a/multipart/examples/README.md b/multipart/examples/README.md new file mode 100644 index 000000000..580d3bbdc --- /dev/null +++ b/multipart/examples/README.md @@ -0,0 +1,115 @@ +`multipart` Examples +=========================== + +These example files show how to use `multipart` with the various crates it integrates with. + +These files carry the same licenses as [`multipart` itself](https://github.com/abonander/multipart#license), though this may be lightened to a copyright-free license in the near future. + +## Client + +Examples for the client-side integrations of `multipart`'s API. + +[`hyper_client`](hyper_client.rs) +--------------------------------- +Author: [abonander] + +This example showcases usage of `multipart` with the `hyper::client::Request` API. + +``` +$ cargo run --example hyper_client +``` + +[`hyper_reqbuilder`](hyper_reqbuilder.rs) +----------------------------------------- +Author: [abonander] + +This example showcases usage of `multipart` with Hyper's new `Client` API, +via the lazy-writing capabilities of `multipart::client::lazy`. + +``` +$ cargo run --example hyper_reqbuilder +``` + +## Server + +[`hyper_server`](hyper_server.rs) +--------------------------------- +Author: [Puhrez] + +This example shows how to use `multipart` with a [`hyper::Server`] (http://hyper.rs/) to intercept multipart requests. + +``` +$ cargo run --example hyper_server +``` + +[`iron`](iron.rs) +----------------- +Author: [White-Oak] + +This example shows how to use `multipart` with the [Iron web application framework](https://github.com/iron/iron), via `multipart`'s support +for the `iron::Request` type. + +To run: + +``` +$ cargo run --features iron --example iron +``` + +[`iron_intercept`](iron_intercept.rs) +------------------------------------- +Author: [abonander] + +This example shows how to use `multipart`'s specialized `Intercept` middleware with Iron, which reads out all fields and +files to local storage so they can be accessed arbitrarily. + +``` +$ cargo run --features iron --example iron_intercept +``` + +[`tiny_http`](tiny_http.rs) +--------------------------- +Author: [White-Oak] + +This example shows how to use `multipart` with the [`tiny_http` crate](https://crates.io/crates/tiny_http), via `multipart`'s support for the `tiny_http::Request` type. + +``` +$ cargo run --features tiny_http --example tiny_http +``` + +[`hyper_server`](hyper_server.rs) +--------------------------------- +Author: [Puhrez] + +This example shows how to use `multipart` with a [`hyper::Server`] (http://hyper.rs/) to intercept multipart requests. + +``` +$ cargo run --example hyper_server +``` + +[`nickel`](nickel.rs) +--------------------- +Author: [iamsebastian] + +This example shows how to use `multipart` to handle multipart uploads in [nickel.rs](https://nickel.rs). + +``` +$ cargo run --example nickel --features nickel +``` + +[Rocket](rocket.rs) +------------------- +Author: [abonander] + +This example shows how `multipart`'s server API can be used with [Rocket](https://rocket.rs) without +explicit support (the Rocket folks seem to want to handle `multipart/form-data` behind the scenes +but haven't gotten around to implementing it yet; this would supercede any integration from `multipart`). + +``` +$ cargo run --example rocket --features "rocket" +``` + +[iamsebastian]: https://github.com/iamsebastian +[Puhrez]: https://github.com/puhrez +[White-Oak]: https://github.com/white-oak +[abonander]: https://github.com/abonander + diff --git a/multipart/examples/hyper_client.rs b/multipart/examples/hyper_client.rs new file mode 100644 index 000000000..8e968f758 --- /dev/null +++ b/multipart/examples/hyper_client.rs @@ -0,0 +1,44 @@ +extern crate hyper; +extern crate multipart; + +use hyper::client::Request; +use hyper::method::Method; +use hyper::net::Streaming; + +use multipart::client::Multipart; + +use std::io::Read; + +fn main() { + let url = "http://localhost:80".parse() + .expect("Failed to parse URL"); + + let request = Request::new(Method::Post, url) + .expect("Failed to create request"); + + let mut multipart = Multipart::from_request(request) + .expect("Failed to create Multipart"); + + write_body(&mut multipart) + .expect("Failed to write multipart body"); + + let mut response = multipart.send().expect("Failed to send multipart request"); + + if !response.status.is_success() { + let mut res = String::new(); + response.read_to_string(&mut res).expect("failed to read response"); + println!("response reported unsuccessful: {:?}\n {}", response, res); + } + + // Optional: read out response +} + +fn write_body(multi: &mut Multipart>) -> hyper::Result<()> { + let mut binary = "Hello world from binary!".as_bytes(); + + multi.write_text("text", "Hello, world!")?; + multi.write_file("file", "lorem_ipsum.txt")?; + // &[u8] impl Read + multi.write_stream("binary", &mut binary, None, None) + .and(Ok(())) +} diff --git a/multipart/examples/hyper_reqbuilder.rs b/multipart/examples/hyper_reqbuilder.rs new file mode 100644 index 000000000..05c5539c8 --- /dev/null +++ b/multipart/examples/hyper_reqbuilder.rs @@ -0,0 +1,19 @@ +extern crate hyper; +extern crate multipart; + +use hyper::Client; + +use multipart::client::lazy::Multipart; + +fn main() { + let mut binary = "Hello world in binary!".as_bytes(); + + let _response = Multipart::new() + .add_text("text", "Hello, world!") + .add_file("file", "lorem_ipsum.txt") + // A little extra type info needed. + .add_stream("binary", &mut binary, None as Option<&str>, None) + // Request is sent here + .client_request(&Client::new(), "http://localhost:80") + .expect("Error sending multipart request"); +} \ No newline at end of file diff --git a/multipart/examples/hyper_server.rs b/multipart/examples/hyper_server.rs new file mode 100644 index 000000000..56ebc8a88 --- /dev/null +++ b/multipart/examples/hyper_server.rs @@ -0,0 +1,51 @@ +extern crate hyper; +extern crate multipart; + +use std::io; +use hyper::server::{Handler, Server, Request, Response}; +use hyper::status::StatusCode; +use hyper::server::response::Response as HyperResponse; +use multipart::server::hyper::{Switch, MultipartHandler, HyperRequest}; +use multipart::server::{Multipart, Entries, SaveResult}; +use multipart::mock::StdoutTee; + +struct NonMultipart; +impl Handler for NonMultipart { + fn handle(&self, _: Request, mut res: Response) { + *res.status_mut() = StatusCode::ImATeapot; + res.send(b"Please send a multipart req :(\n").unwrap(); + } +} + +struct EchoMultipart; +impl MultipartHandler for EchoMultipart { + fn handle_multipart(&self, mut multipart: Multipart, res: HyperResponse) { + match multipart.save().temp() { + SaveResult::Full(entries) => process_entries(res, entries).unwrap(), + SaveResult::Partial(entries, error) => { + println!("Errors saving multipart:\n{:?}", error); + process_entries(res, entries.into()).unwrap(); + } + SaveResult::Error(error) => { + println!("Errors saving multipart:\n{:?}", error); + res.send(format!("An error occurred {}", error).as_bytes()).unwrap(); + } + }; + } +} + +fn process_entries(res: HyperResponse, entries: Entries) -> io::Result<()> { + let mut res = res.start()?; + let stdout = io::stdout(); + let out = StdoutTee::new(&mut res, &stdout); + entries.write_debug(out) +} + +fn main() { + println!("Listening on 0.0.0.0:3333"); + Server::http("0.0.0.0:3333").unwrap().handle( + Switch::new( + NonMultipart, + EchoMultipart + )).unwrap(); +} diff --git a/multipart/examples/iron.rs b/multipart/examples/iron.rs new file mode 100644 index 000000000..effeef68e --- /dev/null +++ b/multipart/examples/iron.rs @@ -0,0 +1,66 @@ +extern crate multipart; +extern crate iron; + +extern crate env_logger; + +use std::io::{self, Write}; +use multipart::mock::StdoutTee; +use multipart::server::{Multipart, Entries, SaveResult}; +use iron::prelude::*; +use iron::status; + +fn main() { + env_logger::init(); + + Iron::new(process_request).http("localhost:80").expect("Could not bind localhost:80"); +} + +/// Processes a request and returns response or an occured error. +fn process_request(request: &mut Request) -> IronResult { + // Getting a multipart reader wrapper + match Multipart::from_request(request) { + Ok(mut multipart) => { + // Fetching all data and processing it. + // save().temp() reads the request fully, parsing all fields and saving all files + // in a new temporary directory under the OS temporary directory. + match multipart.save().temp() { + SaveResult::Full(entries) => process_entries(entries), + SaveResult::Partial(entries, reason) => { + process_entries(entries.keep_partial())?; + Ok(Response::with(( + status::BadRequest, + format!("error reading request: {}", reason.unwrap_err()) + ))) + } + SaveResult::Error(error) => Ok(Response::with(( + status::BadRequest, + format!("error reading request: {}", error) + ))), + } + } + Err(_) => { + Ok(Response::with((status::BadRequest, "The request is not multipart"))) + } + } +} + +/// Processes saved entries from multipart request. +/// Returns an OK response or an error. +fn process_entries(entries: Entries) -> IronResult { + let mut data = Vec::new(); + + { + let stdout = io::stdout(); + let tee = StdoutTee::new(&mut data, &stdout); + entries.write_debug(tee).map_err(|e| { + IronError::new( + e, + (status::InternalServerError, "Error printing request fields") + ) + })?; + } + + let _ = writeln!(data, "Entries processed"); + + Ok(Response::with((status::Ok, data))) +} diff --git a/multipart/examples/iron_intercept.rs b/multipart/examples/iron_intercept.rs new file mode 100644 index 000000000..fa328dd6e --- /dev/null +++ b/multipart/examples/iron_intercept.rs @@ -0,0 +1,25 @@ +extern crate iron; +extern crate multipart; + +use iron::prelude::*; + +use multipart::server::Entries; +use multipart::server::iron::Intercept; + +fn main() { + // We start with a basic request handler chain. + let mut chain = Chain::new(|req: &mut Request| + if let Some(entries) = req.extensions.get::() { + Ok(Response::with(format!("{:?}", entries))) + } else { + Ok(Response::with("Not a multipart request")) + } + ); + + // `Intercept` will read out the entries and place them as an extension in the request. + // It has various builder-style methods for changing how it will behave, but has sane settings + // by default. + chain.link_before(Intercept::default()); + + Iron::new(chain).http("localhost:80").unwrap(); +} \ No newline at end of file diff --git a/multipart/examples/nickel.rs b/multipart/examples/nickel.rs new file mode 100644 index 000000000..71ec8ff3c --- /dev/null +++ b/multipart/examples/nickel.rs @@ -0,0 +1,63 @@ +extern crate multipart; +extern crate nickel; + +use std::io::{self, Write}; +use nickel::{Action, HttpRouter, MiddlewareResult, Nickel, Request, Response}; +use nickel::status::StatusCode; + +use multipart::server::nickel::MultipartBody; +use multipart::server::{Entries, SaveResult}; +use multipart::mock::StdoutTee; + +fn handle_multipart<'mw>(req: &mut Request, mut res: Response<'mw>) -> MiddlewareResult<'mw> { + match (*req).multipart_body() { + Some(mut multipart) => { + match multipart.save().temp() { + SaveResult::Full(entries) => process_entries(res, entries), + + SaveResult::Partial(entries, e) => { + println!("Partial errors ... {:?}", e); + return process_entries(res, entries.keep_partial()); + }, + + SaveResult::Error(e) => { + println!("There are errors in multipart POSTing ... {:?}", e); + res.set(StatusCode::InternalServerError); + return res.send(format!("Server could not handle multipart POST! {:?}", e)); + }, + } + } + None => { + res.set(StatusCode::BadRequest); + return res.send("Request seems not was a multipart request") + } + } +} + +/// Processes saved entries from multipart request. +/// Returns an OK response or an error. +fn process_entries<'mw>(res: Response<'mw>, entries: Entries) -> MiddlewareResult<'mw> { + let stdout = io::stdout(); + let mut res = res.start()?; + if let Err(e) = entries.write_debug(StdoutTee::new(&mut res, &stdout)) { + writeln!(res, "Error while reading entries: {}", e).expect("writeln"); + } + + Ok(Action::Halt(res)) +} + +fn main() { + let mut srv = Nickel::new(); + + srv.post("/multipart_upload/", handle_multipart); + + // Start this example via: + // + // `cargo run --example nickel --features nickel` + // + // And - if you are in the root of this repository - do an example + // upload via: + // + // `curl -F file=@LICENSE 'http://localhost:6868/multipart_upload/'` + srv.listen("127.0.0.1:6868").expect("Failed to bind server"); +} diff --git a/multipart/examples/rocket.rs b/multipart/examples/rocket.rs new file mode 100644 index 000000000..cf9e6f714 --- /dev/null +++ b/multipart/examples/rocket.rs @@ -0,0 +1,85 @@ +// Example usage with Rocket (https://rocket.rs) +// +// Direct integration is not provided at this time as it appears the Rocket folks would prefer +// to handle multipart requests behind the scenes. +#![feature(proc_macro_hygiene, decl_macro)] +#![feature(plugin, custom_attribute)] + +extern crate multipart; +#[macro_use] +extern crate rocket; + +use multipart::mock::StdoutTee; +use multipart::server::Multipart; +use multipart::server::save::Entries; +use multipart::server::save::SaveResult::*; + +use rocket::Data; +use rocket::http::{ContentType, Status}; +use rocket::response::Stream; +use rocket::response::status::Custom; + +use std::io::{self, Cursor, Write}; + +#[post("/upload", data = "")] +// signature requires the request to have a `Content-Type` +fn multipart_upload(cont_type: &ContentType, data: Data) -> Result>>, Custom> { + // this and the next check can be implemented as a request guard but it seems like just + // more boilerplate than necessary + if !cont_type.is_form_data() { + return Err(Custom( + Status::BadRequest, + "Content-Type not multipart/form-data".into() + )); + } + + let (_, boundary) = cont_type.params().find(|&(k, _)| k == "boundary").ok_or_else( + || Custom( + Status::BadRequest, + "`Content-Type: multipart/form-data` boundary param not provided".into() + ) + )?; + + match process_upload(boundary, data) { + Ok(resp) => Ok(Stream::from(Cursor::new(resp))), + Err(err) => Err(Custom(Status::InternalServerError, err.to_string())) + } +} + +fn process_upload(boundary: &str, data: Data) -> io::Result> { + let mut out = Vec::new(); + + // saves all fields, any field longer than 10kB goes to a temporary directory + // Entries could implement FromData though that would give zero control over + // how the files are saved; Multipart would be a good impl candidate though + match Multipart::with_body(data.open(), boundary).save().temp() { + Full(entries) => process_entries(entries, &mut out)?, + Partial(partial, reason) => { + writeln!(out, "Request partially processed: {:?}", reason)?; + if let Some(field) = partial.partial { + writeln!(out, "Stopped on field: {:?}", field.source.headers)?; + } + + process_entries(partial.entries, &mut out)? + }, + Error(e) => return Err(e), + } + + Ok(out) +} + +// having a streaming output would be nice; there's one for returning a `Read` impl +// but not one that you can `write()` to +fn process_entries(entries: Entries, mut out: &mut Vec) -> io::Result<()> { + { + let stdout = io::stdout(); + let tee = StdoutTee::new(&mut out, &stdout); + entries.write_debug(tee)?; + } + + writeln!(out, "Entries processed") +} + +fn main() { + rocket::ignite().mount("/", routes![multipart_upload]).launch(); +} diff --git a/multipart/examples/tiny_http.rs b/multipart/examples/tiny_http.rs new file mode 100644 index 000000000..caef10914 --- /dev/null +++ b/multipart/examples/tiny_http.rs @@ -0,0 +1,78 @@ +extern crate tiny_http; +extern crate multipart; + +use std::io::{self, Cursor, Write}; +use multipart::server::{Multipart, Entries, SaveResult}; +use multipart::mock::StdoutTee; +use tiny_http::{Response, StatusCode, Request}; +fn main() { + // Starting a server on `localhost:80` + let server = tiny_http::Server::http("localhost:80").expect("Could not bind localhost:80"); + loop { + // This blocks until the next request is received + let mut request = server.recv().unwrap(); + + // Processes a request and returns response or an occured error + let result = process_request(&mut request); + let resp = match result { + Ok(resp) => resp, + Err(e) => { + println!("An error has occured during request proccessing: {:?}", e); + build_response(500, "The received data was not correctly proccessed on the server") + } + }; + + // Answers with a response to a client + request.respond(resp).unwrap(); + } +} + +type RespBody = Cursor>; + +/// Processes a request and returns response or an occured error. +fn process_request(request: &mut Request) -> io::Result> { + // Getting a multipart reader wrapper + match Multipart::from_request(request) { + Ok(mut multipart) => { + // Fetching all data and processing it. + // save().temp() reads the request fully, parsing all fields and saving all files + // in a new temporary directory under the OS temporary directory. + match multipart.save().temp() { + SaveResult::Full(entries) => process_entries(entries), + SaveResult::Partial(entries, reason) => { + process_entries(entries.keep_partial())?; + // We don't set limits + Err(reason.unwrap_err()) + } + SaveResult::Error(error) => Err(error), + } + } + Err(_) => Ok(build_response(400, "The request is not multipart")), + } +} + +/// Processes saved entries from multipart request. +/// Returns an OK response or an error. +fn process_entries(entries: Entries) -> io::Result> { + let mut data = Vec::new(); + + { + let stdout = io::stdout(); + let tee = StdoutTee::new(&mut data, &stdout); + entries.write_debug(tee)?; + } + + writeln!(data, "Entries processed")?; + + Ok(build_response(200, data)) +} + +fn build_response>>(status_code: u16, data: D) -> Response { + let data = data.into(); + let data_len = data.len(); + Response::new(StatusCode(status_code), + vec![], + Cursor::new(data), + Some(data_len), + None) +} diff --git a/multipart/fuzz/.gitignore b/multipart/fuzz/.gitignore new file mode 100644 index 000000000..dfeb7db19 --- /dev/null +++ b/multipart/fuzz/.gitignore @@ -0,0 +1,5 @@ + +target +libfuzzer +corpus +artifacts diff --git a/multipart/fuzz/Cargo.lock b/multipart/fuzz/Cargo.lock new file mode 100644 index 000000000..748229dd6 --- /dev/null +++ b/multipart/fuzz/Cargo.lock @@ -0,0 +1,206 @@ +[root] +name = "multipart-fuzz" +version = "0.0.1" +dependencies = [ + "libfuzzer-sys 0.1.0 (git+https://github.com/rust-fuzz/libfuzzer-sys.git)", + "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "multipart 0.11.0", +] + +[[package]] +name = "buf_redux" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "safemem 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gcc" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "httparse" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libfuzzer-sys" +version = "0.1.0" +source = "git+https://github.com/rust-fuzz/libfuzzer-sys.git#36a3928eef5c3c38eb0f251962395bb510c39d46" +dependencies = [ + "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "log" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mime" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mime_guess" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "mime 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "multipart" +version = "0.11.0" +dependencies = [ + "buf_redux 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "mime_guess 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "twoway 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf" +version = "0.7.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_codegen" +version = "0.7.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_generator" +version = "0.7.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_shared" +version = "0.7.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc_version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "safemem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "safemem" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "semver" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "siphasher" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "tempdir" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "twoway" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicase" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum buf_redux 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e1497634c131ba13483b6e8123f69e219253b018bb32949eefd55c6b5051585d" +"checksum gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)" = "40899336fb50db0c78710f53e87afc54d8c7266fb76262fecc78ca1a7f09deae" +"checksum httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e7a63e511f9edffbab707141fbb8707d1a3098615fb2adbd5769cdfcc9b17d" +"checksum libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "88ee81885f9f04bff991e306fea7c1c60a5f0f9e409e99f6b40e3311a3363135" +"checksum libfuzzer-sys 0.1.0 (git+https://github.com/rust-fuzz/libfuzzer-sys.git)" = "" +"checksum log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5141eca02775a762cc6cd564d8d2c50f67c0ea3a372cbf1c51592b3e029e10ad" +"checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" +"checksum mime 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5514f038123342d01ee5f95129e4ef1e0470c93bc29edf058a46f9ee3ba6737e" +"checksum mime_guess 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76da6df85047af8c0edfa53f48eb1073012ce1cc95c8fedc0a374f659a89dd65" +"checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc" +"checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f" +"checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03" +"checksum phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "07e24b0ca9643bdecd0632f2b3da6b1b89bbb0030e0b992afc1113b23a7bc2f2" +"checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d" +"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" +"checksum safemem 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "725b3bf47ae40b4abcd27b5f0a9540369426a29f7b905649b3e1468e13e22009" +"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" +"checksum semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" +"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537" +"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" +"checksum twoway 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e267e178055eb3b081224bbef62d4f508ae3c9f000b6ae6ccdb04a0d9c34b77f" +"checksum unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a5906ca2b98c799f4b1ab4557b76367ebd6ae5ef14930ec841c74aed5f3764" diff --git a/multipart/fuzz/Cargo.toml b/multipart/fuzz/Cargo.toml new file mode 100644 index 000000000..6b3efc7ed --- /dev/null +++ b/multipart/fuzz/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "multipart-fuzz" +version = "0.0.1" +authors = ["Automatically generated"] +publish = false + +[package.metadata] +cargo-fuzz = true + +[dependencies] +log = "*" + +[dependencies.multipart] +path = ".." +default-features = false +features = ["mock", "client", "server"] + +[dependencies.libfuzzer-sys] +git = "https://github.com/rust-fuzz/libfuzzer-sys.git" + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[[bin]] +name = "server_basic" +path = "fuzzers/server_basic.rs" diff --git a/multipart/fuzz/fuzzer_dict b/multipart/fuzz/fuzzer_dict new file mode 100644 index 000000000..f44d26324 --- /dev/null +++ b/multipart/fuzz/fuzzer_dict @@ -0,0 +1,7 @@ +"Content-Disposition: form-data; name=" +# CR LF +"\x0D\x0A" +"Content-Type:" +"filename=" +"--12--34--56" +"--12--34--56--" \ No newline at end of file diff --git a/multipart/fuzz/fuzzers/logger.rs b/multipart/fuzz/fuzzers/logger.rs new file mode 100644 index 000000000..ee8b5d914 --- /dev/null +++ b/multipart/fuzz/fuzzers/logger.rs @@ -0,0 +1,28 @@ +extern crate log; + +use self::log::{LogLevelFilter, Log, LogMetadata, LogRecord}; + +const MAX_LOG_LEVEL: LogLevelFilter = LogLevelFilter::Off; + +struct Logger; + +impl Log for Logger { + fn enabled(&self, metadata: &LogMetadata) -> bool { + metadata.level() <= MAX_LOG_LEVEL + } + + fn log(&self, record: &LogRecord) { + println!("{}: {}", record.level(), record.args()); + } +} + +static LOGGER: Logger = Logger; + +pub fn init() { + let _ = unsafe { + log::set_logger_raw(|max_lvl| { + max_lvl.set(MAX_LOG_LEVEL); + &LOGGER + }) + }; +} diff --git a/multipart/fuzz/fuzzers/server_basic.rs b/multipart/fuzz/fuzzers/server_basic.rs new file mode 100644 index 000000000..09312acce --- /dev/null +++ b/multipart/fuzz/fuzzers/server_basic.rs @@ -0,0 +1,57 @@ +#![no_main] +extern crate libfuzzer_sys; +extern crate multipart; + +#[macro_use] +extern crate log; + +use multipart::server::{Multipart, MultipartData}; +use multipart::mock::ServerRequest; + +mod logger; + +use std::io::BufRead; + +const BOUNDARY: &'static str = "--12--34--56"; + +#[export_name="rust_fuzzer_test_input"] +pub extern fn go(data: &[u8]) { + logger::init(); + + info!("Fuzzing started! Data len: {}", data.len()); + + do_fuzz(data); + + info!("Finished fuzzing iteration"); +} + +fn do_fuzz(data: &[u8]) { + + if data.len() < BOUNDARY.len() { return; } + + let req = ServerRequest::new(data, BOUNDARY); + + info!("Request constructed!"); + + let mut multipart = if let Ok(multi) = Multipart::from_request(req) { + multi + } else { + panic!("This shouldn't have failed") + }; + + // A lot of requests will be malformed + while let Ok(Some(entry)) = multipart.read_entry() { + info!("read_entry() loop!"); + match entry.data { + MultipartData::Text(_) => (), + MultipartData::File(mut file) => loop { + let consume = file.fill_buf().expect("This shouldn't fail").len(); + + info!("Consume amt: {}", consume); + + if consume == 0 { break; } + file.consume(consume); + } + } + } +} diff --git a/multipart/fuzz_server.sh b/multipart/fuzz_server.sh new file mode 100755 index 000000000..b4878f5a7 --- /dev/null +++ b/multipart/fuzz_server.sh @@ -0,0 +1,3 @@ +#! /bin/sh +# pwd +cargo fuzz run server_basic -- -dict=fuzz/fuzzer_dict -only_ascii=1 -timeout=60 ${FUZZ_LEN:+ -max_len=$FUZZ_LEN} $@ diff --git a/multipart/lorem_ipsum.txt b/multipart/lorem_ipsum.txt new file mode 100644 index 000000000..1704658d6 --- /dev/null +++ b/multipart/lorem_ipsum.txt @@ -0,0 +1,7 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dignissim, lectus a placerat vestibulum, mi massa dapibus ante, placerat efficitur dui est non lorem. Donec metus lacus, ultricies id suscipit sed, varius et augue. Vivamus erat lectus, interdum in quam non, euismod venenatis eros. Sed vitae magna orci. Duis finibus velit sem, eu luctus urna fringilla vitae. Suspendisse volutpat eros a tincidunt porttitor. Sed ut massa pretium, tempor neque nec, lobortis quam. Etiam vestibulum mauris eu sem consectetur, condimentum fermentum libero vulputate. Vestibulum porttitor leo et blandit condimentum. Pellentesque auctor odio eros, nec placerat lorem ultrices vitae. Suspendisse pretium tellus a ipsum sagittis consequat. Nullam pulvinar ligula ut fermentum laoreet. Maecenas rhoncus ut neque vitae tincidunt. Maecenas tincidunt at orci sed scelerisque. Sed porttitor tincidunt purus, ut efficitur leo lobortis vitae. Aenean et orci dolor. + +Vestibulum at laoreet felis. Cras et justo libero. Morbi pulvinar tincidunt odio, id finibus magna tincidunt non. Nulla facilisi. In at finibus lacus. Phasellus non volutpat dui. Vivamus porta fermentum dignissim. Nulla facilisi. Mauris laoreet semper ex lacinia interdum. Donec et dui non orci cursus scelerisque vulputate non neque. Fusce efficitur maximus turpis tempor interdum. Proin sit amet nunc pretium, varius dui sed, pretium nulla. Integer commodo orci ut felis bibendum feugiat. + +In interdum pulvinar tellus, quis porta eros consectetur in. Ut pharetra sem quam, id congue urna tempus eu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed egestas mattis ex quis malesuada. Quisque justo enim, posuere non arcu id, dictum pulvinar ante. Curabitur at rhoncus turpis. Donec in odio ut dolor elementum ultricies. Integer massa lacus, ullamcorper non nisi sed, scelerisque commodo lacus. Aliquam erat volutpat. Etiam eleifend libero tincidunt lobortis dignissim. Aliquam in odio sed libero sollicitudin pharetra. + +In quis consectetur ex, nec tempus mi. Donec commodo urna augue, non hendrerit mi lobortis et. Duis a augue laoreet, pulvinar purus luctus, rhoncus est. Quisque sodales sollicitudin augue ac bibendum. Sed a metus risus. Nulla non nulla nisl. Aenean erat velit, tempor id pellentesque eu, volutpat vitae dolor. Praesent commodo, dui in luctus aliquet, est tortor vehicula nibh, sed sollicitudin dui elit eu purus. Integer lacinia rutrum convallis. Nullam varius fringilla dui, elementum finibus magna tincidunt id. Praesent et cursus purus, vitae blandit purus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam vel cursus neque. \ No newline at end of file diff --git a/multipart/src/bin/form_test.rs b/multipart/src/bin/form_test.rs new file mode 100644 index 000000000..8cf1b9499 --- /dev/null +++ b/multipart/src/bin/form_test.rs @@ -0,0 +1,33 @@ +extern crate hyper; +extern crate multipart; + +use multipart::server::Multipart; + +use hyper::header::ContentType; +use hyper::server::*; + +use std::fs::File; +use std::io; + +fn main() { + let listening = Server::http("127.0.0.1:0").expect("failed to bind socket") + .handle(read_multipart).expect("failed to handle request"); + + println!("bound socket to: {}", listening.socket); +} + +fn read_multipart(req: Request, mut resp: Response) { + if let Ok(mut multipart) = Multipart::from_request(req) { + if let Err(e) = multipart.foreach_entry(|_| {}) { + println!("error handling field: {}", e); + } + } + + let mut file = File::open("src/bin/test_form.html") + .expect("failed to open src/bind/test_form.html"); + + resp.headers_mut().set(ContentType("text/html".parse().unwrap())); + + let mut resp = resp.start().expect("failed to open response"); + io::copy(&mut file, &mut resp).expect("failed to write response"); +} diff --git a/multipart/src/bin/read_file.rs b/multipart/src/bin/read_file.rs new file mode 100644 index 000000000..0a55a0b89 --- /dev/null +++ b/multipart/src/bin/read_file.rs @@ -0,0 +1,75 @@ +#[macro_use] extern crate log; +extern crate multipart; +extern crate rand; + +use multipart::server::Multipart; + +use rand::{Rng, ThreadRng}; + +use std::fs::File; +use std::env; +use std::io::{self, Read}; + +const LOG_LEVEL: log::LevelFilter = log::LevelFilter::Debug; + +struct SimpleLogger; + +impl log::Log for SimpleLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + LOG_LEVEL.to_level() + .map_or(false, |level| metadata.level() <= level) + } + + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + println!("{} - {}", record.level(), record.args()); + } + } + + fn flush(&self) {} +} + +static LOGGER: SimpleLogger = SimpleLogger; + +fn main() { + log::set_logger(&LOGGER).expect("Could not initialize logger"); + + let mut args = env::args().skip(1); + + let boundary = args.next().expect("Boundary must be provided as the first argument"); + + let file = args.next().expect("Filename must be provided as the second argument"); + + let file = File::open(file).expect("Could not open file"); + + let reader = RandomReader { + inner: file, + rng: rand::thread_rng() + }; + + let mut multipart = Multipart::with_body(reader, boundary); + + while let Some(field) = multipart.read_entry().unwrap() { + println!("Read field: {:?}", field.headers.name); + } + + println!("All entries read!"); +} + +struct RandomReader { + inner: R, + rng: ThreadRng, +} + +impl Read for RandomReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if buf.len() == 0 { + debug!("RandomReader::read() passed a zero-sized buffer."); + return Ok(0); + } + + let len = self.rng.gen_range(1, buf.len() + 1); + + self.inner.read(&mut buf[..len]) + } +} diff --git a/multipart/src/bin/test_form.html b/multipart/src/bin/test_form.html new file mode 100644 index 000000000..6d10065b7 --- /dev/null +++ b/multipart/src/bin/test_form.html @@ -0,0 +1,14 @@ + + + + + Multipart-Async Form Test + + +
+ + +
+ + diff --git a/multipart/src/client/hyper.rs b/multipart/src/client/hyper.rs new file mode 100644 index 000000000..675cba6d1 --- /dev/null +++ b/multipart/src/client/hyper.rs @@ -0,0 +1,83 @@ +// Copyright 2016 `multipart` Crate Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +//! Client-side integration with [Hyper](https://github.com/hyperium/hyper). +//! Enabled with the `hyper` feature (on by default). +//! +//! Contains `impl HttpRequest for Request` and `impl HttpStream for Request`. +//! +//! Also see: [`lazy::Multipart::client_request()`](../lazy/struct.Multipart.html#method.client_request) +//! and [`lazy::Multipart::client_request_mut()`](../lazy/struct.Multipart.html#method.client_request_mut) +//! (adaptors for `hyper::client::RequestBuilder`). +use hyper::client::request::Request; +use hyper::client::response::Response; +use hyper::header::{ContentType, ContentLength}; +use hyper::method::Method; +use hyper::net::{Fresh, Streaming}; + +use hyper::Error as HyperError; + +use hyper::mime::{Mime, TopLevel, SubLevel, Attr, Value}; + +use super::{HttpRequest, HttpStream}; + +/// #### Feature: `hyper` +impl HttpRequest for Request { + type Stream = Request; + type Error = HyperError; + + /// # Panics + /// If `self.method() != Method::Post`. + fn apply_headers(&mut self, boundary: &str, content_len: Option) -> bool { + if self.method() != Method::Post { + error!( + "Expected Hyper request method to be `Post`, was actually `{:?}`", + self.method() + ); + + return false; + } + + let headers = self.headers_mut(); + + headers.set(ContentType(multipart_mime(boundary))); + + if let Some(size) = content_len { + headers.set(ContentLength(size)); + } + + debug!("Hyper headers: {}", headers); + + true + } + + fn open_stream(self) -> Result { + self.start() + } +} + +/// #### Feature: `hyper` +impl HttpStream for Request { + type Request = Request; + type Response = Response; + type Error = HyperError; + + fn finish(self) -> Result { + self.send() + } +} + +/// Create a `Content-Type: multipart/form-data;boundary={bound}` +pub fn content_type(bound: &str) -> ContentType { + ContentType(multipart_mime(bound)) +} + +fn multipart_mime(bound: &str) -> Mime { + Mime( + TopLevel::Multipart, SubLevel::Ext("form-data".into()), + vec![(Attr::Ext("boundary".into()), Value::Ext(bound.into()))] + ) +} diff --git a/multipart/src/client/lazy.rs b/multipart/src/client/lazy.rs new file mode 100644 index 000000000..4df40cfe5 --- /dev/null +++ b/multipart/src/client/lazy.rs @@ -0,0 +1,546 @@ +//! Multipart requests which write out their data in one fell swoop. +use mime::Mime; + +use std::borrow::Cow; +use std::error::Error; +use std::fs::File; +use std::path::{Path, PathBuf}; + +use std::io::prelude::*; +use std::io::Cursor; +use std::{fmt, io}; + +use super::{HttpRequest, HttpStream}; + +macro_rules! try_lazy ( + ($field:expr, $try:expr) => ( + match $try { + Ok(ok) => ok, + Err(e) => return Err(LazyError::with_field($field.into(), e)), + } + ); + ($try:expr) => ( + match $try { + Ok(ok) => ok, + Err(e) => return Err(LazyError::without_field(e)), + } + ) +); + +/// A `LazyError` wrapping `std::io::Error`. +pub type LazyIoError<'a> = LazyError<'a, io::Error>; + +/// `Result` type for `LazyIoError`. +pub type LazyIoResult<'a, T> = Result>; + +/// An error for lazily written multipart requests, including the original error as well +/// as the field which caused the error, if applicable. +pub struct LazyError<'a, E> { + /// The field that caused the error. + /// If `None`, there was a problem opening the stream to write or finalizing the stream. + pub field_name: Option>, + /// The inner error. + pub error: E, + /// Private field for back-compat. + _priv: (), +} + +impl<'a, E> LazyError<'a, E> { + fn without_field>(error: E_) -> Self { + LazyError { + field_name: None, + error: error.into(), + _priv: (), + } + } + + fn with_field>(field_name: Cow<'a, str>, error: E_) -> Self { + LazyError { + field_name: Some(field_name), + error: error.into(), + _priv: (), + } + } + + fn transform_err>(self) -> LazyError<'a, E_> { + LazyError { + field_name: self.field_name, + error: self.error.into(), + _priv: (), + } + } +} + +/// Take `self.error`, discarding `self.field_name`. +impl<'a> Into for LazyError<'a, io::Error> { + fn into(self) -> io::Error { + self.error + } +} + +impl<'a, E: Error> Error for LazyError<'a, E> { + fn description(&self) -> &str { + self.error.description() + } + + fn cause(&self) -> Option<&dyn Error> { + Some(&self.error) + } +} + +impl<'a, E: fmt::Debug> fmt::Debug for LazyError<'a, E> { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + if let Some(ref field_name) = self.field_name { + fmt.write_fmt(format_args!( + "LazyError (on field {:?}): {:?}", + field_name, self.error + )) + } else { + fmt.write_fmt(format_args!("LazyError (misc): {:?}", self.error)) + } + } +} + +impl<'a, E: fmt::Display> fmt::Display for LazyError<'a, E> { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + if let Some(ref field_name) = self.field_name { + fmt.write_fmt(format_args!( + "Error writing field {:?}: {}", + field_name, self.error + )) + } else { + fmt.write_fmt(format_args!( + "Error opening or flushing stream: {}", + self.error + )) + } + } +} + +/// A multipart request which writes all fields at once upon being provided an output stream. +/// +/// Sacrifices static dispatch for support for dynamic construction. Reusable. +/// +/// #### Lifetimes +/// * `'n`: Lifetime for field **n**ames; will only escape this struct in `LazyIoError<'n>`. +/// * `'d`: Lifetime for **d**ata: will only escape this struct in `PreparedFields<'d>`. +#[derive(Debug, Default)] +pub struct Multipart<'n, 'd> { + fields: Vec>, +} + +impl<'n, 'd> Multipart<'n, 'd> { + /// Initialize a new lazy dynamic request. + pub fn new() -> Self { + Default::default() + } + + /// Add a text field to this request. + pub fn add_text(&mut self, name: N, text: T) -> &mut Self + where + N: Into>, + T: Into>, + { + self.fields.push(Field { + name: name.into(), + data: Data::Text(text.into()), + }); + + self + } + + /// Add a file field to this request. + /// + /// ### Note + /// Does not check if `path` exists. + pub fn add_file(&mut self, name: N, path: P) -> &mut Self + where + N: Into>, + P: IntoCowPath<'d>, + { + self.fields.push(Field { + name: name.into(), + data: Data::File(path.into_cow_path()), + }); + + self + } + + /// Add a generic stream field to this request, + pub fn add_stream( + &mut self, + name: N, + stream: R, + filename: Option, + mime: Option, + ) -> &mut Self + where + N: Into>, + R: Read + 'd, + F: Into>, + { + self.fields.push(Field { + name: name.into(), + data: Data::Stream(Stream { + content_type: mime.unwrap_or(mime::APPLICATION_OCTET_STREAM), + filename: filename.map(|f| f.into()), + stream: Box::new(stream), + }), + }); + + self + } + + /// Convert `req` to `HttpStream`, write out the fields in this request, and finish the + /// request, returning the response if successful, or the first error encountered. + /// + /// If any files were added by path they will now be opened for reading. + pub fn send( + &mut self, + mut req: R, + ) -> Result<::Response, LazyError<'n, ::Error>> + { + let mut prepared = self.prepare().map_err(LazyError::transform_err)?; + + req.apply_headers(prepared.boundary(), prepared.content_len()); + + let mut stream = try_lazy!(req.open_stream()); + + try_lazy!(io::copy(&mut prepared, &mut stream)); + + stream.finish().map_err(LazyError::without_field) + } + + /// Export the multipart data contained in this lazy request as an adaptor which implements `Read`. + /// + /// During this step, if any files were added by path then they will be opened for reading + /// and their length measured. + pub fn prepare(&mut self) -> LazyIoResult<'n, PreparedFields<'d>> { + PreparedFields::from_fields(&mut self.fields) + } +} + +#[derive(Debug)] +struct Field<'n, 'd> { + name: Cow<'n, str>, + data: Data<'n, 'd>, +} + +enum Data<'n, 'd> { + Text(Cow<'d, str>), + File(Cow<'d, Path>), + Stream(Stream<'n, 'd>), +} + +impl<'n, 'd> fmt::Debug for Data<'n, 'd> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Data::Text(ref text) => write!(f, "Data::Text({:?})", text), + Data::File(ref path) => write!(f, "Data::File({:?})", path), + Data::Stream(_) => f.write_str("Data::Stream(Box)"), + } + } +} + +struct Stream<'n, 'd> { + filename: Option>, + content_type: Mime, + stream: Box, +} + +/// The result of [`Multipart::prepare()`](struct.Multipart.html#method.prepare). +/// +/// Implements `Read`, contains the entire request body. +/// +/// Individual files/streams are dropped as they are read to completion. +/// +/// ### Note +/// The fields in the request may have been reordered to simplify the preparation step. +/// No compliant server implementation will be relying on the specific ordering of fields anyways. +pub struct PreparedFields<'d> { + text_data: Cursor>, + streams: Vec>, + end_boundary: Cursor, + content_len: Option, +} + +impl<'d> PreparedFields<'d> { + fn from_fields<'n>(fields: &mut Vec>) -> Result> { + debug!("Field count: {}", fields.len()); + + // One of the two RFCs specifies that any bytes before the first boundary are to be + // ignored anyway + let mut boundary = format!("\r\n--{}", super::gen_boundary()); + + let mut text_data = Vec::new(); + let mut streams = Vec::new(); + let mut content_len = 0u64; + let mut use_len = true; + + for field in fields.drain(..) { + match field.data { + Data::Text(text) => write!( + text_data, + "{}\r\nContent-Disposition: form-data; \ + name=\"{}\"\r\n\r\n{}", + boundary, field.name, text + ) + .unwrap(), + Data::File(file) => { + let (stream, len) = PreparedField::from_path(field.name, &file, &boundary)?; + content_len += len; + streams.push(stream); + } + Data::Stream(stream) => { + use_len = false; + + streams.push(PreparedField::from_stream( + &field.name, + &boundary, + &stream.content_type, + stream.filename.as_ref().map(|f| &**f), + stream.stream, + )); + } + } + } + + // So we don't write a spurious end boundary + if text_data.is_empty() && streams.is_empty() { + boundary = String::new(); + } else { + boundary.push_str("--"); + } + + content_len += boundary.len() as u64; + + Ok(PreparedFields { + text_data: Cursor::new(text_data), + streams, + end_boundary: Cursor::new(boundary), + content_len: if use_len { Some(content_len) } else { None }, + }) + } + + /// Get the content-length value for this set of fields, if applicable (all fields are sized, + /// i.e. not generic streams). + pub fn content_len(&self) -> Option { + self.content_len + } + + /// Get the boundary that was used to serialize the request. + pub fn boundary(&self) -> &str { + let boundary = self.end_boundary.get_ref(); + + // Get just the bare boundary string + &boundary[4..boundary.len() - 2] + } +} + +impl<'d> Read for PreparedFields<'d> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if buf.is_empty() { + debug!("PreparedFields::read() was passed a zero-sized buffer."); + return Ok(0); + } + + let mut total_read = 0; + + while total_read < buf.len() && !cursor_at_end(&self.end_boundary) { + let buf = &mut buf[total_read..]; + + total_read += if !cursor_at_end(&self.text_data) { + self.text_data.read(buf)? + } else if let Some(mut field) = self.streams.pop() { + match field.read(buf) { + Ok(0) => continue, + res => { + self.streams.push(field); + res + } + }? + } else { + self.end_boundary.read(buf)? + }; + } + + Ok(total_read) + } +} + +struct PreparedField<'d> { + header: Cursor>, + stream: Box, +} + +impl<'d> PreparedField<'d> { + fn from_path<'n>( + name: Cow<'n, str>, + path: &Path, + boundary: &str, + ) -> Result<(Self, u64), LazyIoError<'n>> { + let (content_type, filename) = super::mime_filename(&path); + + let file = try_lazy!(name, File::open(path)); + let content_len = try_lazy!(name, file.metadata()).len(); + + let stream = Self::from_stream(&name, boundary, &content_type, filename, Box::new(file)); + + let content_len = content_len + (stream.header.get_ref().len() as u64); + + Ok((stream, content_len)) + } + + fn from_stream( + name: &str, + boundary: &str, + content_type: &Mime, + filename: Option<&str>, + stream: Box, + ) -> Self { + let mut header = Vec::new(); + + write!( + header, + "{}\r\nContent-Disposition: form-data; name=\"{}\"", + boundary, name + ) + .unwrap(); + + if let Some(filename) = filename { + write!(header, "; filename=\"{}\"", filename).unwrap(); + } + + write!(header, "\r\nContent-Type: {}\r\n\r\n", content_type).unwrap(); + + PreparedField { + header: Cursor::new(header), + stream, + } + } +} + +impl<'d> Read for PreparedField<'d> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + debug!("PreparedField::read()"); + + if !cursor_at_end(&self.header) { + self.header.read(buf) + } else { + self.stream.read(buf) + } + } +} + +impl<'d> fmt::Debug for PreparedField<'d> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("PreparedField") + .field("header", &self.header) + .field("stream", &"Box") + .finish() + } +} + +/// Conversion trait necessary for `Multipart::add_file()` to accept borrowed or owned strings +/// and borrowed or owned paths +pub trait IntoCowPath<'a> { + /// Self-explanatory, hopefully + fn into_cow_path(self) -> Cow<'a, Path>; +} + +impl<'a> IntoCowPath<'a> for Cow<'a, Path> { + fn into_cow_path(self) -> Cow<'a, Path> { + self + } +} + +impl IntoCowPath<'static> for PathBuf { + fn into_cow_path(self) -> Cow<'static, Path> { + self.into() + } +} + +impl<'a> IntoCowPath<'a> for &'a Path { + fn into_cow_path(self) -> Cow<'a, Path> { + self.into() + } +} + +impl IntoCowPath<'static> for String { + fn into_cow_path(self) -> Cow<'static, Path> { + PathBuf::from(self).into() + } +} + +impl<'a> IntoCowPath<'a> for &'a str { + fn into_cow_path(self) -> Cow<'a, Path> { + Path::new(self).into() + } +} + +fn cursor_at_end>(cursor: &Cursor) -> bool { + cursor.position() == (cursor.get_ref().as_ref().len() as u64) +} + +#[cfg(feature = "hyper")] +mod hyper { + use hyper::client::{Body, Client, IntoUrl, RequestBuilder, Response}; + use hyper::Result as HyperResult; + + impl<'n, 'd> super::Multipart<'n, 'd> { + /// #### Feature: `hyper` + /// Complete a POST request with the given `hyper::client::Client` and URL. + /// + /// Supplies the fields in the body, optionally setting the content-length header if + /// applicable (all added fields were text or files, i.e. no streams). + pub fn client_request( + &mut self, + client: &Client, + url: U, + ) -> HyperResult { + self.client_request_mut(client, url, |r| r) + } + + /// #### Feature: `hyper` + /// Complete a POST request with the given `hyper::client::Client` and URL; + /// allows mutating the `hyper::client::RequestBuilder` via the passed closure. + /// + /// Note that the body, and the `ContentType` and `ContentLength` headers will be + /// overwritten, either by this method or by Hyper. + pub fn client_request_mut RequestBuilder>( + &mut self, + client: &Client, + url: U, + mut_fn: F, + ) -> HyperResult { + let mut fields = match self.prepare() { + Ok(fields) => fields, + Err(err) => { + error!("Error preparing request: {}", err); + return Err(err.error.into()); + } + }; + + mut_fn(client.post(url)) + .header(::client::hyper::content_type(fields.boundary())) + .body(fields.to_body()) + .send() + } + } + + impl<'d> super::PreparedFields<'d> { + /// #### Feature: `hyper` + /// Convert `self` to `hyper::client::Body`. + #[cfg_attr(feature = "clippy", warn(wrong_self_convention))] + pub fn to_body<'b>(&'b mut self) -> Body<'b> + where + 'd: 'b, + { + if let Some(content_len) = self.content_len { + Body::SizedBody(self, content_len) + } else { + Body::ChunkedBody(self) + } + } + } +} diff --git a/multipart/src/client/mod.rs b/multipart/src/client/mod.rs new file mode 100644 index 000000000..3158d2a7d --- /dev/null +++ b/multipart/src/client/mod.rs @@ -0,0 +1,315 @@ +// Copyright 2016 `multipart` Crate Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +//! The client-side abstraction for multipart requests. Enabled with the `client` feature. +//! +//! Use this when sending POST requests with files to a server. +use mime::Mime; + +use std::borrow::Cow; +use std::fs::File; +use std::io; +use std::io::prelude::*; + +use std::path::Path; + +#[cfg(feature = "hyper")] +pub mod hyper; + +pub mod lazy; + +mod sized; + +pub use self::sized::SizedRequest; + +const BOUNDARY_LEN: usize = 16; + +macro_rules! map_self { + ($selff:expr, $try:expr) => { + match $try { + Ok(_) => Ok($selff), + Err(err) => Err(err.into()), + } + }; +} + +/// The entry point of the client-side multipart API. +/// +/// Though they perform I/O, the `.write_*()` methods do not return `io::Result<_>` in order to +/// facilitate method chaining. Upon the first error, all subsequent API calls will be no-ops until +/// `.send()` is called, at which point the error will be reported. +pub struct Multipart { + writer: MultipartWriter<'static, S>, +} + +impl Multipart<()> { + /// Create a new `Multipart` to wrap a request. + /// + /// ## Returns Error + /// If `req.open_stream()` returns an error. + pub fn from_request(req: R) -> Result, R::Error> { + let (boundary, stream) = open_stream(req, None)?; + + Ok(Multipart { + writer: MultipartWriter::new(stream, boundary), + }) + } +} + +impl Multipart { + /// Write a text field to this multipart request. + /// `name` and `val` can be either owned `String` or `&str`. + /// + /// ## Errors + /// If something went wrong with the HTTP stream. + pub fn write_text, V: AsRef>( + &mut self, + name: N, + val: V, + ) -> Result<&mut Self, S::Error> { + map_self!(self, self.writer.write_text(name.as_ref(), val.as_ref())) + } + + /// Open a file pointed to by `path` and write its contents to the multipart request, + /// supplying its filename and guessing its `Content-Type` from its extension. + /// + /// If you want to set these values manually, or use another type that implements `Read`, + /// use `.write_stream()`. + /// + /// `name` can be either `String` or `&str`, and `path` can be `PathBuf` or `&Path`. + /// + /// ## Errors + /// If there was a problem opening the file (was a directory or didn't exist), + /// or if something went wrong with the HTTP stream. + pub fn write_file, P: AsRef>( + &mut self, + name: N, + path: P, + ) -> Result<&mut Self, S::Error> { + let name = name.as_ref(); + let path = path.as_ref(); + + map_self!(self, self.writer.write_file(name, path)) + } + + /// Write a byte stream to the multipart request as a file field, supplying `filename` if given, + /// and `content_type` if given or `"application/octet-stream"` if not. + /// + /// `name` can be either `String` or `&str`, and `read` can take the `Read` by-value or + /// with an `&mut` borrow. + /// + /// ## Warning + /// The given `Read` **must** be able to read to EOF (end of file/no more data), meaning + /// `Read::read()` returns `Ok(0)`. If it never returns EOF it will be read to infinity + /// and the request will never be completed. + /// + /// When using `SizedRequest` this also can cause out-of-control memory usage as the + /// multipart data has to be written to an in-memory buffer so its size can be calculated. + /// + /// Use `Read::take()` if you wish to send data from a `Read` + /// that will never return EOF otherwise. + /// + /// ## Errors + /// If the reader returned an error, or if something went wrong with the HTTP stream. + // RFC: How to format this declaration? + pub fn write_stream, St: Read>( + &mut self, + name: N, + stream: &mut St, + filename: Option<&str>, + content_type: Option, + ) -> Result<&mut Self, S::Error> { + let name = name.as_ref(); + + map_self!( + self, + self.writer + .write_stream(stream, name, filename, content_type) + ) + } + + /// Finalize the request and return the response from the server, or the last error if set. + pub fn send(self) -> Result { + self.writer + .finish() + .map_err(io::Error::into) + .and_then(|body| body.finish()) + } +} + +impl Multipart> +where + ::Error: From, +{ + /// Create a new `Multipart` using the `SizedRequest` wrapper around `req`. + pub fn from_request_sized(req: R) -> Result { + Multipart::from_request(SizedRequest::from_request(req)) + } +} + +/// A trait describing an HTTP request that can be used to send multipart data. +pub trait HttpRequest { + /// The HTTP stream type that can be opend by this request, to which the multipart data will be + /// written. + type Stream: HttpStream; + /// The error type for this request. + /// Must be compatible with `io::Error` as well as `Self::HttpStream::Error` + type Error: From + Into<::Error>; + + /// Set the `Content-Type` header to `multipart/form-data` and supply the `boundary` value. + /// If `content_len` is given, set the `Content-Length` header to its value. + /// + /// Return `true` if any and all sanity checks passed and the stream is ready to be opened, + /// or `false` otherwise. + fn apply_headers(&mut self, boundary: &str, content_len: Option) -> bool; + + /// Open the request stream and return it or any error otherwise. + fn open_stream(self) -> Result; +} + +/// A trait describing an open HTTP stream that can be written to. +pub trait HttpStream: Write { + /// The request type that opened this stream. + type Request: HttpRequest; + /// The response type that will be returned after the request is completed. + type Response; + /// The error type for this stream. + /// Must be compatible with `io::Error` as well as `Self::Request::Error`. + type Error: From + From<::Error>; + + /// Finalize and close the stream and return the response object, or any error otherwise. + fn finish(self) -> Result; +} + +impl HttpRequest for () { + type Stream = io::Sink; + type Error = io::Error; + + fn apply_headers(&mut self, _: &str, _: Option) -> bool { + true + } + fn open_stream(self) -> Result { + Ok(io::sink()) + } +} + +impl HttpStream for io::Sink { + type Request = (); + type Response = (); + type Error = io::Error; + + fn finish(self) -> Result { + Ok(()) + } +} + +fn gen_boundary() -> String { + ::random_alphanumeric(BOUNDARY_LEN) +} + +fn open_stream( + mut req: R, + content_len: Option, +) -> Result<(String, R::Stream), R::Error> { + let boundary = gen_boundary(); + req.apply_headers(&boundary, content_len); + req.open_stream().map(|stream| (boundary, stream)) +} + +struct MultipartWriter<'a, W> { + inner: W, + boundary: Cow<'a, str>, + data_written: bool, +} + +impl<'a, W: Write> MultipartWriter<'a, W> { + fn new>>(inner: W, boundary: B) -> Self { + MultipartWriter { + inner, + boundary: boundary.into(), + data_written: false, + } + } + + fn write_boundary(&mut self) -> io::Result<()> { + if self.data_written { + self.inner.write_all(b"\r\n")?; + } + + write!(self.inner, "--{}\r\n", self.boundary) + } + + fn write_text(&mut self, name: &str, text: &str) -> io::Result<()> { + chain_result! { + self.write_field_headers(name, None, None), + self.inner.write_all(text.as_bytes()) + } + } + + fn write_file(&mut self, name: &str, path: &Path) -> io::Result<()> { + let (content_type, filename) = mime_filename(path); + let mut file = File::open(path)?; + self.write_stream(&mut file, name, filename, Some(content_type)) + } + + fn write_stream( + &mut self, + stream: &mut S, + name: &str, + filename: Option<&str>, + content_type: Option, + ) -> io::Result<()> { + // This is necessary to make sure it is interpreted as a file on the server end. + let content_type = Some(content_type.unwrap_or(mime::APPLICATION_OCTET_STREAM)); + + chain_result! { + self.write_field_headers(name, filename, content_type), + io::copy(stream, &mut self.inner), + Ok(()) + } + } + + fn write_field_headers( + &mut self, + name: &str, + filename: Option<&str>, + content_type: Option, + ) -> io::Result<()> { + chain_result! { + // Write the first boundary, or the boundary for the previous field. + self.write_boundary(), + { self.data_written = true; Ok(()) }, + write!(self.inner, "Content-Disposition: form-data; name=\"{}\"", name), + filename.map(|filename| write!(self.inner, "; filename=\"{}\"", filename)) + .unwrap_or(Ok(())), + content_type.map(|content_type| write!(self.inner, "\r\nContent-Type: {}", content_type)) + .unwrap_or(Ok(())), + self.inner.write_all(b"\r\n\r\n") + } + } + + fn finish(mut self) -> io::Result { + if self.data_written { + self.inner.write_all(b"\r\n")?; + } + + // always write the closing boundary, even for empty bodies + // trailing CRLF is optional but Actix requires it due to a naive implementation: + // https://github.com/actix/actix-web/issues/598 + write!(self.inner, "--{}--\r\n", self.boundary)?; + Ok(self.inner) + } +} + +fn mime_filename(path: &Path) -> (Mime, Option<&str>) { + let content_type = ::mime_guess::from_path(path); + let filename = opt_filename(path); + (content_type.first_or_octet_stream(), filename) +} + +fn opt_filename(path: &Path) -> Option<&str> { + path.file_name().and_then(|filename| filename.to_str()) +} diff --git a/multipart/src/client/sized.rs b/multipart/src/client/sized.rs new file mode 100644 index 000000000..75a3945c0 --- /dev/null +++ b/multipart/src/client/sized.rs @@ -0,0 +1,90 @@ +// Copyright 2016 `multipart` Crate Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +//! Sized/buffered wrapper around `HttpRequest`. + +use client::{HttpRequest, HttpStream}; + +use std::io; +use std::io::prelude::*; + +/// A wrapper around a request object that measures the request body and sets the `Content-Length` +/// header to its size in bytes. +/// +/// Sized requests are more human-readable and use less bandwidth +/// (as chunking adds [visual noise and overhead][chunked-example]), +/// but they must be able to load their entirety, including the contents of all files +/// and streams, into memory so the request body can be measured. +/// +/// You should really only use sized requests if you intend to inspect the data manually on the +/// server side, as it will produce a more human-readable request body. Also, of course, if the +/// server doesn't support chunked requests or otherwise rejects them. +/// +/// [chunked-example]: http://en.wikipedia.org/wiki/Chunked_transfer_encoding#Example +pub struct SizedRequest { + inner: R, + buffer: Vec, + boundary: String, +} + +impl SizedRequest { + #[doc(hidden)] + pub fn from_request(req: R) -> SizedRequest { + SizedRequest { + inner: req, + buffer: Vec::new(), + boundary: String::new(), + } + } +} + +impl Write for SizedRequest { + fn write(&mut self, data: &[u8]) -> io::Result { + self.buffer.write(data) + } + + fn flush(&mut self) -> io::Result<()> { Ok(()) } +} + +impl HttpRequest for SizedRequest +where ::Error: From { + type Stream = Self; + type Error = R::Error; + + /// `SizedRequest` ignores `_content_len` because it sets its own later. + fn apply_headers(&mut self, boundary: &str, _content_len: Option) -> bool { + self.boundary.clear(); + self.boundary.push_str(boundary); + true + } + + fn open_stream(mut self) -> Result { + self.buffer.clear(); + Ok(self) + } +} + +impl HttpStream for SizedRequest +where ::Error: From { + type Request = Self; + type Response = <::Stream as HttpStream>::Response; + type Error = <::Stream as HttpStream>::Error; + + fn finish(mut self) -> Result { + let content_len = self.buffer.len() as u64; + + if !self.inner.apply_headers(&self.boundary, Some(content_len)) { + return Err(io::Error::new( + io::ErrorKind::Other, + "SizedRequest failed to apply headers to wrapped request." + ).into()); + } + + let mut req = self.inner.open_stream()?; + io::copy(&mut &self.buffer[..], &mut req)?; + req.finish() + } +} diff --git a/multipart/src/lib.rs b/multipart/src/lib.rs new file mode 100644 index 000000000..896ea7549 --- /dev/null +++ b/multipart/src/lib.rs @@ -0,0 +1,131 @@ +// Copyright 2016 `multipart` Crate Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +//! Client- and server-side abstractions for HTTP `multipart/form-data` requests. +//! +//! ### Features: +//! This documentation is built with all features enabled. +//! +//! * `client`: The client-side abstractions for generating multipart requests. +//! +//! * `server`: The server-side abstractions for parsing multipart requests. +//! +//! * `mock`: Provides mock implementations of core `client` and `server` traits for debugging +//! or non-standard use. +//! +//! * `hyper`: Integration with the [Hyper](https://crates.io/crates/hyper) HTTP library +//! for client and/or server depending on which other feature flags are set. +//! +//! * `iron`: Integration with the [Iron](http://crates.io/crates/iron) web application +//! framework. See the [`server::iron`](server/iron/index.html) module for more information. +//! +//! * `nickel` (returning in 0.14!): Integration with the [Nickel](https://crates.io/crates/nickel) +//! web application framework. See the [`server::nickel`](server/nickel/index.html) module for more +//! information. +//! +//! * `tiny_http`: Integration with the [`tiny_http`](https://crates.io/crates/tiny_http) +//! crate. See the [`server::tiny_http`](server/tiny_http/index.html) module for more information. +//! +//! ### Note: Work in Progress +//! I have left a number of Request-for-Comments (RFC) questions on various APIs and other places +//! in the code as there are some cases where I'm not sure what the desirable behavior is. +//! +//! I have opened an issue as a place to collect responses and discussions for these questions +//! [on Github](https://github.com/abonander/multipart/issues/96). Please quote the RFC-statement +//! (and/or link to its source line) and provide your feedback there. +#![cfg_attr(feature = "clippy", feature(plugin))] +#![cfg_attr(feature = "clippy", plugin(clippy))] +#![cfg_attr(feature = "clippy", deny(clippy))] +#![cfg_attr(feature = "bench", feature(test))] +#![deny(missing_docs)] + +#[macro_use] +extern crate log; + +extern crate mime; +extern crate mime_guess; +extern crate rand; +extern crate tempfile; + +#[cfg(feature = "quick-error")] +#[macro_use] +extern crate quick_error; + +#[cfg(feature = "server")] +extern crate safemem; + +#[cfg(feature = "hyper")] +extern crate hyper; + +#[cfg(feature = "iron")] +extern crate iron; + +#[cfg(feature = "tiny_http")] +extern crate tiny_http; + +#[cfg(test)] +extern crate env_logger; + +#[cfg(any(feature = "mock", test))] +pub mod mock; + +use rand::Rng; + +/// Chain a series of results together, with or without previous results. +/// +/// ``` +/// #[macro_use] extern crate multipart; +/// +/// fn try_add_one(val: u32) -> Result { +/// if val < 5 { +/// Ok(val + 1) +/// } else { +/// Err(val) +/// } +/// } +/// +/// fn main() { +/// let res = chain_result! { +/// try_add_one(1), +/// prev -> try_add_one(prev), +/// prev -> try_add_one(prev), +/// prev -> try_add_one(prev) +/// }; +/// +/// println!("{:?}", res); +/// } +/// +/// ``` +#[macro_export] +macro_rules! chain_result { + ($first_expr:expr, $($try_expr:expr),*) => ( + $first_expr $(.and_then(|_| $try_expr))* + ); + ($first_expr:expr, $($($arg:ident),+ -> $try_expr:expr),*) => ( + $first_expr $(.and_then(|$($arg),+| $try_expr))* + ); +} + +#[cfg(feature = "client")] +pub mod client; +#[cfg(feature = "server")] +pub mod server; + +#[cfg(all(test, feature = "client", feature = "server"))] +mod local_test; + +fn random_alphanumeric(len: usize) -> String { + rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(len) + .map(|c| c as char) + .collect() +} + +#[cfg(test)] +fn init_log() { + let _ = env_logger::try_init(); +} diff --git a/multipart/src/local_test.rs b/multipart/src/local_test.rs new file mode 100644 index 000000000..3d24a9204 --- /dev/null +++ b/multipart/src/local_test.rs @@ -0,0 +1,497 @@ +// Copyright 2016 `multipart` Crate Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +use mock::{ClientRequest, HttpBuffer}; + +use server::{FieldHeaders, MultipartField, ReadEntry}; + +use mime::Mime; + +use rand::seq::SliceRandom; +use rand::{self, Rng}; + +use std::collections::hash_map::{Entry, OccupiedEntry}; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::io::prelude::*; +use std::io::Cursor; +use std::iter::{self, FromIterator}; + +const MIN_FIELDS: usize = 1; +const MAX_FIELDS: usize = 3; + +const MIN_LEN: usize = 2; +const MAX_LEN: usize = 5; +const MAX_DASHES: usize = 2; + +fn collect_rand, T, F: FnMut() -> T>(mut gen: F) -> C { + (0..rand::thread_rng().gen_range(MIN_FIELDS..MAX_FIELDS)) + .map(|_| gen()) + .collect() +} + +macro_rules! expect_fmt ( + ($val:expr, $($args:tt)*) => ( + match $val { + Some(val) => val, + None => panic!($($args)*), + } + ); +); + +/// The error is provided as the `err` format argument +macro_rules! expect_ok_fmt ( + ($val:expr, $($args:tt)*) => ( + match $val { + Ok(val) => val, + Err(e) => panic!($($args)*, err=e), + } + ); +); + +fn get_field<'m, V>( + field: &FieldHeaders, + fields: &'m mut HashMap, +) -> Option> { + match fields.entry(field.name.to_string()) { + Entry::Occupied(occupied) => Some(occupied), + Entry::Vacant(_) => None, + } +} + +#[derive(Debug)] +struct TestFields { + texts: HashMap>, + files: HashMap>, +} + +impl TestFields { + fn gen() -> Self { + TestFields { + texts: collect_rand(|| (gen_string(), collect_rand(gen_string))), + files: collect_rand(|| (gen_string(), FileEntry::gen_many())), + } + } + + fn check_field(&mut self, mut field: MultipartField) -> M { + // text/plain fields would be considered a file by `TestFields` + if field.headers.content_type.is_none() { + let mut text_entries = expect_fmt!( + get_field(&field.headers, &mut self.texts), + "Got text field that wasn't in original dataset: {:?}", + field.headers + ); + + let mut text = String::new(); + expect_ok_fmt!( + field.data.read_to_string(&mut text), + "error failed to read text data to string: {:?}\n{err}", + field.headers + ); + + assert!( + text_entries.get_mut().remove(&text), + "Got field text data that wasn't in original data set: {:?}\n{:?}\n{:?}", + field.headers, + text, + text_entries.get(), + ); + + if text_entries.get().is_empty() { + text_entries.remove_entry(); + } + + return field.data.into_inner(); + } + + let mut file_entries = expect_fmt!( + get_field(&field.headers, &mut self.files), + "Got file field that wasn't in original dataset: {:?}", + field.headers + ); + + let field_name = field.headers.name.clone(); + let (test_entry, inner) = FileEntry::from_field(field); + + assert!( + file_entries.get_mut().remove(&test_entry), + "Got field entry that wasn't in original dataset: name: {:?}\n{:?}\nEntries: {:?}", + field_name, + test_entry, + file_entries.get() + ); + + if file_entries.get().is_empty() { + file_entries.remove_entry(); + } + + return inner; + } + + fn assert_is_empty(&self) { + assert!( + self.texts.is_empty(), + "Text Fields were not exhausted! {:?}", + self.texts + ); + assert!( + self.files.is_empty(), + "File Fields were not exhausted! {:?}", + self.files + ); + } +} + +#[derive(Debug, Hash, PartialEq, Eq)] +struct FileEntry { + content_type: Mime, + filename: Option, + data: PrintHex, +} + +impl FileEntry { + fn from_field(mut field: MultipartField) -> (FileEntry, M) { + let mut data = Vec::new(); + expect_ok_fmt!( + field.data.read_to_end(&mut data), + "Error reading file field: {:?}\n{err}", + field.headers + ); + + ( + FileEntry { + content_type: field + .headers + .content_type + .unwrap_or(mime::APPLICATION_OCTET_STREAM), + filename: field.headers.filename, + data: PrintHex(data), + }, + field.data.into_inner(), + ) + } + + fn gen_many() -> HashSet { + collect_rand(Self::gen) + } + + fn gen() -> Self { + let filename = match gen_bool() { + true => Some(gen_string()), + false => None, + }; + + let data = PrintHex(match gen_bool() { + true => gen_string().into_bytes(), + false => gen_bytes(), + }); + + FileEntry { + content_type: rand_mime(), + filename, + data, + } + } + + fn filename(&self) -> Option<&str> { + self.filename.as_ref().map(|s| &**s) + } +} + +#[derive(PartialEq, Eq, Hash)] +struct PrintHex(Vec); + +impl fmt::Debug for PrintHex { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "[")?; + + let mut written = false; + + for byte in &self.0 { + write!(f, "{:X}", byte)?; + + if written { + write!(f, ", ")?; + } + + written = true; + } + + write!(f, "]") + } +} + +macro_rules! do_test ( + ($client_test:ident, $server_test:ident) => ( + ::init_log(); + + info!("Client Test: {:?} Server Test: {:?}", stringify!($client_test), + stringify!($server_test)); + + let mut test_fields = TestFields::gen(); + + trace!("Fields for test: {:?}", test_fields); + + let buf = $client_test(&test_fields); + + trace!( + "\n==Test Buffer Begin==\n{}\n==Test Buffer End==", + String::from_utf8_lossy(&buf.buf) + ); + + $server_test(buf, &mut test_fields); + + test_fields.assert_is_empty(); + ); +); + +#[test] +fn reg_client_reg_server() { + do_test!(test_client, test_server); +} + +#[test] +fn reg_client_entry_server() { + do_test!(test_client, test_server_entry_api); +} + +#[test] +fn lazy_client_reg_server() { + do_test!(test_client_lazy, test_server); +} + +#[test] +fn lazy_client_entry_server() { + do_test!(test_client_lazy, test_server_entry_api); +} + +mod extended { + use super::{test_client, test_client_lazy, test_server, test_server_entry_api, TestFields}; + + use std::time::Instant; + + const TIME_LIMIT_SECS: u64 = 600; + + #[test] + #[ignore] + fn reg_client_reg_server() { + let started = Instant::now(); + + while started.elapsed().as_secs() < TIME_LIMIT_SECS { + do_test!(test_client, test_server); + } + } + + #[test] + #[ignore] + fn reg_client_entry_server() { + let started = Instant::now(); + + while started.elapsed().as_secs() < TIME_LIMIT_SECS { + do_test!(test_client, test_server_entry_api); + } + } + + #[test] + #[ignore] + fn lazy_client_reg_server() { + let started = Instant::now(); + + while started.elapsed().as_secs() < TIME_LIMIT_SECS { + do_test!(test_client_lazy, test_server); + } + } + + #[test] + #[ignore] + fn lazy_client_entry_server() { + let started = Instant::now(); + + while started.elapsed().as_secs() < TIME_LIMIT_SECS { + do_test!(test_client_lazy, test_server_entry_api); + } + } +} + +fn gen_bool() -> bool { + rand::thread_rng().gen() +} + +fn gen_string() -> String { + use rand::distributions::Alphanumeric; + + let mut rng_1 = rand::thread_rng(); + let mut rng_2 = rand::thread_rng(); + + let str_len_1 = rng_1.gen_range(MIN_LEN..=MAX_LEN); + let str_len_2 = rng_2.gen_range(MIN_LEN..=MAX_LEN); + let num_dashes = rng_1.gen_range(0..=MAX_DASHES); + + rng_1 + .sample_iter(&Alphanumeric) + .take(str_len_1) + .chain(iter::repeat(b'-').take(num_dashes)) + .chain(rng_2.sample_iter(&Alphanumeric).take(str_len_2)) + .map(|c| c as char) + .collect() +} + +fn gen_bytes() -> Vec { + gen_string().into_bytes() +} + +fn test_client(test_fields: &TestFields) -> HttpBuffer { + use client::Multipart; + + let request = ClientRequest::default(); + + let mut test_files = test_fields + .files + .iter() + .flat_map(|(name, files)| files.iter().map(move |file| (name, file))); + + let test_texts = test_fields + .texts + .iter() + .flat_map(|(name, texts)| texts.iter().map(move |text| (name, text))); + + let mut multipart = Multipart::from_request(request).unwrap(); + + // Intersperse file fields amongst text fields + for (name, text) in test_texts { + if let Some((file_name, file)) = test_files.next() { + multipart + .write_stream( + file_name, + &mut &*file.data.0, + file.filename(), + Some(file.content_type.clone()), + ) + .unwrap(); + } + + multipart.write_text(name, text).unwrap(); + } + + // Write remaining files + for (file_name, file) in test_files { + multipart + .write_stream( + file_name, + &mut &*file.data.0, + file.filename(), + Some(file.content_type.clone()), + ) + .unwrap(); + } + + multipart.send().unwrap() +} + +fn test_client_lazy(test_fields: &TestFields) -> HttpBuffer { + use client::lazy::Multipart; + + let mut multipart = Multipart::new(); + + let mut test_files = test_fields + .files + .iter() + .flat_map(|(name, files)| files.iter().map(move |file| (name, file))); + + let test_texts = test_fields + .texts + .iter() + .flat_map(|(name, texts)| texts.iter().map(move |text| (name, text))); + + for (name, text) in test_texts { + if let Some((file_name, file)) = test_files.next() { + multipart.add_stream( + &**file_name, + Cursor::new(&file.data.0), + file.filename(), + Some(file.content_type.clone()), + ); + } + + multipart.add_text(&**name, &**text); + } + + for (file_name, file) in test_files { + multipart.add_stream( + &**file_name, + Cursor::new(&file.data.0), + file.filename(), + Some(file.content_type.clone()), + ); + } + + let mut prepared = multipart.prepare().unwrap(); + + let mut buf = Vec::new(); + + let boundary = prepared.boundary().to_owned(); + let content_len = prepared.content_len(); + + prepared.read_to_end(&mut buf).unwrap(); + + HttpBuffer::with_buf(buf, boundary, content_len) +} + +fn test_server(buf: HttpBuffer, fields: &mut TestFields) { + use server::Multipart; + + let server_buf = buf.for_server(); + + if let Some(content_len) = server_buf.content_len { + assert!( + content_len == server_buf.data.len() as u64, + "Supplied content_len different from actual" + ); + } + + let mut multipart = Multipart::from_request(server_buf) + .unwrap_or_else(|_| panic!("Buffer should be multipart!")); + + while let Some(field) = multipart.read_entry_mut().unwrap_opt() { + fields.check_field(field); + } +} + +fn test_server_entry_api(buf: HttpBuffer, fields: &mut TestFields) { + use server::Multipart; + + let server_buf = buf.for_server(); + + if let Some(content_len) = server_buf.content_len { + assert!( + content_len == server_buf.data.len() as u64, + "Supplied content_len different from actual" + ); + } + + let mut multipart = Multipart::from_request(server_buf) + .unwrap_or_else(|_| panic!("Buffer should be multipart!")); + + let entry = multipart + .into_entry() + .expect_alt("Expected entry, got none", "Error reading entry"); + multipart = fields.check_field(entry); + + while let Some(entry) = multipart.into_entry().unwrap_opt() { + multipart = fields.check_field(entry); + } +} + +fn rand_mime() -> Mime { + [ + mime::APPLICATION_OCTET_STREAM, + mime::TEXT_PLAIN, + mime::IMAGE_PNG, + ] + .choose(&mut rand::thread_rng()) + .unwrap() + .clone() +} diff --git a/multipart/src/mock.rs b/multipart/src/mock.rs new file mode 100644 index 000000000..2947661bd --- /dev/null +++ b/multipart/src/mock.rs @@ -0,0 +1,203 @@ +// Copyright 2016 `multipart` Crate Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +//! Mocked types for client-side and server-side APIs. +use std::io::{self, Read, Write}; +use std::fmt; + +use rand::{self, Rng}; +use rand::prelude::ThreadRng; + +/// A mock implementation of `client::HttpRequest` which can spawn an `HttpBuffer`. +/// +/// `client::HttpRequest` impl requires the `client` feature. +#[derive(Default, Debug)] +pub struct ClientRequest { + boundary: Option, + content_len: Option, +} + +#[cfg(feature = "client")] +impl ::client::HttpRequest for ClientRequest { + type Stream = HttpBuffer; + type Error = io::Error; + + fn apply_headers(&mut self, boundary: &str, content_len: Option) -> bool { + self.boundary = Some(boundary.into()); + self.content_len = content_len; + true + } + + /// ## Panics + /// If `apply_headers()` was not called. + fn open_stream(self) -> Result { + debug!("ClientRequest::open_stream called! {:?}", self); + let boundary = self.boundary.expect("ClientRequest::set_headers() was not called!"); + + Ok(HttpBuffer::new_empty(boundary, self.content_len)) + } +} + + +/// A writable buffer which stores the boundary and content-length, if provided. +/// +/// Implements `client::HttpStream` if the `client` feature is enabled. +pub struct HttpBuffer { + /// The buffer containing the raw bytes. + pub buf: Vec, + /// The multipart boundary. + pub boundary: String, + /// The value of the content-length header, if set. + pub content_len: Option, + rng: ThreadRng, +} + +impl HttpBuffer { + /// Create an empty buffer with the given boundary and optional content-length. + pub fn new_empty(boundary: String, content_len: Option) -> HttpBuffer { + Self::with_buf(Vec::new(), boundary, content_len) + } + + /// Wrap the given buffer with the given boundary and optional content-length. + pub fn with_buf(buf: Vec, boundary: String, content_len: Option) -> Self { + HttpBuffer { + buf, + boundary, + content_len, + rng: rand::thread_rng() + } + } + + /// Get a `ServerRequest` wrapping the data in this buffer. + pub fn for_server(&self) -> ServerRequest { + ServerRequest { + data: &self.buf, + boundary: &self.boundary, + content_len: self.content_len, + rng: rand::thread_rng(), + } + } +} + +impl Write for HttpBuffer { + /// To simulate a network connection, this will copy a random number of bytes + /// from `buf` to the buffer. + fn write(&mut self, buf: &[u8]) -> io::Result { + if buf.is_empty() { + debug!("HttpBuffer::write() was passed a zero-sized buffer."); + return Ok(0); + } + + // Simulate the randomness of a network connection by not always reading everything + let len = self.rng.gen_range(1..=buf.len()); + + self.buf.write(&buf[..len]) + } + + fn flush(&mut self) -> io::Result<()> { + self.buf.flush() + } +} + +#[cfg(feature = "client")] +impl ::client::HttpStream for HttpBuffer { + type Request = ClientRequest; + type Response = HttpBuffer; + type Error = io::Error; + + /// Returns `Ok(self)`. + fn finish(self) -> Result { Ok(self) } +} + +impl fmt::Debug for HttpBuffer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("multipart::mock::HttpBuffer") + .field("buf", &self.buf) + .field("boundary", &self.boundary) + .field("content_len", &self.content_len) + .finish() + } +} + +/// A mock implementation of `server::HttpRequest` that can be read. +/// +/// Implements `server::HttpRequest` if the `server` feature is enabled. +pub struct ServerRequest<'a> { + /// Slice of the source `HttpBuffer::buf` + pub data: &'a [u8], + /// The multipart boundary. + pub boundary: &'a str, + /// The value of the content-length header, if set. + pub content_len: Option, + rng: ThreadRng, +} + +impl<'a> ServerRequest<'a> { + /// Create a new `ServerRequest` with the given data and boundary. + /// + /// Assumes `content_len: None` + pub fn new(data: &'a [u8], boundary: &'a str) -> Self { + ServerRequest { + data, + boundary, + content_len: None, + rng: rand::thread_rng(), + } + } +} + +impl<'a> Read for ServerRequest<'a> { + /// To simulate a network connection, this will copy a random number of bytes + /// from the buffer to `out`. + fn read(&mut self, out: &mut [u8]) -> io::Result { + if out.is_empty() { + debug!("ServerRequest::read() was passed a zero-sized buffer."); + return Ok(0); + } + + // Simulate the randomness of a network connection by not always reading everything + let len = self.rng.gen_range(1..=out.len()); + self.data.read(&mut out[..len]) + } +} + +#[cfg(feature = "server")] +impl<'a> ::server::HttpRequest for ServerRequest<'a> { + type Body = Self; + + fn multipart_boundary(&self) -> Option<&str> { Some(self.boundary) } + + fn body(self) -> Self::Body { + self + } +} + +/// A `Write` adapter that duplicates all data written to the inner writer as well as stdout. +pub struct StdoutTee<'s, W> { + inner: W, + stdout: io::StdoutLock<'s>, +} + +impl<'s, W> StdoutTee<'s, W> { + /// Constructor + pub fn new(inner: W, stdout: &'s io::Stdout) -> Self { + Self { + inner, stdout: stdout.lock(), + } + } +} + +impl<'s, W: Write> Write for StdoutTee<'s, W> { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.inner.write_all(buf)?; + self.stdout.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.inner.flush()?; + self.stdout.flush() + } +} diff --git a/multipart/src/server/boundary.rs b/multipart/src/server/boundary.rs new file mode 100644 index 000000000..21f37d14d --- /dev/null +++ b/multipart/src/server/boundary.rs @@ -0,0 +1,598 @@ +// Copyright 2016 `multipart` Crate Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! Boundary parsing for `multipart` requests. + +use ::safemem; + +use super::buf_redux::BufReader; +use super::buf_redux::policy::MinBuffered; +use super::twoway; + +use std::cmp; +use std::borrow::Borrow; + +use std::io; +use std::io::prelude::*; + +use self::State::*; + +pub const MIN_BUF_SIZE: usize = 1024; + +#[derive(Debug, PartialEq, Eq)] +enum State { + Searching, + BoundaryRead, + AtEnd +} + +/// A struct implementing `Read` and `BufRead` that will yield bytes until it sees a given sequence. +#[derive(Debug)] +pub struct BoundaryReader { + source: BufReader, + boundary: Vec, + search_idx: usize, + state: State, +} + +impl BoundaryReader where R: Read { + /// Internal API + pub fn from_reader>>(reader: R, boundary: B) -> BoundaryReader { + let mut boundary = boundary.into(); + safemem::prepend(b"--", &mut boundary); + let source = BufReader::new(reader).set_policy(MinBuffered(MIN_BUF_SIZE)); + + BoundaryReader { + source, + boundary, + search_idx: 0, + state: Searching, + } + } + + fn read_to_boundary(&mut self) -> io::Result<&[u8]> { + let buf = self.source.fill_buf()?; + + trace!("Buf: {:?}", String::from_utf8_lossy(buf)); + + debug!("Before search Buf len: {} Search idx: {} State: {:?}", + buf.len(), self.search_idx, self.state); + + if self.state == BoundaryRead || self.state == AtEnd { + return Ok(&buf[..self.search_idx]) + } + + if self.state == Searching && self.search_idx < buf.len() { + let lookahead = &buf[self.search_idx..]; + + // Look for the boundary, or if it isn't found, stop near the end. + match find_boundary(lookahead, &self.boundary) { + Ok(found_idx) => { + self.search_idx += found_idx; + self.state = BoundaryRead; + }, + Err(yield_len) => { + self.search_idx += yield_len; + } + } + } + + debug!("After search Buf len: {} Search idx: {} State: {:?}", + buf.len(), self.search_idx, self.state); + + // back up the cursor to before the boundary's preceding CRLF if we haven't already + if self.search_idx >= 2 && !buf[self.search_idx..].starts_with(b"\r\n") { + let two_bytes_before = &buf[self.search_idx - 2 .. self.search_idx]; + + trace!("Two bytes before: {:?} ({:?}) (\"\\r\\n\": {:?})", + String::from_utf8_lossy(two_bytes_before), two_bytes_before, b"\r\n"); + + if two_bytes_before == *b"\r\n" { + debug!("Subtract two!"); + self.search_idx -= 2; + } + } + + let ret_buf = &buf[..self.search_idx]; + + trace!("Returning buf: {:?}", String::from_utf8_lossy(ret_buf)); + + Ok(ret_buf) + } + + pub fn set_min_buf_size(&mut self, min_buf_size: usize) { + // ensure the minimum buf size is at least enough to find a boundary with some extra + let min_buf_size = cmp::max(self.boundary.len() * 2, min_buf_size); + + self.source.policy_mut().0 = min_buf_size; + } + + pub fn consume_boundary(&mut self) -> io::Result { + if self.state == AtEnd { + return Ok(false); + } + + while self.state == Searching { + debug!("Boundary not found yet"); + + let buf_len = self.read_to_boundary()?.len(); + + if buf_len == 0 && self.state == Searching { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, + "unexpected end of request body")); + } + + debug!("Discarding {} bytes", buf_len); + + self.consume(buf_len); + } + + let consume_amt = { + let buf = self.source.fill_buf()?; + + // if the boundary is found we should have at least this much in-buffer + let mut consume_amt = self.search_idx + self.boundary.len(); + + // we don't care about data before the cursor + let bnd_segment = &buf[self.search_idx..]; + + if bnd_segment.starts_with(b"\r\n") { + // preceding CRLF needs to be consumed as well + consume_amt += 2; + + // assert that we've found the boundary after the CRLF + debug_assert_eq!(*self.boundary, bnd_segment[2 .. self.boundary.len() + 2]); + } else { + // assert that we've found the boundary + debug_assert_eq!(*self.boundary, bnd_segment[..self.boundary.len()]); + } + + // include the trailing CRLF or -- + consume_amt += 2; + + if buf.len() < consume_amt { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, + "not enough bytes to verify boundary")); + } + + // we have enough bytes to verify + self.state = Searching; + + let last_two = &buf[consume_amt - 2 .. consume_amt]; + + match last_two { + b"\r\n" => self.state = Searching, + b"--" => self.state = AtEnd, + _ => return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("unexpected bytes following multipart boundary: {:X} {:X}", + last_two[0], last_two[1]) + )), + } + + consume_amt + }; + + trace!("Consuming {} bytes, remaining buf: {:?}", + consume_amt, + String::from_utf8_lossy(self.source.buffer())); + + self.source.consume(consume_amt); + + if cfg!(debug_assertions) { + + } + + self.search_idx = 0; + + trace!("Consumed boundary (state: {:?}), remaining buf: {:?}", self.state, + String::from_utf8_lossy(self.source.buffer())); + + Ok(self.state != AtEnd) + } +} + +/// Find the boundary occurrence or the highest length to safely yield +fn find_boundary(buf: &[u8], boundary: &[u8]) -> Result { + if let Some(idx) = twoway::find_bytes(buf, boundary) { + return Ok(idx); + } + + let search_start = buf.len().saturating_sub(boundary.len()); + + // search for just the boundary fragment + for i in search_start .. buf.len() { + if boundary.starts_with(&buf[i..]) { + return Err(i); + } + } + + Err(buf.len()) +} + +#[cfg(feature = "bench")] +impl<'a> BoundaryReader> { + fn new_with_bytes(bytes: &'a [u8], boundary: &str) -> Self { + Self::from_reader(io::Cursor::new(bytes), boundary) + } + + fn reset(&mut self) { + // Dump buffer and reset cursor + self.source.seek(io::SeekFrom::Start(0)); + self.state = Searching; + self.search_idx = 0; + } +} + +impl Borrow for BoundaryReader { + fn borrow(&self) -> &R { + self.source.get_ref() + } +} + +impl Read for BoundaryReader where R: Read { + fn read(&mut self, out: &mut [u8]) -> io::Result { + let read = { + let mut buf = self.read_to_boundary()?; + // This shouldn't ever be an error so unwrapping is fine. + buf.read(out).unwrap() + }; + + self.consume(read); + Ok(read) + } +} + +impl BufRead for BoundaryReader where R: Read { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + self.read_to_boundary() + } + + fn consume(&mut self, amt: usize) { + let true_amt = cmp::min(amt, self.search_idx); + + debug!("Consume! amt: {} true amt: {}", amt, true_amt); + + self.source.consume(true_amt); + self.search_idx -= true_amt; + } +} + +#[cfg(test)] +mod test { + use super::BoundaryReader; + + use std::io; + use std::io::prelude::*; + + const BOUNDARY: &'static str = "boundary"; + const TEST_VAL: &'static str = "--boundary\r\n\ + dashed-value-1\r\n\ + --boundary\r\n\ + dashed-value-2\r\n\ + --boundary--"; + + #[test] + fn test_boundary() { + ::init_log(); + + debug!("Testing boundary (no split)"); + + let src = &mut TEST_VAL.as_bytes(); + let mut reader = BoundaryReader::from_reader(src, BOUNDARY); + + let mut buf = String::new(); + + test_boundary_reader(&mut reader, &mut buf); + } + + struct SplitReader<'a> { + left: &'a [u8], + right: &'a [u8], + } + + impl<'a> SplitReader<'a> { + fn split(data: &'a [u8], at: usize) -> SplitReader<'a> { + let (left, right) = data.split_at(at); + + SplitReader { + left: left, + right: right, + } + } + } + + impl<'a> Read for SplitReader<'a> { + fn read(&mut self, dst: &mut [u8]) -> io::Result { + fn copy_bytes_partial(src: &mut &[u8], dst: &mut [u8]) -> usize { + src.read(dst).unwrap() + } + + let mut copy_amt = copy_bytes_partial(&mut self.left, dst); + + if copy_amt == 0 { + copy_amt = copy_bytes_partial(&mut self.right, dst) + }; + + Ok(copy_amt) + } + } + + #[test] + fn test_split_boundary() { + ::init_log(); + + debug!("Testing boundary (split)"); + + let mut buf = String::new(); + + // Substitute for `.step_by()` being unstable. + for split_at in 0 .. TEST_VAL.len(){ + debug!("Testing split at: {}", split_at); + + let src = SplitReader::split(TEST_VAL.as_bytes(), split_at); + let mut reader = BoundaryReader::from_reader(src, BOUNDARY); + test_boundary_reader(&mut reader, &mut buf); + } + } + + fn test_boundary_reader(reader: &mut BoundaryReader, buf: &mut String) { + buf.clear(); + + debug!("Read 1"); + let _ = reader.read_to_string(buf).unwrap(); + assert!(buf.is_empty(), "Buffer not empty: {:?}", buf); + buf.clear(); + + debug!("Consume 1"); + reader.consume_boundary().unwrap(); + + debug!("Read 2"); + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, "dashed-value-1"); + buf.clear(); + + debug!("Consume 2"); + reader.consume_boundary().unwrap(); + + debug!("Read 3"); + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, "dashed-value-2"); + buf.clear(); + + debug!("Consume 3"); + reader.consume_boundary().unwrap(); + + debug!("Read 4"); + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, ""); + } + + #[test] + fn test_empty_body() { + ::init_log(); + + // empty body contains closing boundary only + let mut body: &[u8] = b"--boundary--"; + + let ref mut buf = String::new(); + let mut reader = BoundaryReader::from_reader(&mut body, BOUNDARY); + + debug!("Consume 1"); + assert_eq!(reader.consume_boundary().unwrap(), false); + + debug!("Read 1"); + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, ""); + buf.clear(); + + debug!("Consume 2"); + assert_eq!(reader.consume_boundary().unwrap(), false); + } + + #[test] + fn test_leading_crlf() { + ::init_log(); + + let mut body: &[u8] = b"\r\n\r\n--boundary\r\n\ + asdf1234\ + \r\n\r\n--boundary--"; + + let ref mut buf = String::new(); + let mut reader = BoundaryReader::from_reader(&mut body, BOUNDARY); + + + debug!("Consume 1"); + assert_eq!(reader.consume_boundary().unwrap(), true); + + debug!("Read 1"); + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, "asdf1234\r\n"); + buf.clear(); + + debug!("Consume 2"); + assert_eq!(reader.consume_boundary().unwrap(), false); + + debug!("Read 2 (empty)"); + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, ""); + } + + #[test] + fn test_trailing_crlf() { + ::init_log(); + + let mut body: &[u8] = b"--boundary\r\n\ + asdf1234\ + \r\n\r\n--boundary\r\n\ + hjkl5678\r\n--boundary--"; + + let ref mut buf = String::new(); + let mut reader = BoundaryReader::from_reader(&mut body, BOUNDARY); + + debug!("Consume 1"); + assert_eq!(reader.consume_boundary().unwrap(), true); + + debug!("Read 1"); + + // Repro for https://github.com/abonander/multipart/issues/93 + // These two reads should produce the same buffer + let buf1 = reader.read_to_boundary().unwrap().to_owned(); + let buf2 = reader.read_to_boundary().unwrap().to_owned(); + assert_eq!(buf1, buf2); + + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, "asdf1234\r\n"); + buf.clear(); + + debug!("Consume 2"); + assert_eq!(reader.consume_boundary().unwrap(), true); + + debug!("Read 2"); + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, "hjkl5678"); + buf.clear(); + + debug!("Consume 3"); + assert_eq!(reader.consume_boundary().unwrap(), false); + + debug!("Read 3 (empty)"); + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, ""); + } + + // https://github.com/abonander/multipart/issues/93#issuecomment-343610587 + #[test] + fn test_trailing_lflf() { + ::init_log(); + + let mut body: &[u8] = b"--boundary\r\n\ + asdf1234\ + \n\n\r\n--boundary\r\n\ + hjkl5678\r\n--boundary--"; + + let ref mut buf = String::new(); + let mut reader = BoundaryReader::from_reader(&mut body, BOUNDARY); + + debug!("Consume 1"); + assert_eq!(reader.consume_boundary().unwrap(), true); + + debug!("Read 1"); + + // same as above + let buf1 = reader.read_to_boundary().unwrap().to_owned(); + let buf2 = reader.read_to_boundary().unwrap().to_owned(); + assert_eq!(buf1, buf2); + + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, "asdf1234\n\n"); + buf.clear(); + + debug!("Consume 2"); + assert_eq!(reader.consume_boundary().unwrap(), true); + + debug!("Read 2"); + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, "hjkl5678"); + buf.clear(); + + debug!("Consume 3"); + assert_eq!(reader.consume_boundary().unwrap(), false); + + debug!("Read 3 (empty)"); + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, ""); + } + + // https://github.com/abonander/multipart/issues/104 + #[test] + fn test_unterminated_body() { + ::init_log(); + + let mut body: &[u8] = b"--boundary\r\n\ + asdf1234\ + \n\n\r\n--boundary\r\n\ + hjkl5678 "; + + let ref mut buf = String::new(); + let mut reader = BoundaryReader::from_reader(&mut body, BOUNDARY); + + debug!("Consume 1"); + assert_eq!(reader.consume_boundary().unwrap(), true); + + debug!("Read 1"); + + // same as above + let buf1 = reader.read_to_boundary().unwrap().to_owned(); + let buf2 = reader.read_to_boundary().unwrap().to_owned(); + assert_eq!(buf1, buf2); + + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, "asdf1234\n\n"); + buf.clear(); + + debug!("Consume 2"); + assert_eq!(reader.consume_boundary().unwrap(), true); + + debug!("Read 2"); + let _ = reader.read_to_string(buf).unwrap(); + assert_eq!(buf, "hjkl5678 "); + buf.clear(); + + debug!("Consume 3 - expecting error"); + reader.consume_boundary().unwrap_err(); + } + + #[test] + fn test_lone_boundary() { + let mut body: &[u8] = b"--boundary"; + let mut reader = BoundaryReader::from_reader(&mut body, "boundary"); + reader.consume_boundary().unwrap_err(); + } + + #[test] + fn test_invalid_boundary() { + let mut body: &[u8] = b"--boundary\x00\x00"; + let mut reader = BoundaryReader::from_reader(&mut body, "boundary"); + reader.consume_boundary().unwrap_err(); + } + + #[test] + fn test_skip_field() { + let mut body: &[u8] = b"--boundary\r\nfield1\r\n--boundary\r\nfield2\r\n--boundary--"; + let mut reader = BoundaryReader::from_reader(&mut body, "boundary"); + + assert_eq!(reader.consume_boundary().unwrap(), true); + // skip `field1` + assert_eq!(reader.consume_boundary().unwrap(), true); + + let mut buf = String::new(); + reader.read_to_string(&mut buf).unwrap(); + assert_eq!(buf, "field2"); + + assert_eq!(reader.consume_boundary().unwrap(), false); + } + + #[cfg(feature = "bench")] + mod bench { + extern crate test; + use self::test::Bencher; + + use super::*; + + #[bench] + fn bench_boundary_reader(b: &mut Bencher) { + let mut reader = BoundaryReader::new_with_bytes(TEST_VAL.as_bytes(), BOUNDARY); + let mut buf = String::with_capacity(256); + + b.iter(|| { + reader.reset(); + test_boundary_reader(&mut reader, &mut buf); + }); + } + } +} diff --git a/multipart/src/server/field.rs b/multipart/src/server/field.rs new file mode 100644 index 000000000..6ccf9525b --- /dev/null +++ b/multipart/src/server/field.rs @@ -0,0 +1,603 @@ +// Copyright 2016 `multipart` Crate Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! `multipart` field header parsing. +use mime::Mime; + +use std::error::Error; +use std::io::{self, BufRead, Read}; +use std::{fmt, str}; + +use std::sync::Arc; + +use super::httparse::{self, Error as HttparseError, Header, Status, EMPTY_HEADER}; + +use self::ReadEntryResult::*; + +use super::save::SaveBuilder; + +const EMPTY_STR_HEADER: StrHeader<'static> = StrHeader { name: "", val: "" }; + +macro_rules! invalid_cont_disp { + ($reason: expr, $cause: expr) => { + return Err(ParseHeaderError::InvalidContDisp( + $reason, + $cause.to_string(), + )); + }; +} + +/// Not exposed +#[derive(Copy, Clone, Debug)] +pub struct StrHeader<'a> { + name: &'a str, + val: &'a str, +} + +struct DisplayHeaders<'s, 'a: 's>(&'s [StrHeader<'a>]); + +impl<'s, 'a: 's> fmt::Display for DisplayHeaders<'s, 'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for hdr in self.0 { + writeln!(f, "{}: {}", hdr.name, hdr.val)?; + } + + Ok(()) + } +} + +fn with_headers(r: &mut R, closure: F) -> Result +where + R: BufRead, + F: FnOnce(&[StrHeader]) -> Ret, +{ + const HEADER_LEN: usize = 4; + + let consume; + let ret; + + let mut last_len = 0; + + loop { + // this should return a larger buffer each time + let buf = r.fill_buf()?; + + // buffer has stopped growing + if buf.len() == last_len { + return Err(ParseHeaderError::TooLarge); + } + + let mut raw_headers = [EMPTY_HEADER; HEADER_LEN]; + + match httparse::parse_headers(buf, &mut raw_headers)? { + // read more and try again + Status::Partial => last_len = buf.len(), + Status::Complete((consume_, raw_headers)) => { + let mut headers = [EMPTY_STR_HEADER; HEADER_LEN]; + let headers = copy_headers(raw_headers, &mut headers)?; + debug!("Parsed headers: {:?}", headers); + consume = consume_; + ret = closure(headers); + break; + } + } + } + + r.consume(consume); + Ok(ret) +} + +fn copy_headers<'h, 'b: 'h>( + raw: &[Header<'b>], + headers: &'h mut [StrHeader<'b>], +) -> io::Result<&'h [StrHeader<'b>]> { + for (raw, header) in raw.iter().zip(&mut *headers) { + header.name = raw.name; + header.val = io_str_utf8(raw.value)?; + } + + Ok(&headers[..raw.len()]) +} + +/// The headers that (may) appear before a `multipart/form-data` field. +/// +/// ### Warning: Values are Client-Provided +/// Everything in this struct are values from the client and should be considered **untrustworthy**. +/// This crate makes no effort to validate or sanitize any client inputs. +#[derive(Clone, Debug)] +pub struct FieldHeaders { + /// The field's name from the form. + pub name: Arc, + + /// The filename of this entry, if supplied. This is not guaranteed to match the original file + /// or even to be a valid filename for the current platform. + pub filename: Option, + + /// The MIME type (`Content-Type` value) of this file, if supplied by the client. + /// + /// If this is not supplied, the content-type of the field should default to `text/plain` as + /// per [IETF RFC 7578, section 4.4](https://tools.ietf.org/html/rfc7578#section-4.4), but this + /// should not be implicitly trusted. This crate makes no attempt to identify or validate + /// the content-type of the actual field data. + pub content_type: Option, +} + +impl FieldHeaders { + /// Parse the field headers from the passed `BufRead`, consuming the relevant bytes. + fn read_from(r: &mut R) -> Result { + with_headers(r, Self::parse)? + } + + fn parse(headers: &[StrHeader]) -> Result { + let cont_disp = ContentDisp::parse_required(headers)?; + + Ok(FieldHeaders { + name: cont_disp.field_name.into(), + filename: cont_disp.filename, + content_type: parse_content_type(headers)?, + }) + } +} + +/// The `Content-Disposition` header. +struct ContentDisp { + /// The name of the `multipart/form-data` field. + field_name: String, + /// The optional filename for this field. + filename: Option, +} + +impl ContentDisp { + fn parse_required(headers: &[StrHeader]) -> Result { + let header = if let Some(header) = find_header(headers, "Content-Disposition") { + header + } else { + return Err(ParseHeaderError::MissingContentDisposition( + DisplayHeaders(headers).to_string(), + )); + }; + + // Content-Disposition: ? + let after_disp_type = match split_once(header.val, ';') { + Some((disp_type, after_disp_type)) => { + // assert Content-Disposition: form-data + // but needs to be parsed out to trim the spaces (allowed by spec IIRC) + if disp_type.trim() != "form-data" { + invalid_cont_disp!("unexpected Content-Disposition value", disp_type); + } + after_disp_type + } + None => invalid_cont_disp!( + "expected additional data after Content-Disposition type", + header.val + ), + }; + + // Content-Disposition: form-data; name=? + let (field_name, filename) = match get_str_after("name=", ';', after_disp_type) { + None => invalid_cont_disp!( + "expected field name and maybe filename, got", + after_disp_type + ), + // Content-Disposition: form-data; name={field_name}; filename=? + Some((field_name, after_field_name)) => { + let field_name = trim_quotes(field_name); + let filename = get_str_after("filename=", ';', after_field_name) + .map(|(filename, _)| trim_quotes(filename).to_owned()); + (field_name, filename) + } + }; + + Ok(ContentDisp { + field_name: field_name.to_owned(), + filename, + }) + } +} + +fn parse_content_type(headers: &[StrHeader]) -> Result, ParseHeaderError> { + if let Some(header) = find_header(headers, "Content-Type") { + // Boundary parameter will be parsed into the `Mime` + debug!("Found Content-Type: {:?}", header.val); + Ok(Some(header.val.parse::().map_err(|_| { + ParseHeaderError::MimeError(header.val.into()) + })?)) + } else { + Ok(None) + } +} + +/// A field in a multipart request with its associated headers and data. +#[derive(Debug)] +pub struct MultipartField { + /// The headers for this field, including the name, filename, and content-type, if provided. + /// + /// ### Warning: Values are Client-Provided + /// Everything in this struct are values from the client and should be considered **untrustworthy**. + /// This crate makes no effort to validate or sanitize any client inputs. + pub headers: FieldHeaders, + + /// The field's data. + pub data: MultipartData, +} + +impl MultipartField { + /// Returns `true` if this field has no content-type or the content-type is `text/...`. + /// + /// This typically means it can be read to a string, but it could still be using an unsupported + /// character encoding, so decoding to `String` needs to ensure that the data is valid UTF-8. + /// + /// Note also that the field contents may be too large to reasonably fit in memory. + /// The `.save()` adapter can be used to enforce a size limit. + /// + /// Detecting character encodings by any means is (currently) beyond the scope of this crate. + pub fn is_text(&self) -> bool { + self.headers + .content_type + .as_ref() + .map_or(true, |ct| ct.type_() == mime::TEXT) + } + + /// Read the next entry in the request. + pub fn next_entry(self) -> ReadEntryResult { + self.data.into_inner().read_entry() + } + + /// Update `self` as the next entry. + /// + /// Returns `Ok(Some(self))` if another entry was read, `Ok(None)` if the end of the body was + /// reached, and `Err(e)` for any errors that occur. + pub fn next_entry_inplace(&mut self) -> io::Result> + where + for<'a> &'a mut M: ReadEntry, + { + let multipart = self.data.take_inner(); + + match multipart.read_entry() { + Entry(entry) => { + *self = entry; + Ok(Some(self)) + } + End(multipart) => { + self.data.give_inner(multipart); + Ok(None) + } + Error(multipart, err) => { + self.data.give_inner(multipart); + Err(err) + } + } + } +} + +/// The data of a field in a `multipart/form-data` request. +/// +/// You can read it to EOF, or use the `save()` adaptor to save it to disk/memory. +#[derive(Debug)] +pub struct MultipartData { + inner: Option, +} + +const DATA_INNER_ERR: &str = "MultipartFile::inner taken and not replaced; this is likely \ + caused by a logic error in `multipart` or by resuming after \ + a previously caught panic.\nPlease open an issue with the \ + relevant backtrace and debug logs at \ + https://github.com/abonander/multipart"; + +impl MultipartData +where + M: ReadEntry, +{ + /// Get a builder type which can save the field with or without a size limit. + pub fn save(&mut self) -> SaveBuilder<&mut Self> { + SaveBuilder::new(self) + } + + /// Take the inner `Multipart` or `&mut Multipart` + pub fn into_inner(self) -> M { + self.inner.expect(DATA_INNER_ERR) + } + + /// Set the minimum buffer size that `BufRead::fill_buf(self)` will return + /// until the end of the stream is reached. Set this as small as you can tolerate + /// to minimize `read()` calls (`read()` won't be called again until the buffer + /// is smaller than this). + /// + /// This value is reset between fields. + pub fn set_min_buf_size(&mut self, min_buf_size: usize) { + self.inner_mut().set_min_buf_size(min_buf_size) + } + + fn inner_mut(&mut self) -> &mut M { + self.inner.as_mut().expect(DATA_INNER_ERR) + } + + fn take_inner(&mut self) -> M { + self.inner.take().expect(DATA_INNER_ERR) + } + + fn give_inner(&mut self, inner: M) { + self.inner = Some(inner); + } +} + +impl Read for MultipartData { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.inner_mut().source_mut().read(buf) + } +} + +/// In this implementation, `fill_buf()` can return more data with each call. +/// +/// Use `set_min_buf_size()` if you require a minimum buffer length. +impl BufRead for MultipartData { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + self.inner_mut().source_mut().fill_buf() + } + + fn consume(&mut self, amt: usize) { + self.inner_mut().source_mut().consume(amt) + } +} + +fn split_once(s: &str, delim: char) -> Option<(&str, &str)> { + s.find(delim).map(|idx| s.split_at(idx)) +} + +fn trim_quotes(s: &str) -> &str { + s.trim_matches('"') +} + +/// Get the string after `needle` in `haystack`, stopping before `end_val_delim` +fn get_str_after<'a>( + needle: &str, + end_val_delim: char, + haystack: &'a str, +) -> Option<(&'a str, &'a str)> { + let val_start_idx = try_opt!(haystack.find(needle)) + needle.len(); + let val_end_idx = haystack[val_start_idx..] + .find(end_val_delim) + .map_or(haystack.len(), |end_idx| end_idx + val_start_idx); + Some(( + &haystack[val_start_idx..val_end_idx], + &haystack[val_end_idx..], + )) +} + +fn io_str_utf8(buf: &[u8]) -> io::Result<&str> { + str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) +} + +fn find_header<'a, 'b>(headers: &'a [StrHeader<'b>], name: &str) -> Option<&'a StrHeader<'b>> { + // Field names are case insensitive and consist of ASCII characters + // only (see https://tools.ietf.org/html/rfc822#section-3.2). + headers + .iter() + .find(|header| header.name.eq_ignore_ascii_case(name)) +} + +/// Common trait for `Multipart` and `&mut Multipart` +pub trait ReadEntry: PrivReadEntry + Sized { + /// Attempt to read the next entry in the multipart stream. + fn read_entry(mut self) -> ReadEntryResult { + self.set_min_buf_size(super::boundary::MIN_BUF_SIZE); + + debug!("ReadEntry::read_entry()"); + + if !try_read_entry!(self; self.consume_boundary()) { + return End(self); + } + + let field_headers: FieldHeaders = try_read_entry!(self; self.read_headers()); + + if let Some(ct) = field_headers.content_type.as_ref() { + if ct.type_() == mime::MULTIPART { + // fields of this type are sent by (supposedly) no known clients + // (https://tools.ietf.org/html/rfc7578#appendix-A) so I'd be fascinated + // to hear about any in the wild + info!( + "Found nested multipart field: {:?}:\r\n\ + Please report this client's User-Agent and any other available details \ + at https://github.com/abonander/multipart/issues/56", + field_headers + ); + } + } + + Entry(MultipartField { + headers: field_headers, + data: MultipartData { inner: Some(self) }, + }) + } + + /// Equivalent to `read_entry()` but takes `&mut self` + fn read_entry_mut(&mut self) -> ReadEntryResult<&mut Self> { + ReadEntry::read_entry(self) + } +} + +impl ReadEntry for T where T: PrivReadEntry {} + +/// Public trait but not re-exported. +pub trait PrivReadEntry { + type Source: BufRead; + + fn source_mut(&mut self) -> &mut Self::Source; + + fn set_min_buf_size(&mut self, min_buf_size: usize); + + /// Consume the next boundary. + /// Returns `true` if a field should follow, `false` otherwise. + fn consume_boundary(&mut self) -> io::Result; + + fn read_headers(&mut self) -> Result { + FieldHeaders::read_from(self.source_mut()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) + } + + fn read_to_string(&mut self) -> io::Result { + let mut buf = String::new(); + + match self.source_mut().read_to_string(&mut buf) { + Ok(_) => Ok(buf), + Err(err) => Err(err), + } + } +} + +impl<'a, M: ReadEntry> PrivReadEntry for &'a mut M { + type Source = M::Source; + + fn source_mut(&mut self) -> &mut M::Source { + (**self).source_mut() + } + + fn set_min_buf_size(&mut self, min_buf_size: usize) { + (**self).set_min_buf_size(min_buf_size) + } + + fn consume_boundary(&mut self) -> io::Result { + (**self).consume_boundary() + } +} + +/// Ternary result type returned by `ReadEntry::next_entry()`, +/// `Multipart::into_entry()` and `MultipartField::next_entry()`. +pub enum ReadEntryResult> { + /// The next entry was found. + Entry(Entry), + /// No more entries could be read. + End(M), + /// An error occurred. + Error(M, io::Error), +} + +impl ReadEntryResult { + /// Convert `self` into `Result>` as follows: + /// + /// * `Entry(entry) -> Ok(Some(entry))` + /// * `End(_) -> Ok(None)` + /// * `Error(_, err) -> Err(err)` + pub fn into_result(self) -> io::Result> { + match self { + ReadEntryResult::Entry(entry) => Ok(Some(entry)), + ReadEntryResult::End(_) => Ok(None), + ReadEntryResult::Error(_, err) => Err(err), + } + } + + /// Attempt to unwrap `Entry`, panicking if this is `End` or `Error`. + pub fn unwrap(self) -> Entry { + self.expect_alt( + "`ReadEntryResult::unwrap()` called on `End` value", + "`ReadEntryResult::unwrap()` called on `Error` value: {:?}", + ) + } + + /// Attempt to unwrap `Entry`, panicking if this is `End` or `Error` + /// with the given message. Adds the error's message in the `Error` case. + pub fn expect(self, msg: &str) -> Entry { + self.expect_alt(msg, msg) + } + + /// Attempt to unwrap `Entry`, panicking if this is `End` or `Error`. + /// If this is `End`, panics with `end_msg`; if `Error`, panics with `err_msg` + /// as well as the error's message. + pub fn expect_alt(self, end_msg: &str, err_msg: &str) -> Entry { + match self { + Entry(entry) => entry, + End(_) => panic!("{}", end_msg), + Error(_, err) => panic!("{}: {:?}", err_msg, err), + } + } + + /// Attempt to unwrap as `Option`, panicking in the `Error` case. + pub fn unwrap_opt(self) -> Option { + self.expect_opt("`ReadEntryResult::unwrap_opt()` called on `Error` value") + } + + /// Attempt to unwrap as `Option`, panicking in the `Error` case + /// with the given message as well as the error's message. + pub fn expect_opt(self, msg: &str) -> Option { + match self { + Entry(entry) => Some(entry), + End(_) => None, + Error(_, err) => panic!("{}: {:?}", msg, err), + } + } +} + +const GENERIC_PARSE_ERR: &str = "an error occurred while parsing field headers"; + +quick_error! { + #[derive(Debug)] + enum ParseHeaderError { + /// The `Content-Disposition` header was not found + MissingContentDisposition(headers: String) { + display(x) -> ("{}:\n{}", x.description(), headers) + description("\"Content-Disposition\" header not found in field headers") + } + InvalidContDisp(reason: &'static str, cause: String) { + display(x) -> ("{}: {}: {}", x.description(), reason, cause) + description("invalid \"Content-Disposition\" header") + } + /// The header was found but could not be parsed + TokenizeError(err: HttparseError) { + description(GENERIC_PARSE_ERR) + display(x) -> ("{}: {}", x.description(), err) + cause(err) + from() + } + MimeError(cont_type: String) { + description("Failed to parse Content-Type") + display(this) -> ("{}: {}", this.description(), cont_type) + } + TooLarge { + description("field headers section ridiculously long or missing trailing CRLF-CRLF") + } + /// IO error + Io(err: io::Error) { + description("an io error occurred while parsing the headers") + display(x) -> ("{}: {}", x.description(), err) + cause(err) + from() + } + } +} + +#[test] +fn test_find_header() { + let headers = [ + StrHeader { + name: "Content-Type", + val: "text/plain", + }, + StrHeader { + name: "Content-disposition", + val: "form-data", + }, + StrHeader { + name: "content-transfer-encoding", + val: "binary", + }, + ]; + + assert_eq!( + find_header(&headers, "Content-Type").unwrap().val, + "text/plain" + ); + assert_eq!( + find_header(&headers, "Content-Disposition").unwrap().val, + "form-data" + ); + assert_eq!( + find_header(&headers, "Content-Transfer-Encoding") + .unwrap() + .val, + "binary" + ); +} diff --git a/multipart/src/server/hyper.rs b/multipart/src/server/hyper.rs new file mode 100644 index 000000000..57022dc6d --- /dev/null +++ b/multipart/src/server/hyper.rs @@ -0,0 +1,139 @@ +// Copyright 2016 `multipart` Crate Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +//! Server-side integration with [Hyper](https://github.com/hyperium/hyper). +//! Enabled with the `hyper` feature (on by default). +//! +//! Also contains an implementation of [`HttpRequest`](../trait.HttpRequest.html) +//! for `hyper::server::Request` and `&mut hyper::server::Request`. +use hyper::net::Fresh; +use hyper::header::ContentType; +use hyper::method::Method; +use hyper::server::{Handler, Request, Response}; + +pub use hyper::server::Request as HyperRequest; + +use hyper::mime::{Mime, TopLevel, SubLevel, Attr, Value}; + +use super::{Multipart, HttpRequest}; + +/// A container that implements `hyper::server::Handler` which will switch +/// the handler implementation depending on if the incoming request is multipart or not. +/// +/// Create an instance with `new()` and pass it to `hyper::server::Server::listen()` where +/// you would normally pass a `Handler` instance. +/// +/// A convenient wrapper for `Multipart::from_request()`. +pub struct Switch { + normal: H, + multipart: M, +} + +impl Switch where H: Handler, M: MultipartHandler { + /// Create a new `Switch` instance where + /// `normal` handles normal Hyper requests and `multipart` handles Multipart requests + pub fn new(normal: H, multipart: M) -> Switch { + Switch { normal, multipart } + } +} + +impl Handler for Switch where H: Handler, M: MultipartHandler { + fn handle<'a, 'k>(&'a self, req: Request<'a, 'k>, res: Response<'a, Fresh>) { + match Multipart::from_request(req) { + Ok(multi) => self.multipart.handle_multipart(multi, res), + Err(req) => self.normal.handle(req, res), + } + } +} + +/// A trait defining a type that can handle an incoming multipart request. +/// +/// Extends to closures of the type `Fn(Multipart, Response)`, +/// and subsequently static functions. +pub trait MultipartHandler: Send + Sync { + /// Generate a response from this multipart request. + fn handle_multipart<'a, 'k>(&self, + multipart: Multipart>, + response: Response<'a, Fresh>); +} + +impl MultipartHandler for F +where F: Fn(Multipart, Response), F: Send + Sync { + fn handle_multipart<'a, 'k>(&self, + multipart: Multipart>, + response: Response<'a, Fresh>) { + (*self)(multipart, response); + } +} + +impl<'a, 'b> HttpRequest for HyperRequest<'a, 'b> { + type Body = Self; + + fn multipart_boundary(&self) -> Option<&str> { + if self.method != Method::Post { + return None; + } + + self.headers.get::().and_then(|ct| { + let ContentType(ref mime) = *ct; + let params = match *mime { + Mime(TopLevel::Multipart, SubLevel::FormData, ref params) => params, + _ => return None, + }; + + params.iter().find(|&&(ref name, _)| + match *name { + Attr::Boundary => true, + _ => false, + } + ).and_then(|&(_, ref val)| + match *val { + Value::Ext(ref val) => Some(&**val), + _ => None, + } + ) + }) + } + + fn body(self) -> Self { + self + } +} + +impl<'r, 'a, 'b> HttpRequest for &'r mut HyperRequest<'a, 'b> { + type Body = Self; + + fn multipart_boundary(&self) -> Option<&str> { + if self.method != Method::Post { + return None; + } + + self.headers.get::().and_then(|ct| { + let ContentType(ref mime) = *ct; + let params = match *mime { + Mime(TopLevel::Multipart, SubLevel::FormData, ref params) => params, + _ => return None, + }; + + params.iter().find(|&&(ref name, _)| + match *name { + Attr::Boundary => true, + _ => false, + } + ).and_then(|&(_, ref val)| + match *val { + Value::Ext(ref val) => Some(&**val), + _ => None, + } + ) + }) + } + + fn body(self) -> Self::Body { + self + } +} + diff --git a/multipart/src/server/iron.rs b/multipart/src/server/iron.rs new file mode 100644 index 000000000..c4bef5712 --- /dev/null +++ b/multipart/src/server/iron.rs @@ -0,0 +1,284 @@ +//! Integration with the [Iron](https://github.com/iron/iron) framework, enabled with the `iron` feature (optional). Includes a `BeforeMiddleware` implementation. +//! +//! Not shown here: `impl `[`HttpRequest`](../trait.HttpRequest.html#implementors)` for +//! iron::Request`. + +use iron::headers::ContentType; +use iron::mime::{Mime, TopLevel, SubLevel}; +use iron::request::{Body as IronBody, Request as IronRequest}; +use iron::typemap::Key; +use iron::{BeforeMiddleware, IronError, IronResult}; + +use std::path::PathBuf; +use std::{error, fmt, io}; +use tempfile; + +use super::{FieldHeaders, HttpRequest, Multipart}; +use super::save::{Entries, PartialReason, TempDir}; +use super::save::SaveResult::*; + +impl<'r, 'a, 'b> HttpRequest for &'r mut IronRequest<'a, 'b> { + type Body = &'r mut IronBody<'a, 'b>; + + fn multipart_boundary(&self) -> Option<&str> { + let content_type = try_opt!(self.headers.get::()); + if let Mime(TopLevel::Multipart, SubLevel::FormData, _) = **content_type { + content_type.get_param("boundary").map(|b| b.as_str()) + } else { + None + } + } + + fn body(self) -> &'r mut IronBody<'a, 'b> { + &mut self.body + } +} + +/// The default file size limit for [`Intercept`](struct.Intercept.html), in bytes. +pub const DEFAULT_FILE_SIZE_LIMIT: u64 = 2 * 1024 * 1024; + +/// The default file count limit for [`Intercept`](struct.Intercept.html). +pub const DEFAULT_FILE_COUNT_LIMIT: u32 = 16; + +/// A `BeforeMiddleware` for Iron which will intercept and read-out multipart requests and store +/// the result in the request. +/// +/// Successful reads will be placed in the `extensions: TypeMap` field of `iron::Request` as an +/// [`Entries`](../struct.Entries.html) instance (as both key-type and value): +/// +/// ```no_run +/// extern crate iron; +/// extern crate multipart; +/// +/// use iron::prelude::*; +/// +/// use multipart::server::Entries; +/// use multipart::server::iron::Intercept; +/// +/// fn main() { +/// let mut chain = Chain::new(|req: &mut Request| if let Some(entries) = +/// req.extensions.get::() { +/// +/// Ok(Response::with(format!("{:?}", entries))) +/// } else { +/// Ok(Response::with("Not a multipart request")) +/// }); +/// +/// chain.link_before(Intercept::default()); +/// +/// Iron::new(chain).http("localhost:80").unwrap(); +/// } +/// ``` +/// +/// Any errors during which occur during reading will be passed on as `IronError`. +#[derive(Debug)] +pub struct Intercept { + /// The parent directory for all temporary directories created by this middleware. + /// Will be created if it doesn't exist (lazy). + /// + /// If omitted, uses the OS temporary directory. + /// + /// Default value: `None`. + pub temp_dir_path: Option, + /// The size limit of uploaded files, in bytes. + /// + /// Files which exceed this size will be rejected. + /// See the `limit_behavior` field for more info. + /// + /// Default value: [`DEFAULT_FILE_SIZE_LIMIT`](constant.default_file_size_limit.html) + pub file_size_limit: u64, + /// The limit on the number of files which will be saved from + /// the request. Requests which exceed this count will be rejected. + /// + /// Default value: [`DEFAULT_FILE_COUNT_LIMT`](constant.default_file_count_limit.html) + pub file_count_limit: u32, + /// What to do when a file count or size limit has been exceeded. + /// + /// See [`LimitBehavior`](enum.limitbehavior.html) for more info. + pub limit_behavior: LimitBehavior, +} + +impl Intercept { + /// Set the `temp_dir_path` for this middleware. + pub fn temp_dir_path>(self, path: P) -> Self { + Intercept { temp_dir_path: Some(path.into()), .. self } + } + + /// Set the `file_size_limit` for this middleware. + pub fn file_size_limit(self, limit: u64) -> Self { + Intercept { file_size_limit: limit, .. self } + } + + /// Set the `file_count_limit` for this middleware. + pub fn file_count_limit(self, limit: u32) -> Self { + Intercept { file_count_limit: limit, .. self } + } + + /// Set the `limit_behavior` for this middleware. + pub fn limit_behavior(self, behavior: LimitBehavior) -> Self { + Intercept { limit_behavior: behavior, .. self } + } + + fn read_request(&self, req: &mut IronRequest) -> IronResult> { + let multipart = match Multipart::from_request(req) { + Ok(multipart) => multipart, + Err(_) => return Ok(None), + }; + + let tempdir = self.temp_dir_path.as_ref() + .map_or_else( + || tempfile::Builder::new().prefix("multipart-iron").tempdir(), + |path| tempfile::Builder::new().prefix("multipart-iron").tempdir_in(path) + ) + .map_err(|e| io_to_iron(e, "Error opening temporary directory for request."))?; + + match self.limit_behavior { + LimitBehavior::ThrowError => self.read_request_strict(multipart, tempdir), + LimitBehavior::Continue => self.read_request_lenient(multipart, tempdir), + } + } + + fn read_request_strict(&self, mut multipart: IronMultipart, tempdir: TempDir) -> IronResult> { + match multipart.save().size_limit(self.file_size_limit) + .count_limit(self.file_count_limit) + .with_temp_dir(tempdir) { + Full(entries) => Ok(Some(entries)), + Partial(_, PartialReason::Utf8Error(_)) => unreachable!(), + Partial(_, PartialReason::IoError(err)) => Err(io_to_iron(err, "Error midway through request")), + Partial(_, PartialReason::CountLimit) => Err(FileCountLimitError(self.file_count_limit).into()), + Partial(partial, PartialReason::SizeLimit) => { + let partial = partial.partial.expect(EXPECT_PARTIAL_FILE); + Err( + FileSizeLimitError { + field: partial.source.headers, + }.into() + ) + }, + Error(err) => Err(io_to_iron(err, "Error at start of request")), + } + } + + fn read_request_lenient(&self, mut multipart: IronMultipart, tempdir: TempDir) -> IronResult> { + let mut entries = match multipart.save().size_limit(self.file_size_limit) + .count_limit(self.file_count_limit) + .with_temp_dir(tempdir) { + Full(entries) => return Ok(Some(entries)), + Partial(_, PartialReason::IoError(err)) => return Err(io_to_iron(err, "Error midway through request")), + Partial(partial, _) => partial.keep_partial(), + Error(err) => return Err(io_to_iron(err, "Error at start of request")), + }; + + loop { + entries = match multipart.save().size_limit(self.file_size_limit) + .count_limit(self.file_count_limit) + .with_entries(entries) { + Full(entries) => return Ok(Some(entries)), + Partial(_, PartialReason::IoError(err)) => return Err(io_to_iron(err, "Error midway through request")), + Partial(partial, _) => partial.keep_partial(), + Error(err) => return Err(io_to_iron(err, "Error at start of request")), + }; + } + } +} + +type IronMultipart<'r, 'a, 'b> = Multipart<&'r mut IronBody<'a, 'b>>; + +const EXPECT_PARTIAL_FILE: &str = "File size limit hit but the offending \ + file was not available; this is a bug."; + +impl Default for Intercept { + fn default() -> Self { + Intercept { + temp_dir_path: None, + file_size_limit: DEFAULT_FILE_SIZE_LIMIT, + file_count_limit: DEFAULT_FILE_COUNT_LIMIT, + limit_behavior: LimitBehavior::ThrowError, + } + } +} + +impl BeforeMiddleware for Intercept { + fn before(&self, req: &mut IronRequest) -> IronResult<()> { + self.read_request(req)? + .map(|entries| req.extensions.insert::(entries)); + + Ok(()) + } +} + +impl Key for Entries { + type Value = Self; +} + +/// The behavior of `Intercept` when a file size or count limit is exceeded. +#[derive(Clone, Copy, Debug)] +#[repr(u32)] +pub enum LimitBehavior { + /// Return an error from the middleware describing the issue. + ThrowError, + /// Ignore the limit. + /// + /// In the case of file size limits, the offending file will be truncated + /// in the result. + /// + /// In the case of file count limits, the request will be completed. + Continue, +} + +/// An error returned from `Intercept` when the size limit +/// for an individual file is exceeded. +#[derive(Debug)] +pub struct FileSizeLimitError { + /// The field where the error occurred. + pub field: FieldHeaders, +} + +impl error::Error for FileSizeLimitError { + fn description(&self) -> &str { + "file size limit reached" + } +} + +impl fmt::Display for FileSizeLimitError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.field.filename { + Some(ref filename) => write!(f, "File size limit reached for field \"{}\" (filename: \"{}\")", self.field.name, filename), + None => write!(f, "File size limit reached for field \"{}\" (no filename)", self.field.name), + } + } +} + +impl Into for FileSizeLimitError { + fn into(self) -> IronError { + let desc_str = self.to_string(); + IronError::new(self, desc_str) + } +} + +/// An error returned from `Intercept` when the file count limit +/// for a single request was exceeded. +#[derive(Debug)] +pub struct FileCountLimitError(u32); + +impl error::Error for FileCountLimitError { + fn description(&self) -> &str { + "file count limit reached" + } +} + +impl fmt::Display for FileCountLimitError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "File count limit reached for request. Limit: {}", self.0) + } +} + +impl Into for FileCountLimitError { + fn into(self) -> IronError { + let desc_string = self.to_string(); + IronError::new(self, desc_string) + } +} + +fn io_to_iron>(err: io::Error, msg: M) -> IronError { + IronError::new(err, msg.into()) +} diff --git a/multipart/src/server/mod.rs b/multipart/src/server/mod.rs new file mode 100644 index 000000000..8cd01a0b8 --- /dev/null +++ b/multipart/src/server/mod.rs @@ -0,0 +1,290 @@ +// Copyright 2016 `multipart` Crate Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +//! The server-side abstraction for multipart requests. Enabled with the `server` feature. +//! +//! Use this when you are implementing an HTTP server and want to +//! to accept, parse, and serve HTTP `multipart/form-data` requests (file uploads). +//! +//! See the `Multipart` struct for more info. + +pub extern crate buf_redux; +extern crate httparse; +extern crate twoway; + +use std::borrow::Borrow; +use std::io::prelude::*; +use std::io; + +use self::boundary::BoundaryReader; + +use self::field::PrivReadEntry; + +pub use self::field::{FieldHeaders, MultipartField, MultipartData, ReadEntry, ReadEntryResult}; + +use self::save::SaveBuilder; + +pub use self::save::{Entries, SaveResult, SavedField}; + +macro_rules! try_opt ( + ($expr:expr) => ( + match $expr { + Some(val) => val, + None => return None, + } + ); + ($expr:expr, $before_ret:expr) => ( + match $expr { + Some(val) => val, + None => { + $before_ret; + return None; + } + } + ) +); + +macro_rules! try_read_entry { + ($self_:expr; $try:expr) => ( + match $try { + Ok(res) => res, + Err(err) => return ::server::ReadEntryResult::Error($self_, err), + } + ) +} + +mod boundary; +mod field; + +#[cfg(feature = "hyper")] +pub mod hyper; + +#[cfg(feature = "iron")] +pub mod iron; + +#[cfg(feature = "tiny_http")] +pub mod tiny_http; + +#[cfg(feature = "nickel")] +pub mod nickel; + +pub mod save; + +/// The server-side implementation of `multipart/form-data` requests. +/// +/// Implements `Borrow` to allow access to the request body, if desired. +pub struct Multipart { + reader: BoundaryReader, +} + +impl Multipart<()> { + /// If the given `HttpRequest` is a multipart/form-data POST request, + /// return the request body wrapped in the multipart reader. Otherwise, + /// returns the original request. + pub fn from_request(req: R) -> Result, R> { + //FIXME: move `map` expr to `Some` arm when nonlexical borrow scopes land. + let boundary = match req.multipart_boundary().map(String::from) { + Some(boundary) => boundary, + None => return Err(req), + }; + + Ok(Multipart::with_body(req.body(), boundary)) + } +} + +impl Multipart { + /// Construct a new `Multipart` with the given body reader and boundary. + /// + /// ## Note: `boundary` + /// This will prepend the requisite `--` to the boundary string as documented in + /// [IETF RFC 1341, Section 7.2.1: "Multipart: the common syntax"][rfc1341-7.2.1]. + /// Simply pass the value of the `boundary` key from the `Content-Type` header in the + /// request (or use `Multipart::from_request()`, if supported). + /// + /// [rfc1341-7.2.1]: https://tools.ietf.org/html/rfc1341#page-30 + pub fn with_body>(body: R, boundary: Bnd) -> Self { + let boundary = boundary.into(); + + info!("Multipart::with_boundary(_, {:?})", boundary); + + Multipart { + reader: BoundaryReader::from_reader(body, boundary), + } + } + + /// Read the next entry from this multipart request, returning a struct with the field's name and + /// data. See `MultipartField` for more info. + /// + /// ## Warning: Risk of Data Loss + /// If the previously returned entry had contents of type `MultipartField::File`, + /// calling this again will discard any unread contents of that entry. + pub fn read_entry(&mut self) -> io::Result>> { + self.read_entry_mut().into_result() + } + + /// Read the next entry from this multipart request, returning a struct with the field's name and + /// data. See `MultipartField` for more info. + pub fn into_entry(self) -> ReadEntryResult { + self.read_entry() + } + + /// Call `f` for each entry in the multipart request. + /// + /// This is a substitute for Rust not supporting streaming iterators (where the return value + /// from `next()` borrows the iterator for a bound lifetime). + /// + /// Returns `Ok(())` when all fields have been read, or the first error. + pub fn foreach_entry(&mut self, mut foreach: F) -> io::Result<()> where F: FnMut(MultipartField<&mut Self>) { + loop { + match self.read_entry() { + Ok(Some(field)) => foreach(field), + Ok(None) => return Ok(()), + Err(err) => return Err(err), + } + } + } + + /// Get a builder type for saving the files in this request to the filesystem. + /// + /// See [`SaveBuilder`](save/struct.SaveBuilder.html) for more information. + pub fn save(&mut self) -> SaveBuilder<&mut Self> { + SaveBuilder::new(self) + } +} + +impl Borrow for Multipart { + fn borrow(&self) -> &R { + self.reader.borrow() + } +} + +impl PrivReadEntry for Multipart { + type Source = BoundaryReader; + + fn source_mut(&mut self) -> &mut BoundaryReader { + &mut self.reader + } + + fn set_min_buf_size(&mut self, min_buf_size: usize) { + self.reader.set_min_buf_size(min_buf_size) + } + + /// Consume the next boundary. + /// Returns `true` if a field should follow this boundary, `false` otherwise. + fn consume_boundary(&mut self) -> io::Result { + debug!("Consume boundary!"); + self.reader.consume_boundary() + } +} + +/// A server-side HTTP request that may or may not be multipart. +/// +/// May be implemented by mutable references if providing the request or body by-value is +/// undesirable. +pub trait HttpRequest { + /// The body of this request. + type Body: Read; + /// Get the boundary string of this request if it is a POST request + /// with the `Content-Type` header set to `multipart/form-data`. + /// + /// The boundary string should be supplied as an extra value of the `Content-Type` header, e.g. + /// `Content-Type: multipart/form-data; boundary={boundary}`. + fn multipart_boundary(&self) -> Option<&str>; + + /// Return the request body for reading. + fn body(self) -> Self::Body; +} + +#[test] +fn issue_104() { + ::init_log(); + + use std::io::Cursor; + + let body = "\ + POST /test.html HTTP/1.1\r\n\ + Host: example.org\r\n\ + Content-Type: multipart/form-data;boundary=\"boundary\"\r\n\r\n\ + Content-Disposition: form-data; name=\"field1\"\r\n\r\n\ + value1\r\n\ + Content-Disposition: form-data; name=\"field2\"; filename=\"example.txt\"\r\n\r\n\ + value2 "; + + let request = Cursor::new(body); + + let mut multipart = Multipart::with_body(request, "boundary"); + multipart.foreach_entry(|_field| {/* Do nothing */}).unwrap_err(); +} + +#[test] +fn issue_114() { + ::init_log(); + + fn consume_all(mut rdr: R) { + loop { + let consume = rdr.fill_buf().unwrap().len(); + if consume == 0 { return; } + rdr.consume(consume); + } + } + + use std::io::Cursor; + + let body = "\ + --------------------------c616e5fded96a3c7\r\n\ + Content-Disposition: form-data; name=\"key1\"\r\n\r\n\ + v1,\r\n\ + --------------------------c616e5fded96a3c7\r\n\ + Content-Disposition: form-data; name=\"key2\"\r\n\r\n\ + v2,\r\n\ + --------------------------c616e5fded96a3c7\r\n\ + Content-Disposition: form-data; name=\"key3\"\r\n\r\n\ + v3\r\n\ + --------------------------c616e5fded96a3c7--\r\n"; + + let request = Cursor::new(body); + let mut multipart = Multipart::with_body(request, "------------------------c616e5fded96a3c7"); + + // one error if you do nothing + multipart.foreach_entry(|_entry| { /* do nothing */}).unwrap(); + + // a different error if you skip the first field + multipart.foreach_entry(|entry| if *entry.headers.name != *"key1" { consume_all(entry.data); }) + .unwrap(); + + + multipart.foreach_entry(|_entry| () /* match entry.headers.name.as_str() { + "file" => { + let mut vec = Vec::new(); + entry.data.read_to_end(&mut vec).expect("can't read"); + // message.file = String::from_utf8(vec).ok(); + println!("key file got"); + } + + "key1" => { + let mut vec = Vec::new(); + entry.data.read_to_end(&mut vec).expect("can't read"); + // message.key1 = String::from_utf8(vec).ok(); + println!("key1 got"); + } + + "key2" => { + let mut vec = Vec::new(); + entry.data.read_to_end(&mut vec).expect("can't read"); + // message.key2 = String::from_utf8(vec).ok(); + println!("key2 got"); + } + + _ => { + // as multipart has a bug https://github.com/abonander/multipart/issues/114 + // we manually do read_to_end here + //let mut _vec = Vec::new(); + //entry.data.read_to_end(&mut _vec).expect("can't read"); + println!("key neglected"); + } + }*/) + .expect("Unable to iterate multipart?") +} diff --git a/multipart/src/server/nickel.rs b/multipart/src/server/nickel.rs new file mode 100644 index 000000000..0f725a3b2 --- /dev/null +++ b/multipart/src/server/nickel.rs @@ -0,0 +1,69 @@ +//! Support for `multipart/form-data` bodies in [Nickel](https://nickel.rs). +pub extern crate nickel; + +use self::nickel::hyper; +use self::hyper::header::ContentType; + +pub use self::nickel::Request as NickelRequest; +pub use self::nickel::hyper::server::Request as HyperRequest; + +use server::{HttpRequest, Multipart}; + +/// A wrapper for `&mut nickel::Request` which implements `multipart::server::HttpRequest`. +/// +/// Necessary because this crate cannot directly provide an impl of `HttpRequest` for +/// `&mut NickelRequest`. +pub struct Maybe<'r, 'mw: 'r, 'server: 'mw, D: 'mw>(pub &'r mut NickelRequest<'mw, 'server, D>); + +impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> HttpRequest for Maybe<'r, 'mw, 'server, D> { + type Body = &'r mut HyperRequest<'mw, 'server>; + + fn multipart_boundary(&self) -> Option<&str> { + // we can't use the impl from the `hyper` module because it might be the wrong version + let cont_type = try_opt!(self.0.origin.headers.get::()); + cont_type.get_param("boundary").map(|v| v.as_str()) + } + + fn body(self) -> Self::Body { + &mut self.0.origin + } +} + +/// Extension trait for getting the `multipart/form-data` body from `nickel::Request`. +/// +/// Implemented for `nickel::Request`. +pub trait MultipartBody<'mw, 'server> { + /// Get a multipart reader for the request body, if the request is of the right type. + fn multipart_body(&mut self) -> Option>>; +} + +impl<'mw, 'server, D: 'mw> MultipartBody<'mw, 'server> for NickelRequest<'mw, 'server, D> { + fn multipart_body(&mut self) -> Option>> { + Multipart::from_request(Maybe(self)).ok() + } +} + +impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> AsRef<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { + fn as_ref(&self) -> &&'r mut NickelRequest<'mw, 'server, D> { + &self.0 + } +} + +impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> AsMut<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { + fn as_mut(&mut self) -> &mut &'r mut NickelRequest<'mw, 'server, D> { + &mut self.0 + } +} + +impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> Into<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { + fn into(self) -> &'r mut NickelRequest<'mw, 'server, D> { + self.0 + } +} + +impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> From<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { + fn from(req: &'r mut NickelRequest<'mw, 'server, D>) -> Self { + Maybe(req) + } +} + diff --git a/multipart/src/server/save.rs b/multipart/src/server/save.rs new file mode 100644 index 000000000..fa95c9ed8 --- /dev/null +++ b/multipart/src/server/save.rs @@ -0,0 +1,1045 @@ +// Copyright 2016 `multipart` Crate Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +//! Utilities for saving request entries to the filesystem. + +pub use server::buf_redux::BufReader; + +pub use tempfile::TempDir; + +use std::collections::HashMap; +use std::io::prelude::*; +use std::fs::{self, File, OpenOptions}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::{cmp, env, io, mem, str, u32, u64}; +use tempfile; + +use server::field::{FieldHeaders, MultipartField, MultipartData, ReadEntry, ReadEntryResult}; + +use self::SaveResult::*; +use self::TextPolicy::*; +use self::PartialReason::*; + +const RANDOM_FILENAME_LEN: usize = 12; + +fn rand_filename() -> String { + ::random_alphanumeric(RANDOM_FILENAME_LEN) +} + +macro_rules! try_start ( + ($try:expr) => ( + match $try { + Ok(val) => val, + Err(e) => return Error(e), + } + ) +); + +macro_rules! try_full ( + ($try:expr) => { + match $try { + Full(full) => full, + other => return other, + } + } +); + +macro_rules! try_partial ( + ($try:expr) => { + match $try { + Full(full) => return Full(full.into()), + Partial(partial, reason) => (partial, reason), + Error(e) => return Error(e), + } + } +); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum TextPolicy { + /// Attempt to read a text field as text, falling back to binary on error + Try, + /// Attempt to read a text field as text, returning any errors + Force, + /// Don't try to read text + Ignore +} + +/// A builder for saving a file or files to the local filesystem. +/// +/// ### `OpenOptions` +/// This builder holds an instance of `std::fs::OpenOptions` which is used +/// when creating the new file(s). +/// +/// By default, the open options are set with `.write(true).create_new(true)`, +/// so if the file already exists then an error will be thrown. This is to avoid accidentally +/// overwriting files from other requests. +/// +/// If you want to modify the options used to open the save file, you can use +/// `mod_open_opts()`. +/// +/// ### File Size and Count Limits +/// You can set a size limit for individual fields with `size_limit()`, which takes either `u64` +/// or `Option`. +/// +/// You can also set the maximum number of fields to process with `count_limit()`, which +/// takes either `u32` or `Option`. This only has an effect when using +/// `SaveBuilder<[&mut] Multipart>`. +/// +/// By default, these limits are set conservatively to limit the maximum memory and disk space +/// usage of a single request. You should set `count_limit` specifically for each request endpoint +/// based on the number of fields you're expecting (exactly to that number if you're not expecting +/// duplicate fields). +/// +/// ### Memory Threshold and Text Policy +/// By default, small fields (a few kilobytes or smaller) will be read directly to memory +/// without creating a file. This behavior is controlled by the `memory_threshold()` setter. You can +/// *roughly* tune the maximum memory a single request uses by tuning +/// `count_limit * memory_threshold` +/// +/// If a field appears to contain text data (its content-type is `text/*` or it doesn't declare +/// one), `SaveBuilder` can read it to a string instead of saving the raw bytes as long as it falls +/// below the set `memory_threshold`. +/// +/// By default, the behavior is to attempt to validate the data as UTF-8, falling back to saving +/// just the bytes if the validation fails at any point. You can restore/ensure this behavior +/// with the `try_text()` modifier. +/// +/// Alternatively, you can use the `force_text()` modifier to make the save operation return +/// an error when UTF-8 decoding fails, though this only holds true while the size is below +/// `memory_threshold`. The `ignore_text()` modifier turns off UTF-8 validation altogether. +/// +/// UTF-8 validation is performed incrementally (after every `BufRead::fill_buf()` call) +/// to hopefully maximize throughput, instead of blocking while the field is read to completion +/// and performing validation over the entire result at the end. (RFC: this could be a lot of +/// unnecessary work if most fields end up being written to the filesystem, however, but this +/// can be turned off with `ignore_text()` if it fits the use-case.) +/// +/// ### Warning: Do **not** trust user input! +/// It is a serious security risk to create files or directories with paths based on user input. +/// A malicious user could craft a path which can be used to overwrite important files, such as +/// web templates, static assets, Javascript files, database files, configuration files, etc., +/// if they are writable by the server process. +/// +/// This can be mitigated somewhat by setting filesystem permissions as +/// conservatively as possible and running the server under its own user with restricted +/// permissions, but you should still not use user input directly as filesystem paths. +/// If it is truly necessary, you should sanitize user input such that it cannot cause a path to be +/// misinterpreted by the OS. Such functionality is outside the scope of this crate. +#[must_use = "nothing saved to the filesystem yet"] +pub struct SaveBuilder { + savable: S, + open_opts: OpenOptions, + size_limit: u64, + count_limit: u32, + memory_threshold: u64, + text_policy: TextPolicy, +} + +/// Common methods for whole requests as well as individual fields. +impl SaveBuilder { + /// Implementation detail but not problematic to have accessible. + #[doc(hidden)] + pub fn new(savable: S) -> SaveBuilder { + let mut open_opts = OpenOptions::new(); + open_opts.write(true).create_new(true); + + SaveBuilder { + savable, + open_opts, + // 8 MiB, on the conservative end compared to most frameworks + size_limit: 8 * 1024 * 1024, + // Arbitrary, I have no empirical data for this + count_limit: 256, + // 10KiB, used by Apache Commons + // https://commons.apache.org/proper/commons-fileupload/apidocs/org/apache/commons/fileupload/disk/DiskFileItemFactory.html + memory_threshold: 10 * 1024, + text_policy: TextPolicy::Try, + } + } + + /// Set the maximum number of bytes to write out *per file*. + /// + /// Can be `u64` or `Option`. If `None` or `u64::MAX`, clears the limit. + pub fn size_limit>>(mut self, limit: L) -> Self { + self.size_limit = limit.into().unwrap_or(u64::MAX); + self + } + + /// Modify the `OpenOptions` used to open any files for writing. + /// + /// The `write` flag will be reset to `true` after the closure returns. (It'd be pretty + /// pointless otherwise, right?) + pub fn mod_open_opts(mut self, opts_fn: F) -> Self { + opts_fn(&mut self.open_opts); + self.open_opts.write(true); + self + } + + /// Set the threshold at which to switch from copying a field into memory to copying + /// it to disk. + /// + /// If `0`, forces fields to save directly to the filesystem. + /// If `u64::MAX`, effectively forces fields to always save to memory. + pub fn memory_threshold(self, memory_threshold: u64) -> Self { + Self { memory_threshold, ..self } + } + + /// When encountering a field that is apparently text, try to read it to a string or fall + /// back to binary otherwise. + /// + /// If set for an individual field (`SaveBuilder<&mut MultipartData<_>>`), will + /// always attempt to decode text regardless of the field's `Content-Type`. + /// + /// Has no effect once `memory_threshold` has been reached. + pub fn try_text(self) -> Self { + Self { text_policy: TextPolicy::Try, ..self } + } + + /// When encountering a field that is apparently text, read it to a string or return an error. + /// + /// If set for an individual field (`SaveBuilder<&mut MultipartData<_>>`), will + /// always attempt to decode text regardless of the field's `Content-Type`. + /// + /// (RFC: should this continue to validate UTF-8 when writing to the filesystem?) + pub fn force_text(self) -> Self { + Self { text_policy: TextPolicy::Force, ..self} + } + + /// Don't try to read or validate any field data as UTF-8. + pub fn ignore_text(self) -> Self { + Self { text_policy: TextPolicy::Ignore, ..self } + } +} + +/// Save API for whole multipart requests. +impl SaveBuilder where M: ReadEntry { + /// Set the maximum number of fields to process. + /// + /// Can be `u32` or `Option`. If `None` or `u32::MAX`, clears the limit. + pub fn count_limit>>(mut self, count_limit: L) -> Self { + self.count_limit = count_limit.into().unwrap_or(u32::MAX); + self + } + + /// Save all fields in the request using a new temporary directory prefixed with + /// `multipart-rs` in the OS temporary directory. + /// + /// For more options, create a `TempDir` yourself and pass it to `with_temp_dir()` instead. + /// + /// See `with_entries()` for more info. + /// + /// ### Note: Temporary + /// See `SaveDir` for more info (the type of `Entries::save_dir`). + pub fn temp(self) -> EntriesSaveResult { + self.temp_with_prefix("multipart-rs") + } + + /// Save all fields in the request using a new temporary directory with the given string + /// as a prefix in the OS temporary directory. + /// + /// For more options, create a `TempDir` yourself and pass it to `with_temp_dir()` instead. + /// + /// See `with_entries()` for more info. + /// + /// ### Note: Temporary + /// See `SaveDir` for more info (the type of `Entries::save_dir`). + pub fn temp_with_prefix(self, prefix: &str) -> EntriesSaveResult { + match tempfile::Builder::new().prefix(prefix).tempdir() { + Ok(tempdir) => self.with_temp_dir(tempdir), + Err(e) => SaveResult::Error(e), + } + } + + /// Save all fields in the request using the given `TempDir`. + /// + /// See `with_entries()` for more info. + /// + /// The `TempDir` is returned in the result under `Entries::save_dir`. + pub fn with_temp_dir(self, tempdir: TempDir) -> EntriesSaveResult { + self.with_entries(Entries::new(SaveDir::Temp(tempdir))) + } + + /// Save the file fields in the request to a new permanent directory with the given path. + /// + /// Any nonexistent directories in the path will be created. + /// + /// See `with_entries()` for more info. + pub fn with_dir>(self, dir: P) -> EntriesSaveResult { + let dir = dir.into(); + + try_start!(create_dir_all(&dir)); + + self.with_entries(Entries::new(SaveDir::Perm(dir))) + } + + /// Commence the save operation using the existing `Entries` instance. + /// + /// May be used to resume a saving operation after handling an error. + /// + /// If `count_limit` is set, only reads that many fields before returning an error. + /// If you wish to resume from `PartialReason::CountLimit`, simply remove some entries. + /// + /// Note that `PartialReason::CountLimit` will still be returned if the number of fields + /// reaches `u32::MAX`, but this would be an extremely degenerate case. + pub fn with_entries(self, mut entries: Entries) -> EntriesSaveResult { + let SaveBuilder { + savable, open_opts, count_limit, size_limit, + memory_threshold, text_policy + } = self; + + let mut res = ReadEntry::read_entry(savable); + + let _ = entries.recount_fields(); + + let save_field = |field: &mut MultipartField, entries: &Entries| { + let text_policy = if field.is_text() { text_policy } else { Ignore }; + + let mut saver = SaveBuilder { + savable: &mut field.data, open_opts: open_opts.clone(), + count_limit, size_limit, memory_threshold, text_policy + }; + + saver.with_dir(entries.save_dir.as_path()) + }; + + while entries.fields_count < count_limit { + let mut field: MultipartField = match res { + ReadEntryResult::Entry(field) => field, + ReadEntryResult::End(_) => return Full(entries), // normal exit point + ReadEntryResult::Error(_, e) => return Partial ( + PartialEntries { + entries, + partial: None, + }, + e.into(), + ) + }; + + let (dest, reason) = match save_field(&mut field, &entries) { + Full(saved) => { + entries.push_field(field.headers, saved); + res = ReadEntry::read_entry(field.data.into_inner()); + continue; + }, + Partial(saved, reason) => (Some(saved), reason), + Error(error) => (None, PartialReason::IoError(error)), + }; + + return Partial( + PartialEntries { + entries, + partial: Some(PartialSavedField { + source: field, + dest, + }), + }, + reason + ); + } + + Partial( + PartialEntries { + entries, + partial: None, + }, + PartialReason::CountLimit + ) + } +} + +/// Save API for individual fields. +impl<'m, M: 'm> SaveBuilder<&'m mut MultipartData> where MultipartData: BufRead { + /// Save the field data, potentially using a file with a random name in the + /// OS temporary directory. + /// + /// See `with_path()` for more details. + pub fn temp(&mut self) -> FieldSaveResult { + let path = env::temp_dir().join(rand_filename()); + self.with_path(path) + } + + /// Save the field data, potentially using a file with the given name in + /// the OS temporary directory. + /// + /// See `with_path()` for more details. + pub fn with_filename(&mut self, filename: &str) -> FieldSaveResult { + let mut tempdir = env::temp_dir(); + tempdir.set_file_name(filename); + + self.with_path(tempdir) + } + + /// Save the field data, potentially using a file with a random alphanumeric name + /// in the given directory. + /// + /// See `with_path()` for more details. + pub fn with_dir>(&mut self, dir: P) -> FieldSaveResult { + let path = dir.as_ref().join(rand_filename()); + self.with_path(path) + } + + /// Save the field data, potentially using a file with the given path. + /// + /// Creates any missing directories in the path (RFC: skip this step?). + /// Uses the contained `OpenOptions` to create the file. + /// Truncates the file to the given `size_limit`, if set. + /// + /// The no directories or files will be created until the set `memory_threshold` is reached. + /// If `size_limit` is set and less than or equal to `memory_threshold`, + /// then the disk will never be touched. + pub fn with_path>(&mut self, path: P) -> FieldSaveResult { + let bytes = if self.text_policy != Ignore { + let (text, reason) = try_partial!(self.save_text()); + match reason { + SizeLimit if !self.cmp_size_limit(text.len()) => text.into_bytes(), + Utf8Error(_) if self.text_policy != Force => text.into_bytes(), + other => return Partial(text.into(), other), + } + } else { + Vec::new() + }; + + let (bytes, reason) = try_partial!(self.save_mem(bytes)); + + match reason { + SizeLimit if !self.cmp_size_limit(bytes.len()) => (), + other => return Partial(bytes.into(), other) + } + + let path = path.into(); + + let mut file = match create_dir_all(&path).and_then(|_| self.open_opts.open(&path)) { + Ok(file) => file, + Err(e) => return Error(e), + }; + + let data = try_full!( + try_write_all(&bytes, &mut file) + .map(move |size| SavedData::File(path, size as u64)) + ); + + self.write_to(file).map(move |written| data.add_size(written)) + } + + + /// Write out the field data to `dest`, truncating if a limit was set. + /// + /// Returns the number of bytes copied, and whether or not the limit was reached + /// (tested by `MultipartFile::fill_buf().is_empty()` so no bytes are consumed). + /// + /// Retries on interrupts. + pub fn write_to(&mut self, mut dest: W) -> SaveResult { + if self.size_limit < u64::MAX { + try_copy_limited(&mut self.savable, |buf| try_write_all(buf, &mut dest), self.size_limit) + } else { + try_read_buf(&mut self.savable, |buf| try_write_all(buf, &mut dest)) + } + } + + fn save_mem(&mut self, mut bytes: Vec) -> SaveResult, Vec> { + let pre_read = bytes.len() as u64; + match self.read_mem(|buf| { bytes.extend_from_slice(buf); Full(buf.len()) }, pre_read) { + Full(_) => Full(bytes), + Partial(_, reason) => Partial(bytes, reason), + Error(e) => if !bytes.is_empty() { Partial(bytes, e.into()) } + else { Error(e) } + } + + } + + fn save_text(&mut self) -> SaveResult { + let mut string = String::new(); + + // incrementally validate UTF-8 to do as much work as possible during network activity + let res = self.read_mem(|buf| { + match str::from_utf8(buf) { + Ok(s) => { string.push_str(s); Full(buf.len()) }, + // buffer should always be bigger + Err(e) => if buf.len() < 4 { + Partial(0, e.into()) + } else { + string.push_str(str::from_utf8(&buf[..e.valid_up_to()]).unwrap()); + Full(e.valid_up_to()) + } + } + }, 0); + + match res { + Full(_) => Full(string), + Partial(_, reason) => Partial(string, reason), + Error(e) => Error(e), + } + } + + fn read_mem SaveResult>(&mut self, with_buf: Wb, pre_read: u64) -> SaveResult { + let limit = cmp::min(self.size_limit, self.memory_threshold) + .saturating_sub(pre_read); + try_copy_limited(&mut self.savable, with_buf, limit) + } + + fn cmp_size_limit(&self, size: usize) -> bool { + size as u64 >= self.size_limit + } +} + +/// A field that has been saved (to memory or disk) from a multipart request. +#[derive(Debug)] +pub struct SavedField { + /// The headers of the field that was saved. + pub headers: FieldHeaders, + /// The data of the field which may reside in memory or on disk. + pub data: SavedData, +} + +/// A saved field's data container (in memory or on disk) +#[derive(Debug)] +pub enum SavedData { + /// Validated UTF-8 text data. + Text(String), + /// Binary data. + Bytes(Vec), + /// A path to a file on the filesystem and its size as written by `multipart`. + File(PathBuf, u64), +} + +impl SavedData { + /// Get an adapter for this data which implements `Read`. + /// + /// If the data is in a file, the file is opened in read-only mode. + pub fn readable(&self) -> io::Result { + use self::SavedData::*; + + match *self { + Text(ref text) => Ok(DataReader::Bytes(text.as_ref())), + Bytes(ref bytes) => Ok(DataReader::Bytes(bytes)), + File(ref path, _) => Ok(DataReader::File(BufReader::new(fs::File::open(path)?))), + } + } + + /// Get the size of the data, in memory or on disk. + /// + /// #### Note + /// The size on disk may not match the size of the file if it is externally modified. + pub fn size(&self) -> u64 { + use self::SavedData::*; + + match *self { + Text(ref text) => text.len() as u64, + Bytes(ref bytes) => bytes.len() as u64, + File(_, size) => size, + } + } + + /// Returns `true` if the data is known to be in memory (`Text | Bytes`) + pub fn is_memory(&self) -> bool { + use self::SavedData::*; + + match *self { + Text(_) | Bytes(_) => true, + File(_, _) => false, + } + } + + fn add_size(self, add: u64) -> Self { + use self::SavedData::File; + + match self { + File(path, size) => File(path, size.saturating_add(add)), + other => other + } + } +} + +impl From for SavedData { + fn from(s: String) -> Self { + SavedData::Text(s) + } +} + +impl From> for SavedData { + fn from(b: Vec) -> Self { + SavedData::Bytes(b) + } +} + +/// A `Read` (and `BufRead`) adapter for `SavedData` +pub enum DataReader<'a> { + /// In-memory data source (`SavedData::Bytes | Text`) + Bytes(&'a [u8]), + /// On-disk data source (`SavedData::File`) + File(BufReader), +} + +impl<'a> Read for DataReader<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + use self::DataReader::*; + + match *self { + Bytes(ref mut bytes) => bytes.read(buf), + File(ref mut file) => file.read(buf), + } + } +} + +impl<'a> BufRead for DataReader<'a> { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + use self::DataReader::*; + + match *self { + Bytes(ref mut bytes) => bytes.fill_buf(), + File(ref mut file) => file.fill_buf(), + } + } + + fn consume(&mut self, amt: usize) { + use self::DataReader::*; + + match *self { + Bytes(ref mut bytes) => bytes.consume(amt), + File(ref mut file) => file.consume(amt), + } + } +} + +/// A result of `Multipart::save()`. +#[derive(Debug)] +pub struct Entries { + /// The fields of the multipart request, mapped by field name -> value. + /// + /// A field name may have multiple actual fields associated with it, but the most + /// common case is a single field. + /// + /// Each vector is guaranteed not to be empty unless externally modified. + // Even though individual fields might only have one entry, it's better to limit the + // size of a value type in `HashMap` to improve cache efficiency in lookups. + pub fields: HashMap, Vec>, + /// The directory that the entries in `fields` were saved into. + pub save_dir: SaveDir, + fields_count: u32, +} + +impl Entries { + /// Create a new `Entries` with the given `SaveDir` + pub fn new(save_dir: SaveDir) -> Self { + Entries { + fields: HashMap::new(), + save_dir, + fields_count: 0, + } + } + + /// Returns `true` if `fields` is empty, `false` otherwise. + pub fn is_empty(&self) -> bool { + self.fields.is_empty() + } + + /// The number of actual fields contained within this `Entries`. + /// + /// Effectively `self.fields.values().map(Vec::len).sum()` but maintained separately. + /// + /// ## Note + /// This will be incorrect if `fields` is modified externally. Call `recount_fields()` + /// to get the correct count. + pub fn fields_count(&self) -> u32 { + self.fields_count + } + + /// Sum the number of fields in this `Entries` and then return the updated value. + pub fn recount_fields(&mut self) -> u32 { + let fields_count = self.fields.values().map(Vec::len).sum(); + // saturating cast + self.fields_count = cmp::min(u32::MAX as usize, fields_count) as u32; + self.fields_count + } + + fn push_field(&mut self, mut headers: FieldHeaders, data: SavedData) { + use std::collections::hash_map::Entry::*; + + match self.fields.entry(headers.name.clone()) { + Vacant(vacant) => { vacant.insert(vec![SavedField { headers, data }]); }, + Occupied(occupied) => { + // dedup the field name by reusing the key's `Arc` + headers.name = occupied.key().clone(); + occupied.into_mut().push({ SavedField { headers, data }}); + }, + } + + self.fields_count = self.fields_count.saturating_add(1); + } + + /// Print all fields and their contents to stdout. Mostly for testing purposes. + pub fn print_debug(&self) -> io::Result<()> { + let stdout = io::stdout(); + let stdout_lock = stdout.lock(); + self.write_debug(stdout_lock) + } + + /// Write all fields and their contents to the given output. Mostly for testing purposes. + pub fn write_debug(&self, mut writer: W) -> io::Result<()> { + for (name, entries) in &self.fields { + writeln!(writer, "Field {:?} has {} entries:", name, entries.len())?; + + for (idx, field) in entries.iter().enumerate() { + let mut data = field.data.readable()?; + let headers = &field.headers; + writeln!(writer, "{}: {:?} ({:?}):", idx, headers.filename, headers.content_type)?; + io::copy(&mut data, &mut writer)?; + } + } + + Ok(()) + } +} + +/// The save directory for `Entries`. May be temporary (delete-on-drop) or permanent. +#[derive(Debug)] +pub enum SaveDir { + /// This directory is temporary and will be deleted, along with its contents, when this wrapper + /// is dropped. + Temp(TempDir), + /// This directory is permanent and will be left on the filesystem when this wrapper is dropped. + /// + /// **N.B.** If this directory is in the OS temporary directory then it may still be + /// deleted at any time. + Perm(PathBuf), +} + +impl SaveDir { + /// Get the path of this directory, either temporary or permanent. + pub fn as_path(&self) -> &Path { + use self::SaveDir::*; + match *self { + Temp(ref tempdir) => tempdir.path(), + Perm(ref pathbuf) => &*pathbuf, + } + } + + /// Returns `true` if this is a temporary directory which will be deleted on-drop. + pub fn is_temporary(&self) -> bool { + use self::SaveDir::*; + match *self { + Temp(_) => true, + Perm(_) => false, + } + } + + /// Unwrap the `PathBuf` from `self`; if this is a temporary directory, + /// it will be converted to a permanent one. + pub fn into_path(self) -> PathBuf { + use self::SaveDir::*; + + match self { + Temp(tempdir) => tempdir.into_path(), + Perm(pathbuf) => pathbuf, + } + } + + /// If this `SaveDir` is temporary, convert it to permanent. + /// This is a no-op if it already is permanent. + /// + /// ### Warning: Potential Data Loss + /// Even though this will prevent deletion on-drop, the temporary folder on most OSes + /// (where this directory is created by default) can be automatically cleared by the OS at any + /// time, usually on reboot or when free space is low. + /// + /// It is recommended that you relocate the files from a request which you want to keep to a + /// permanent folder on the filesystem. + pub fn keep(&mut self) { + use self::SaveDir::*; + *self = match mem::replace(self, Perm(PathBuf::new())) { + Temp(tempdir) => Perm(tempdir.into_path()), + old_self => old_self, + }; + } + + /// Delete this directory and its contents, regardless of its permanence. + /// + /// ### Warning: Potential Data Loss + /// This is very likely irreversible, depending on the OS implementation. + /// + /// Files deleted programmatically are deleted directly from disk, as compared to most file + /// manager applications which use a staging area from which deleted files can be safely + /// recovered (i.e. Windows' Recycle Bin, OS X's Trash Can, etc.). + pub fn delete(self) -> io::Result<()> { + use self::SaveDir::*; + match self { + Temp(tempdir) => tempdir.close(), + Perm(pathbuf) => fs::remove_dir_all(&pathbuf), + } + } +} + +impl AsRef for SaveDir { + fn as_ref(&self) -> &Path { + self.as_path() + } +} + +/// The reason the save operation quit partway through. +#[derive(Debug)] +pub enum PartialReason { + /// The count limit for files in the request was hit. + /// + /// The associated file has not been saved to the filesystem. + CountLimit, + /// The size limit for an individual file was hit. + /// + /// The file was partially written to the filesystem. + SizeLimit, + /// An error occurred during the operation. + IoError(io::Error), + /// An error returned from validating a field as UTF-8 due to `SaveBuilder::force_text()` + Utf8Error(str::Utf8Error), +} + +impl From for PartialReason { + fn from(e: io::Error) -> Self { + IoError(e) + } +} + +impl From for PartialReason { + fn from(e: str::Utf8Error) -> Self { + Utf8Error(e) + } +} + +impl PartialReason { + /// Return `io::Error` in the `IoError` case or panic otherwise. + pub fn unwrap_err(self) -> io::Error { + self.expect_err("`PartialReason` was not `IoError`") + } + + /// Return `io::Error` in the `IoError` case or panic with the given + /// message otherwise. + pub fn expect_err(self, msg: &str) -> io::Error { + match self { + PartialReason::IoError(e) => e, + _ => panic!("{}: {:?}", msg, self), + } + } +} + +/// The field that was being read when the save operation quit. +/// +/// May be partially saved to the filesystem if `dest` is `Some`. +#[derive(Debug)] +pub struct PartialSavedField { + /// The field that was being read. + /// + /// May be partially read if `dest` is `Some`. + pub source: MultipartField, + /// The data from the saving operation, if it got that far. + pub dest: Option, +} + +/// The partial result type for `Multipart::save*()`. +/// +/// Contains the successfully saved entries as well as the partially +/// saved file that was in the process of being read when the error occurred, +/// if applicable. +#[derive(Debug)] +pub struct PartialEntries { + /// The entries that were saved successfully. + pub entries: Entries, + /// The field that was in the process of being read. `None` if the error + /// occurred between entries. + pub partial: Option>, +} + +/// Discards `partial` +impl Into for PartialEntries { + fn into(self) -> Entries { + self.entries + } +} + +impl PartialEntries { + /// If `partial` is present and contains a `SavedFile` then just + /// add it to the `Entries` instance and return it. + /// + /// Otherwise, returns `self.entries` + pub fn keep_partial(mut self) -> Entries { + if let Some(partial) = self.partial { + if let Some(saved) = partial.dest { + self.entries.push_field(partial.source.headers, saved); + } + } + + self.entries + } +} + +/// The ternary result type used for the `SaveBuilder<_>` API. +#[derive(Debug)] +pub enum SaveResult { + /// The operation was a total success. Contained is the complete result. + Full(Success), + /// The operation quit partway through. Included is the partial + /// result along with the reason. + Partial(Partial, PartialReason), + /// An error occurred at the start of the operation, before anything was done. + Error(io::Error), +} + +/// Shorthand result for methods that return `Entries` +pub type EntriesSaveResult = SaveResult>; + +/// Shorthand result for methods that return `FieldData`s. +/// +/// The `MultipartData` is not provided here because it is not necessary to return +/// a borrow when the owned version is probably in the same scope. This hopefully +/// saves some headache with the borrow-checker. +pub type FieldSaveResult = SaveResult; + +impl EntriesSaveResult { + /// Take the `Entries` from `self`, if applicable, and discarding + /// the error, if any. + pub fn into_entries(self) -> Option { + match self { + Full(entries) | Partial(PartialEntries { entries, .. }, _) => Some(entries), + Error(_) => None, + } + } +} + +impl SaveResult where P: Into { + /// Convert `self` to `Option`; there may still have been an error. + pub fn okish(self) -> Option { + self.into_opt_both().0 + } + + /// Map the `Full` or `Partial` values to a new type, retaining the reason + /// in the `Partial` case. + pub fn map(self, map: Map) -> SaveResult where Map: FnOnce(S) -> T { + match self { + Full(full) => Full(map(full)), + Partial(partial, reason) => Partial(map(partial.into()), reason), + Error(e) => Error(e), + } + } + + /// Decompose `self` to `(Option, Option)` + pub fn into_opt_both(self) -> (Option, Option) { + match self { + Full(full) => (Some(full), None), + Partial(partial, IoError(e)) => (Some(partial.into()), Some(e)), + Partial(partial, _) => (Some(partial.into()), None), + Error(error) => (None, Some(error)), + } + } + + /// Map `self` to an `io::Result`, discarding the error in the `Partial` case. + pub fn into_result(self) -> io::Result { + match self { + Full(entries) => Ok(entries), + Partial(partial, _) => Ok(partial.into()), + Error(error) => Err(error), + } + } + + /// Pessimistic version of `into_result()` which will return an error even + /// for the `Partial` case. + /// + /// ### Note: Possible Storage Leak + /// It's generally not a good idea to ignore the `Partial` case, as there may still be a + /// partially written file on-disk. If you're not using a temporary directory + /// (OS-managed or via `TempDir`) then partially written files will remain on-disk until + /// explicitly removed which could result in excessive disk usage if not monitored closely. + pub fn into_result_strict(self) -> io::Result { + match self { + Full(entries) => Ok(entries), + Partial(_, PartialReason::IoError(e)) | Error(e) => Err(e), + Partial(partial, _) => Ok(partial.into()), + } + } +} + +fn create_dir_all(path: &Path) -> io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + } else { + // RFC: return an error instead? + warn!("Attempting to save file in what looks like a root directory. File path: {:?}", path); + Ok(()) + } +} + +fn try_copy_limited SaveResult>(src: R, mut with_buf: Wb, limit: u64) -> SaveResult { + let mut copied = 0u64; + try_read_buf(src, |buf| { + let new_copied = copied.saturating_add(buf.len() as u64); + if new_copied > limit { return Partial(0, PartialReason::SizeLimit) } + copied = new_copied; + + with_buf(buf) + }) +} + +fn try_read_buf SaveResult>(mut src: R, mut with_buf: Wb) -> SaveResult { + let mut total_copied = 0u64; + + macro_rules! try_here ( + ($try:expr) => ( + match $try { + Ok(val) => val, + Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue, + Err(e) => return if total_copied == 0 { Error(e) } + else { Partial(total_copied, e.into()) }, + } + ) + ); + + loop { + let res = { + let buf = try_here!(src.fill_buf()); + if buf.is_empty() { break; } + with_buf(buf) + }; + + match res { + Full(copied) => { src.consume(copied); total_copied += copied as u64; } + Partial(copied, reason) => { + src.consume(copied); total_copied += copied as u64; + return Partial(total_copied, reason); + }, + Error(err) => { + return Partial(total_copied, err.into()); + } + } + } + + Full(total_copied) +} + +fn try_write_all(mut buf: &[u8], mut dest: W) -> SaveResult { + let mut total_copied = 0; + + macro_rules! try_here ( + ($try:expr) => ( + match $try { + Ok(val) => val, + Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue, + Err(e) => return if total_copied == 0 { Error(e) } + else { Partial(total_copied, e.into()) }, + } + ) + ); + + while !buf.is_empty() { + match try_here!(dest.write(buf)) { + 0 => try_here!(Err(io::Error::new(io::ErrorKind::WriteZero, + "failed to write whole buffer"))), + copied => { + buf = &buf[copied..]; + total_copied += copied; + }, + } + } + + Full(total_copied) +} diff --git a/multipart/src/server/tiny_http.rs b/multipart/src/server/tiny_http.rs new file mode 100644 index 000000000..ae246cf21 --- /dev/null +++ b/multipart/src/server/tiny_http.rs @@ -0,0 +1,36 @@ +//! Integration with [`tiny_http`](https://github.com/frewsxcv/tiny-http) with the `tiny_http` +//! feature (optional). +//! +//! Contains `impl `[`HttpRequest`](../trait.HttpRequest.html)` for tiny_http::Request` (not shown +//! here; see [`HttpRequest`'s implementors](../trait.HttpRequest.html#implementors)). + +pub use tiny_http::Request as TinyHttpRequest; + +use super::HttpRequest; + +use std::io::Read; + +impl<'r> HttpRequest for &'r mut TinyHttpRequest { + type Body = &'r mut dyn Read; + + fn multipart_boundary(&self) -> Option<&str> { + const BOUNDARY: &str = "boundary="; + + let content_type = try_opt!(self + .headers() + .iter() + .find(|header| header.field.equiv("Content-Type"))) + .value + .as_str(); + let start = try_opt!(content_type.find(BOUNDARY)) + BOUNDARY.len(); + let end = content_type[start..] + .find(';') + .map_or(content_type.len(), |end| start + end); + + Some(&content_type[start..end]) + } + + fn body(self) -> Self::Body { + self.as_reader() + } +} From ab694afc4157b14238f6d11bea4563089907e5b3 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 14 Jun 2025 00:29:06 +0000 Subject: [PATCH 2/7] Resolve FCWs in multipart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply Ondřej's patch from [1] to resolve future compatibility warnings in multipart, then update it further to resolve FCWs in all default features (the patch only resolved issues with the features that Rouille uses). Co-authored-by: Ondřej Hruška [1]: https://github.com/tomaka/rouille/issues/271 --- multipart/Cargo.toml | 4 ++++ multipart/src/client/lazy.rs | 3 ++- multipart/src/client/mod.rs | 2 +- multipart/src/client/sized.rs | 2 +- multipart/src/mock.rs | 8 +++++--- multipart/src/server/field.rs | 17 +++++++---------- multipart/src/server/mod.rs | 2 +- multipart/src/server/nickel.rs | 2 +- multipart/src/server/save.rs | 10 +++++----- 9 files changed, 27 insertions(+), 23 deletions(-) diff --git a/multipart/Cargo.toml b/multipart/Cargo.toml index 7516948cf..92d488a59 100644 --- a/multipart/Cargo.toml +++ b/multipart/Cargo.toml @@ -16,6 +16,8 @@ documentation = "http://docs.rs/multipart/" license = "MIT OR Apache-2.0" readme = "README.md" +autobins = false +edition = "2021" [dependencies] lazy_static = { version = "1.2.0", optional = true } @@ -28,7 +30,9 @@ tempfile = "3" clippy = { version = ">=0.0, <0.1", optional = true} #Server Dependencies + buf_redux = { version = "0.8", optional = true, default-features = false } + httparse = { version = "1.2", optional = true } twoway = { version = "0.1", optional = true } quick-error = { version = "1.2", optional = true } diff --git a/multipart/src/client/lazy.rs b/multipart/src/client/lazy.rs index 4df40cfe5..d19bb242a 100644 --- a/multipart/src/client/lazy.rs +++ b/multipart/src/client/lazy.rs @@ -486,6 +486,7 @@ fn cursor_at_end>(cursor: &Cursor) -> bool { mod hyper { use hyper::client::{Body, Client, IntoUrl, RequestBuilder, Response}; use hyper::Result as HyperResult; + use crate::client; impl<'n, 'd> super::Multipart<'n, 'd> { /// #### Feature: `hyper` @@ -522,7 +523,7 @@ mod hyper { }; mut_fn(client.post(url)) - .header(::client::hyper::content_type(fields.boundary())) + .header(client::hyper::content_type(fields.boundary())) .body(fields.to_body()) .send() } diff --git a/multipart/src/client/mod.rs b/multipart/src/client/mod.rs index 3158d2a7d..b1ba9391f 100644 --- a/multipart/src/client/mod.rs +++ b/multipart/src/client/mod.rs @@ -207,7 +207,7 @@ impl HttpStream for io::Sink { } fn gen_boundary() -> String { - ::random_alphanumeric(BOUNDARY_LEN) + crate::random_alphanumeric(BOUNDARY_LEN) } fn open_stream( diff --git a/multipart/src/client/sized.rs b/multipart/src/client/sized.rs index 75a3945c0..874ad2253 100644 --- a/multipart/src/client/sized.rs +++ b/multipart/src/client/sized.rs @@ -6,7 +6,7 @@ // copied, modified, or distributed except according to those terms. //! Sized/buffered wrapper around `HttpRequest`. -use client::{HttpRequest, HttpStream}; +use super::{HttpRequest, HttpStream}; use std::io; use std::io::prelude::*; diff --git a/multipart/src/mock.rs b/multipart/src/mock.rs index 2947661bd..c6f9d4ee8 100644 --- a/multipart/src/mock.rs +++ b/multipart/src/mock.rs @@ -11,6 +11,8 @@ use std::fmt; use rand::{self, Rng}; use rand::prelude::ThreadRng; +use crate::{client, server}; + /// A mock implementation of `client::HttpRequest` which can spawn an `HttpBuffer`. /// /// `client::HttpRequest` impl requires the `client` feature. @@ -21,7 +23,7 @@ pub struct ClientRequest { } #[cfg(feature = "client")] -impl ::client::HttpRequest for ClientRequest { +impl client::HttpRequest for ClientRequest { type Stream = HttpBuffer; type Error = io::Error; @@ -103,7 +105,7 @@ impl Write for HttpBuffer { } #[cfg(feature = "client")] -impl ::client::HttpStream for HttpBuffer { +impl client::HttpStream for HttpBuffer { type Request = ClientRequest; type Response = HttpBuffer; type Error = io::Error; @@ -165,7 +167,7 @@ impl<'a> Read for ServerRequest<'a> { } #[cfg(feature = "server")] -impl<'a> ::server::HttpRequest for ServerRequest<'a> { +impl<'a> server::HttpRequest for ServerRequest<'a> { type Body = Self; fn multipart_boundary(&self) -> Option<&str> { Some(self.boundary) } diff --git a/multipart/src/server/field.rs b/multipart/src/server/field.rs index 6ccf9525b..1a6849819 100644 --- a/multipart/src/server/field.rs +++ b/multipart/src/server/field.rs @@ -8,7 +8,6 @@ //! `multipart` field header parsing. use mime::Mime; -use std::error::Error; use std::io::{self, BufRead, Read}; use std::{fmt, str}; @@ -27,7 +26,7 @@ macro_rules! invalid_cont_disp { return Err(ParseHeaderError::InvalidContDisp( $reason, $cause.to_string(), - )); + )) }; } @@ -531,30 +530,28 @@ impl ReadEntryResult { } } -const GENERIC_PARSE_ERR: &str = "an error occurred while parsing field headers"; - quick_error! { #[derive(Debug)] enum ParseHeaderError { /// The `Content-Disposition` header was not found MissingContentDisposition(headers: String) { - display(x) -> ("{}:\n{}", x.description(), headers) + display(x) -> ("{}:\n{}", x, headers) description("\"Content-Disposition\" header not found in field headers") } InvalidContDisp(reason: &'static str, cause: String) { - display(x) -> ("{}: {}: {}", x.description(), reason, cause) + display(x) -> ("{}: {}: {}", x, reason, cause) description("invalid \"Content-Disposition\" header") } /// The header was found but could not be parsed TokenizeError(err: HttparseError) { - description(GENERIC_PARSE_ERR) - display(x) -> ("{}: {}", x.description(), err) + description("an error occurred while parsing field headers") + display(x) -> ("{}: {}", x, err) cause(err) from() } MimeError(cont_type: String) { description("Failed to parse Content-Type") - display(this) -> ("{}: {}", this.description(), cont_type) + display(this) -> ("{}: {}", this, cont_type) } TooLarge { description("field headers section ridiculously long or missing trailing CRLF-CRLF") @@ -562,7 +559,7 @@ quick_error! { /// IO error Io(err: io::Error) { description("an io error occurred while parsing the headers") - display(x) -> ("{}: {}", x.description(), err) + display(x) -> ("{}: {}", x, err) cause(err) from() } diff --git a/multipart/src/server/mod.rs b/multipart/src/server/mod.rs index 8cd01a0b8..7d8931d29 100644 --- a/multipart/src/server/mod.rs +++ b/multipart/src/server/mod.rs @@ -51,7 +51,7 @@ macro_rules! try_read_entry { ($self_:expr; $try:expr) => ( match $try { Ok(res) => res, - Err(err) => return ::server::ReadEntryResult::Error($self_, err), + Err(err) => return crate::server::ReadEntryResult::Error($self_, err), } ) } diff --git a/multipart/src/server/nickel.rs b/multipart/src/server/nickel.rs index 0f725a3b2..1cec1e1eb 100644 --- a/multipart/src/server/nickel.rs +++ b/multipart/src/server/nickel.rs @@ -7,7 +7,7 @@ use self::hyper::header::ContentType; pub use self::nickel::Request as NickelRequest; pub use self::nickel::hyper::server::Request as HyperRequest; -use server::{HttpRequest, Multipart}; +use crate::server::{HttpRequest, Multipart}; /// A wrapper for `&mut nickel::Request` which implements `multipart::server::HttpRequest`. /// diff --git a/multipart/src/server/save.rs b/multipart/src/server/save.rs index fa95c9ed8..372ff4384 100644 --- a/multipart/src/server/save.rs +++ b/multipart/src/server/save.rs @@ -6,9 +6,9 @@ // copied, modified, or distributed except according to those terms. //! Utilities for saving request entries to the filesystem. -pub use server::buf_redux::BufReader; +pub use crate::server::buf_redux::BufReader; -pub use tempfile::TempDir; +pub use crate::tempfile::TempDir; use std::collections::HashMap; use std::io::prelude::*; @@ -18,7 +18,7 @@ use std::sync::Arc; use std::{cmp, env, io, mem, str, u32, u64}; use tempfile; -use server::field::{FieldHeaders, MultipartField, MultipartData, ReadEntry, ReadEntryResult}; +use crate::server::field::{FieldHeaders, MultipartField, MultipartData, ReadEntry, ReadEntryResult}; use self::SaveResult::*; use self::TextPolicy::*; @@ -27,7 +27,7 @@ use self::PartialReason::*; const RANDOM_FILENAME_LEN: usize = 12; fn rand_filename() -> String { - ::random_alphanumeric(RANDOM_FILENAME_LEN) + crate::random_alphanumeric(RANDOM_FILENAME_LEN) } macro_rules! try_start ( @@ -664,7 +664,7 @@ impl Entries { Occupied(occupied) => { // dedup the field name by reusing the key's `Arc` headers.name = occupied.key().clone(); - occupied.into_mut().push({ SavedField { headers, data }}); + occupied.into_mut().push(SavedField { headers, data }); }, } From 3c7556fd06b56602b3ecce39e6bc6c922e933e5e Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 14 Jun 2025 01:24:44 +0000 Subject: [PATCH 3/7] Replace `buf-redux` with `buffer-redux` `buffer-redux` [1] is a maintained fork of `buf-redux` [2]. Switch to it in `multipart`. [1]: https://docs.rs/buffer-redux/latest/buffer_redux/ [2]: https://docs.rs/buf_redux/latest/buf_redux/ --- multipart/Cargo.toml | 6 ++---- multipart/src/server/boundary.rs | 4 ++-- multipart/src/server/mod.rs | 2 +- multipart/src/server/save.rs | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/multipart/Cargo.toml b/multipart/Cargo.toml index 92d488a59..7f3dadbec 100644 --- a/multipart/Cargo.toml +++ b/multipart/Cargo.toml @@ -30,9 +30,7 @@ tempfile = "3" clippy = { version = ">=0.0, <0.1", optional = true} #Server Dependencies - -buf_redux = { version = "0.8", optional = true, default-features = false } - +buffer-redux = { version = "1.0", optional = true, default-features = false } httparse = { version = "1.2", optional = true } twoway = { version = "0.1", optional = true } quick-error = { version = "1.2", optional = true } @@ -52,7 +50,7 @@ env_logger = "0.5" [features] client = [] default = ["client", "hyper", "iron", "mock", "nickel", "server", "tiny_http"] -server = ["buf_redux", "httparse", "quick-error", "safemem", "twoway"] +server = ["buffer-redux", "httparse", "quick-error", "safemem", "twoway"] mock = [] nightly = [] bench = [] diff --git a/multipart/src/server/boundary.rs b/multipart/src/server/boundary.rs index 21f37d14d..37ccfda08 100644 --- a/multipart/src/server/boundary.rs +++ b/multipart/src/server/boundary.rs @@ -9,8 +9,8 @@ use ::safemem; -use super::buf_redux::BufReader; -use super::buf_redux::policy::MinBuffered; +use super::buffer_redux::BufReader; +use super::buffer_redux::policy::MinBuffered; use super::twoway; use std::cmp; diff --git a/multipart/src/server/mod.rs b/multipart/src/server/mod.rs index 7d8931d29..20fedc0ae 100644 --- a/multipart/src/server/mod.rs +++ b/multipart/src/server/mod.rs @@ -11,7 +11,7 @@ //! //! See the `Multipart` struct for more info. -pub extern crate buf_redux; +pub extern crate buffer_redux; extern crate httparse; extern crate twoway; diff --git a/multipart/src/server/save.rs b/multipart/src/server/save.rs index 372ff4384..7df9171cc 100644 --- a/multipart/src/server/save.rs +++ b/multipart/src/server/save.rs @@ -6,7 +6,7 @@ // copied, modified, or distributed except according to those terms. //! Utilities for saving request entries to the filesystem. -pub use crate::server::buf_redux::BufReader; +pub use crate::server::buffer_redux::BufReader; pub use crate::tempfile::TempDir; From 2cdc050364d03af9ea728284ad597cb7735cd7f6 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 14 Jun 2025 00:26:06 +0000 Subject: [PATCH 4/7] Switch to using the local `multipart` in Rouille --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bd6103d7a..1c20eec10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ brotli = { version = "3.3.2", optional = true } chrono = { version = "0.4.19", default-features = false, features = ["clock"] } filetime = "0.2.0" deflate = { version = "1.0.0", optional = true, features = ["gzip"] } -multipart = { version = "0.18", default-features = false, features = ["server"] } +multipart = { version = "0.18", path = "multipart", default-features = false, features = ["server"] } percent-encoding = "2" rand = "0.8" serde = "1" From e240ded89806185eb880d63bb9d77c3c78df02d2 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 14 Jun 2025 01:35:07 +0000 Subject: [PATCH 5/7] Add `multipart` to the workspace --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 1c20eec10..2945ed388 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,6 @@ sha1_smol = "1.0.0" [dev-dependencies] postgres = { version = "0.19", default-features = false } log = "0.4" + +[workspace] +members = ["multipart"] From ab4cec5eaa84a6d5042920b36b250793df8ae523 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 14 Jun 2025 18:10:51 +0000 Subject: [PATCH 6/7] Run `cargo fmt` to update `multipart` --- multipart/examples/hyper_client.rs | 19 +-- multipart/examples/hyper_reqbuilder.rs | 2 +- multipart/examples/hyper_server.rs | 22 +-- multipart/examples/iron.rs | 25 +-- multipart/examples/iron_intercept.rs | 8 +- multipart/examples/nickel.rs | 36 ++-- multipart/examples/rocket.rs | 32 ++-- multipart/examples/tiny_http.rs | 25 +-- multipart/src/bin/form_test.rs | 13 +- multipart/src/client/hyper.rs | 19 ++- multipart/src/client/lazy.rs | 2 +- multipart/src/client/sized.rs | 31 ++-- multipart/src/mock.rs | 22 ++- multipart/src/server/boundary.rs | 117 ++++++++----- multipart/src/server/hyper.rs | 68 ++++---- multipart/src/server/iron.rs | 126 +++++++++----- multipart/src/server/mod.rs | 106 +++++++----- multipart/src/server/nickel.rs | 21 ++- multipart/src/server/save.rs | 224 +++++++++++++++++-------- 19 files changed, 575 insertions(+), 343 deletions(-) diff --git a/multipart/examples/hyper_client.rs b/multipart/examples/hyper_client.rs index 8e968f758..a8bdaad27 100644 --- a/multipart/examples/hyper_client.rs +++ b/multipart/examples/hyper_client.rs @@ -10,23 +10,21 @@ use multipart::client::Multipart; use std::io::Read; fn main() { - let url = "http://localhost:80".parse() - .expect("Failed to parse URL"); + let url = "http://localhost:80".parse().expect("Failed to parse URL"); - let request = Request::new(Method::Post, url) - .expect("Failed to create request"); + let request = Request::new(Method::Post, url).expect("Failed to create request"); - let mut multipart = Multipart::from_request(request) - .expect("Failed to create Multipart"); + let mut multipart = Multipart::from_request(request).expect("Failed to create Multipart"); - write_body(&mut multipart) - .expect("Failed to write multipart body"); + write_body(&mut multipart).expect("Failed to write multipart body"); let mut response = multipart.send().expect("Failed to send multipart request"); if !response.status.is_success() { let mut res = String::new(); - response.read_to_string(&mut res).expect("failed to read response"); + response + .read_to_string(&mut res) + .expect("failed to read response"); println!("response reported unsuccessful: {:?}\n {}", response, res); } @@ -39,6 +37,7 @@ fn write_body(multi: &mut Multipart>) -> hyper::Result<()> { multi.write_text("text", "Hello, world!")?; multi.write_file("file", "lorem_ipsum.txt")?; // &[u8] impl Read - multi.write_stream("binary", &mut binary, None, None) + multi + .write_stream("binary", &mut binary, None, None) .and(Ok(())) } diff --git a/multipart/examples/hyper_reqbuilder.rs b/multipart/examples/hyper_reqbuilder.rs index 05c5539c8..06c0bbc0e 100644 --- a/multipart/examples/hyper_reqbuilder.rs +++ b/multipart/examples/hyper_reqbuilder.rs @@ -16,4 +16,4 @@ fn main() { // Request is sent here .client_request(&Client::new(), "http://localhost:80") .expect("Error sending multipart request"); -} \ No newline at end of file +} diff --git a/multipart/examples/hyper_server.rs b/multipart/examples/hyper_server.rs index 56ebc8a88..30333323d 100644 --- a/multipart/examples/hyper_server.rs +++ b/multipart/examples/hyper_server.rs @@ -1,13 +1,13 @@ extern crate hyper; extern crate multipart; -use std::io; -use hyper::server::{Handler, Server, Request, Response}; -use hyper::status::StatusCode; use hyper::server::response::Response as HyperResponse; -use multipart::server::hyper::{Switch, MultipartHandler, HyperRequest}; -use multipart::server::{Multipart, Entries, SaveResult}; +use hyper::server::{Handler, Request, Response, Server}; +use hyper::status::StatusCode; use multipart::mock::StdoutTee; +use multipart::server::hyper::{HyperRequest, MultipartHandler, Switch}; +use multipart::server::{Entries, Multipart, SaveResult}; +use std::io; struct NonMultipart; impl Handler for NonMultipart { @@ -28,7 +28,8 @@ impl MultipartHandler for EchoMultipart { } SaveResult::Error(error) => { println!("Errors saving multipart:\n{:?}", error); - res.send(format!("An error occurred {}", error).as_bytes()).unwrap(); + res.send(format!("An error occurred {}", error).as_bytes()) + .unwrap(); } }; } @@ -43,9 +44,8 @@ fn process_entries(res: HyperResponse, entries: Entries) -> io::Result<()> { fn main() { println!("Listening on 0.0.0.0:3333"); - Server::http("0.0.0.0:3333").unwrap().handle( - Switch::new( - NonMultipart, - EchoMultipart - )).unwrap(); + Server::http("0.0.0.0:3333") + .unwrap() + .handle(Switch::new(NonMultipart, EchoMultipart)) + .unwrap(); } diff --git a/multipart/examples/iron.rs b/multipart/examples/iron.rs index effeef68e..944c0b34b 100644 --- a/multipart/examples/iron.rs +++ b/multipart/examples/iron.rs @@ -1,18 +1,20 @@ -extern crate multipart; extern crate iron; +extern crate multipart; extern crate env_logger; -use std::io::{self, Write}; -use multipart::mock::StdoutTee; -use multipart::server::{Multipart, Entries, SaveResult}; use iron::prelude::*; use iron::status; +use multipart::mock::StdoutTee; +use multipart::server::{Entries, Multipart, SaveResult}; +use std::io::{self, Write}; fn main() { env_logger::init(); - Iron::new(process_request).http("localhost:80").expect("Could not bind localhost:80"); + Iron::new(process_request) + .http("localhost:80") + .expect("Could not bind localhost:80"); } /// Processes a request and returns response or an occured error. @@ -29,18 +31,19 @@ fn process_request(request: &mut Request) -> IronResult { process_entries(entries.keep_partial())?; Ok(Response::with(( status::BadRequest, - format!("error reading request: {}", reason.unwrap_err()) + format!("error reading request: {}", reason.unwrap_err()), ))) } SaveResult::Error(error) => Ok(Response::with(( status::BadRequest, - format!("error reading request: {}", error) + format!("error reading request: {}", error), ))), } } - Err(_) => { - Ok(Response::with((status::BadRequest, "The request is not multipart"))) - } + Err(_) => Ok(Response::with(( + status::BadRequest, + "The request is not multipart", + ))), } } @@ -55,7 +58,7 @@ fn process_entries(entries: Entries) -> IronResult { entries.write_debug(tee).map_err(|e| { IronError::new( e, - (status::InternalServerError, "Error printing request fields") + (status::InternalServerError, "Error printing request fields"), ) })?; } diff --git a/multipart/examples/iron_intercept.rs b/multipart/examples/iron_intercept.rs index fa328dd6e..d9f5ba0d6 100644 --- a/multipart/examples/iron_intercept.rs +++ b/multipart/examples/iron_intercept.rs @@ -3,18 +3,18 @@ extern crate multipart; use iron::prelude::*; -use multipart::server::Entries; use multipart::server::iron::Intercept; +use multipart::server::Entries; fn main() { // We start with a basic request handler chain. - let mut chain = Chain::new(|req: &mut Request| + let mut chain = Chain::new(|req: &mut Request| { if let Some(entries) = req.extensions.get::() { Ok(Response::with(format!("{:?}", entries))) } else { Ok(Response::with("Not a multipart request")) } - ); + }); // `Intercept` will read out the entries and place them as an extension in the request. // It has various builder-style methods for changing how it will behave, but has sane settings @@ -22,4 +22,4 @@ fn main() { chain.link_before(Intercept::default()); Iron::new(chain).http("localhost:80").unwrap(); -} \ No newline at end of file +} diff --git a/multipart/examples/nickel.rs b/multipart/examples/nickel.rs index 71ec8ff3c..b1a5334fd 100644 --- a/multipart/examples/nickel.rs +++ b/multipart/examples/nickel.rs @@ -1,35 +1,33 @@ extern crate multipart; extern crate nickel; -use std::io::{self, Write}; -use nickel::{Action, HttpRouter, MiddlewareResult, Nickel, Request, Response}; use nickel::status::StatusCode; +use nickel::{Action, HttpRouter, MiddlewareResult, Nickel, Request, Response}; +use std::io::{self, Write}; +use multipart::mock::StdoutTee; use multipart::server::nickel::MultipartBody; use multipart::server::{Entries, SaveResult}; -use multipart::mock::StdoutTee; fn handle_multipart<'mw>(req: &mut Request, mut res: Response<'mw>) -> MiddlewareResult<'mw> { match (*req).multipart_body() { - Some(mut multipart) => { - match multipart.save().temp() { - SaveResult::Full(entries) => process_entries(res, entries), - - SaveResult::Partial(entries, e) => { - println!("Partial errors ... {:?}", e); - return process_entries(res, entries.keep_partial()); - }, - - SaveResult::Error(e) => { - println!("There are errors in multipart POSTing ... {:?}", e); - res.set(StatusCode::InternalServerError); - return res.send(format!("Server could not handle multipart POST! {:?}", e)); - }, + Some(mut multipart) => match multipart.save().temp() { + SaveResult::Full(entries) => process_entries(res, entries), + + SaveResult::Partial(entries, e) => { + println!("Partial errors ... {:?}", e); + return process_entries(res, entries.keep_partial()); } - } + + SaveResult::Error(e) => { + println!("There are errors in multipart POSTing ... {:?}", e); + res.set(StatusCode::InternalServerError); + return res.send(format!("Server could not handle multipart POST! {:?}", e)); + } + }, None => { res.set(StatusCode::BadRequest); - return res.send("Request seems not was a multipart request") + return res.send("Request seems not was a multipart request"); } } } diff --git a/multipart/examples/rocket.rs b/multipart/examples/rocket.rs index cf9e6f714..0cdb7953b 100644 --- a/multipart/examples/rocket.rs +++ b/multipart/examples/rocket.rs @@ -10,39 +10,45 @@ extern crate multipart; extern crate rocket; use multipart::mock::StdoutTee; -use multipart::server::Multipart; use multipart::server::save::Entries; use multipart::server::save::SaveResult::*; +use multipart::server::Multipart; -use rocket::Data; use rocket::http::{ContentType, Status}; -use rocket::response::Stream; use rocket::response::status::Custom; +use rocket::response::Stream; +use rocket::Data; use std::io::{self, Cursor, Write}; #[post("/upload", data = "")] // signature requires the request to have a `Content-Type` -fn multipart_upload(cont_type: &ContentType, data: Data) -> Result>>, Custom> { +fn multipart_upload( + cont_type: &ContentType, + data: Data, +) -> Result>>, Custom> { // this and the next check can be implemented as a request guard but it seems like just // more boilerplate than necessary if !cont_type.is_form_data() { return Err(Custom( Status::BadRequest, - "Content-Type not multipart/form-data".into() + "Content-Type not multipart/form-data".into(), )); } - let (_, boundary) = cont_type.params().find(|&(k, _)| k == "boundary").ok_or_else( - || Custom( + let (_, boundary) = cont_type + .params() + .find(|&(k, _)| k == "boundary") + .ok_or_else(|| { + Custom( Status::BadRequest, - "`Content-Type: multipart/form-data` boundary param not provided".into() + "`Content-Type: multipart/form-data` boundary param not provided".into(), ) - )?; + })?; match process_upload(boundary, data) { Ok(resp) => Ok(Stream::from(Cursor::new(resp))), - Err(err) => Err(Custom(Status::InternalServerError, err.to_string())) + Err(err) => Err(Custom(Status::InternalServerError, err.to_string())), } } @@ -61,7 +67,7 @@ fn process_upload(boundary: &str, data: Data) -> io::Result> { } process_entries(partial.entries, &mut out)? - }, + } Error(e) => return Err(e), } @@ -81,5 +87,7 @@ fn process_entries(entries: Entries, mut out: &mut Vec) -> io::Result<()> { } fn main() { - rocket::ignite().mount("/", routes![multipart_upload]).launch(); + rocket::ignite() + .mount("/", routes![multipart_upload]) + .launch(); } diff --git a/multipart/examples/tiny_http.rs b/multipart/examples/tiny_http.rs index caef10914..73b62abac 100644 --- a/multipart/examples/tiny_http.rs +++ b/multipart/examples/tiny_http.rs @@ -1,10 +1,10 @@ -extern crate tiny_http; extern crate multipart; +extern crate tiny_http; -use std::io::{self, Cursor, Write}; -use multipart::server::{Multipart, Entries, SaveResult}; use multipart::mock::StdoutTee; -use tiny_http::{Response, StatusCode, Request}; +use multipart::server::{Entries, Multipart, SaveResult}; +use std::io::{self, Cursor, Write}; +use tiny_http::{Request, Response, StatusCode}; fn main() { // Starting a server on `localhost:80` let server = tiny_http::Server::http("localhost:80").expect("Could not bind localhost:80"); @@ -18,7 +18,10 @@ fn main() { Ok(resp) => resp, Err(e) => { println!("An error has occured during request proccessing: {:?}", e); - build_response(500, "The received data was not correctly proccessed on the server") + build_response( + 500, + "The received data was not correctly proccessed on the server", + ) } }; @@ -70,9 +73,11 @@ fn process_entries(entries: Entries) -> io::Result> { fn build_response>>(status_code: u16, data: D) -> Response { let data = data.into(); let data_len = data.len(); - Response::new(StatusCode(status_code), - vec![], - Cursor::new(data), - Some(data_len), - None) + Response::new( + StatusCode(status_code), + vec![], + Cursor::new(data), + Some(data_len), + None, + ) } diff --git a/multipart/src/bin/form_test.rs b/multipart/src/bin/form_test.rs index 8cf1b9499..f039e8195 100644 --- a/multipart/src/bin/form_test.rs +++ b/multipart/src/bin/form_test.rs @@ -10,8 +10,10 @@ use std::fs::File; use std::io; fn main() { - let listening = Server::http("127.0.0.1:0").expect("failed to bind socket") - .handle(read_multipart).expect("failed to handle request"); + let listening = Server::http("127.0.0.1:0") + .expect("failed to bind socket") + .handle(read_multipart) + .expect("failed to handle request"); println!("bound socket to: {}", listening.socket); } @@ -23,10 +25,11 @@ fn read_multipart(req: Request, mut resp: Response) { } } - let mut file = File::open("src/bin/test_form.html") - .expect("failed to open src/bind/test_form.html"); + let mut file = + File::open("src/bin/test_form.html").expect("failed to open src/bind/test_form.html"); - resp.headers_mut().set(ContentType("text/html".parse().unwrap())); + resp.headers_mut() + .set(ContentType("text/html".parse().unwrap())); let mut resp = resp.start().expect("failed to open response"); io::copy(&mut file, &mut resp).expect("failed to write response"); diff --git a/multipart/src/client/hyper.rs b/multipart/src/client/hyper.rs index 675cba6d1..0d30be2ac 100644 --- a/multipart/src/client/hyper.rs +++ b/multipart/src/client/hyper.rs @@ -4,7 +4,7 @@ // http://apache.org/licenses/LICENSE-2.0> or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. -//! Client-side integration with [Hyper](https://github.com/hyperium/hyper). +//! Client-side integration with [Hyper](https://github.com/hyperium/hyper). //! Enabled with the `hyper` feature (on by default). //! //! Contains `impl HttpRequest for Request` and `impl HttpStream for Request`. @@ -14,13 +14,13 @@ //! (adaptors for `hyper::client::RequestBuilder`). use hyper::client::request::Request; use hyper::client::response::Response; -use hyper::header::{ContentType, ContentLength}; +use hyper::header::{ContentLength, ContentType}; use hyper::method::Method; use hyper::net::{Fresh, Streaming}; use hyper::Error as HyperError; -use hyper::mime::{Mime, TopLevel, SubLevel, Attr, Value}; +use hyper::mime::{Attr, Mime, SubLevel, TopLevel, Value}; use super::{HttpRequest, HttpStream}; @@ -46,10 +46,10 @@ impl HttpRequest for Request { headers.set(ContentType(multipart_mime(boundary))); if let Some(size) = content_len { - headers.set(ContentLength(size)); + headers.set(ContentLength(size)); } - debug!("Hyper headers: {}", headers); + debug!("Hyper headers: {}", headers); true } @@ -57,7 +57,7 @@ impl HttpRequest for Request { fn open_stream(self) -> Result { self.start() } -} +} /// #### Feature: `hyper` impl HttpStream for Request { @@ -77,7 +77,8 @@ pub fn content_type(bound: &str) -> ContentType { fn multipart_mime(bound: &str) -> Mime { Mime( - TopLevel::Multipart, SubLevel::Ext("form-data".into()), - vec![(Attr::Ext("boundary".into()), Value::Ext(bound.into()))] - ) + TopLevel::Multipart, + SubLevel::Ext("form-data".into()), + vec![(Attr::Ext("boundary".into()), Value::Ext(bound.into()))], + ) } diff --git a/multipart/src/client/lazy.rs b/multipart/src/client/lazy.rs index d19bb242a..ca7cbb656 100644 --- a/multipart/src/client/lazy.rs +++ b/multipart/src/client/lazy.rs @@ -484,9 +484,9 @@ fn cursor_at_end>(cursor: &Cursor) -> bool { #[cfg(feature = "hyper")] mod hyper { + use crate::client; use hyper::client::{Body, Client, IntoUrl, RequestBuilder, Response}; use hyper::Result as HyperResult; - use crate::client; impl<'n, 'd> super::Multipart<'n, 'd> { /// #### Feature: `hyper` diff --git a/multipart/src/client/sized.rs b/multipart/src/client/sized.rs index 874ad2253..e13c2e5bc 100644 --- a/multipart/src/client/sized.rs +++ b/multipart/src/client/sized.rs @@ -14,16 +14,16 @@ use std::io::prelude::*; /// A wrapper around a request object that measures the request body and sets the `Content-Length` /// header to its size in bytes. /// -/// Sized requests are more human-readable and use less bandwidth +/// Sized requests are more human-readable and use less bandwidth /// (as chunking adds [visual noise and overhead][chunked-example]), /// but they must be able to load their entirety, including the contents of all files /// and streams, into memory so the request body can be measured. /// /// You should really only use sized requests if you intend to inspect the data manually on the /// server side, as it will produce a more human-readable request body. Also, of course, if the -/// server doesn't support chunked requests or otherwise rejects them. +/// server doesn't support chunked requests or otherwise rejects them. /// -/// [chunked-example]: http://en.wikipedia.org/wiki/Chunked_transfer_encoding#Example +/// [chunked-example]: http://en.wikipedia.org/wiki/Chunked_transfer_encoding#Example pub struct SizedRequest { inner: R, buffer: Vec, @@ -46,11 +46,15 @@ impl Write for SizedRequest { self.buffer.write(data) } - fn flush(&mut self) -> io::Result<()> { Ok(()) } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } } -impl HttpRequest for SizedRequest -where ::Error: From { +impl HttpRequest for SizedRequest +where + ::Error: From, +{ type Stream = Self; type Error = R::Error; @@ -67,20 +71,23 @@ where ::Error: From { } } -impl HttpStream for SizedRequest -where ::Error: From { +impl HttpStream for SizedRequest +where + ::Error: From, +{ type Request = Self; type Response = <::Stream as HttpStream>::Response; type Error = <::Stream as HttpStream>::Error; fn finish(mut self) -> Result { let content_len = self.buffer.len() as u64; - + if !self.inner.apply_headers(&self.boundary, Some(content_len)) { return Err(io::Error::new( - io::ErrorKind::Other, - "SizedRequest failed to apply headers to wrapped request." - ).into()); + io::ErrorKind::Other, + "SizedRequest failed to apply headers to wrapped request.", + ) + .into()); } let mut req = self.inner.open_stream()?; diff --git a/multipart/src/mock.rs b/multipart/src/mock.rs index c6f9d4ee8..1831cb4a9 100644 --- a/multipart/src/mock.rs +++ b/multipart/src/mock.rs @@ -5,11 +5,11 @@ // http://opensource.org/licenses/MIT>, at your option. This file may not be // copied, modified, or distributed except according to those terms. //! Mocked types for client-side and server-side APIs. -use std::io::{self, Read, Write}; use std::fmt; +use std::io::{self, Read, Write}; -use rand::{self, Rng}; use rand::prelude::ThreadRng; +use rand::{self, Rng}; use crate::{client, server}; @@ -37,13 +37,14 @@ impl client::HttpRequest for ClientRequest { /// If `apply_headers()` was not called. fn open_stream(self) -> Result { debug!("ClientRequest::open_stream called! {:?}", self); - let boundary = self.boundary.expect("ClientRequest::set_headers() was not called!"); + let boundary = self + .boundary + .expect("ClientRequest::set_headers() was not called!"); Ok(HttpBuffer::new_empty(boundary, self.content_len)) } } - /// A writable buffer which stores the boundary and content-length, if provided. /// /// Implements `client::HttpStream` if the `client` feature is enabled. @@ -69,7 +70,7 @@ impl HttpBuffer { buf, boundary, content_len, - rng: rand::thread_rng() + rng: rand::thread_rng(), } } @@ -111,7 +112,9 @@ impl client::HttpStream for HttpBuffer { type Error = io::Error; /// Returns `Ok(self)`. - fn finish(self) -> Result { Ok(self) } + fn finish(self) -> Result { + Ok(self) + } } impl fmt::Debug for HttpBuffer { @@ -170,7 +173,9 @@ impl<'a> Read for ServerRequest<'a> { impl<'a> server::HttpRequest for ServerRequest<'a> { type Body = Self; - fn multipart_boundary(&self) -> Option<&str> { Some(self.boundary) } + fn multipart_boundary(&self) -> Option<&str> { + Some(self.boundary) + } fn body(self) -> Self::Body { self @@ -187,7 +192,8 @@ impl<'s, W> StdoutTee<'s, W> { /// Constructor pub fn new(inner: W, stdout: &'s io::Stdout) -> Self { Self { - inner, stdout: stdout.lock(), + inner, + stdout: stdout.lock(), } } } diff --git a/multipart/src/server/boundary.rs b/multipart/src/server/boundary.rs index 37ccfda08..6d4fbf2f9 100644 --- a/multipart/src/server/boundary.rs +++ b/multipart/src/server/boundary.rs @@ -9,12 +9,12 @@ use ::safemem; -use super::buffer_redux::BufReader; use super::buffer_redux::policy::MinBuffered; +use super::buffer_redux::BufReader; use super::twoway; -use std::cmp; use std::borrow::Borrow; +use std::cmp; use std::io; use std::io::prelude::*; @@ -27,7 +27,7 @@ pub const MIN_BUF_SIZE: usize = 1024; enum State { Searching, BoundaryRead, - AtEnd + AtEnd, } /// A struct implementing `Read` and `BufRead` that will yield bytes until it sees a given sequence. @@ -39,7 +39,10 @@ pub struct BoundaryReader { state: State, } -impl BoundaryReader where R: Read { +impl BoundaryReader +where + R: Read, +{ /// Internal API pub fn from_reader>>(reader: R, boundary: B) -> BoundaryReader { let mut boundary = boundary.into(); @@ -59,11 +62,15 @@ impl BoundaryReader where R: Read { trace!("Buf: {:?}", String::from_utf8_lossy(buf)); - debug!("Before search Buf len: {} Search idx: {} State: {:?}", - buf.len(), self.search_idx, self.state); + debug!( + "Before search Buf len: {} Search idx: {} State: {:?}", + buf.len(), + self.search_idx, + self.state + ); if self.state == BoundaryRead || self.state == AtEnd { - return Ok(&buf[..self.search_idx]) + return Ok(&buf[..self.search_idx]); } if self.state == Searching && self.search_idx < buf.len() { @@ -74,22 +81,30 @@ impl BoundaryReader where R: Read { Ok(found_idx) => { self.search_idx += found_idx; self.state = BoundaryRead; - }, + } Err(yield_len) => { self.search_idx += yield_len; } } - } - - debug!("After search Buf len: {} Search idx: {} State: {:?}", - buf.len(), self.search_idx, self.state); + } + + debug!( + "After search Buf len: {} Search idx: {} State: {:?}", + buf.len(), + self.search_idx, + self.state + ); // back up the cursor to before the boundary's preceding CRLF if we haven't already if self.search_idx >= 2 && !buf[self.search_idx..].starts_with(b"\r\n") { - let two_bytes_before = &buf[self.search_idx - 2 .. self.search_idx]; + let two_bytes_before = &buf[self.search_idx - 2..self.search_idx]; - trace!("Two bytes before: {:?} ({:?}) (\"\\r\\n\": {:?})", - String::from_utf8_lossy(two_bytes_before), two_bytes_before, b"\r\n"); + trace!( + "Two bytes before: {:?} ({:?}) (\"\\r\\n\": {:?})", + String::from_utf8_lossy(two_bytes_before), + two_bytes_before, + b"\r\n" + ); if two_bytes_before == *b"\r\n" { debug!("Subtract two!"); @@ -122,8 +137,10 @@ impl BoundaryReader where R: Read { let buf_len = self.read_to_boundary()?.len(); if buf_len == 0 && self.state == Searching { - return Err(io::Error::new(io::ErrorKind::UnexpectedEof, - "unexpected end of request body")); + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "unexpected end of request body", + )); } debug!("Discarding {} bytes", buf_len); @@ -145,7 +162,7 @@ impl BoundaryReader where R: Read { consume_amt += 2; // assert that we've found the boundary after the CRLF - debug_assert_eq!(*self.boundary, bnd_segment[2 .. self.boundary.len() + 2]); + debug_assert_eq!(*self.boundary, bnd_segment[2..self.boundary.len() + 2]); } else { // assert that we've found the boundary debug_assert_eq!(*self.boundary, bnd_segment[..self.boundary.len()]); @@ -155,42 +172,51 @@ impl BoundaryReader where R: Read { consume_amt += 2; if buf.len() < consume_amt { - return Err(io::Error::new(io::ErrorKind::UnexpectedEof, - "not enough bytes to verify boundary")); + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "not enough bytes to verify boundary", + )); } // we have enough bytes to verify self.state = Searching; - let last_two = &buf[consume_amt - 2 .. consume_amt]; + let last_two = &buf[consume_amt - 2..consume_amt]; match last_two { b"\r\n" => self.state = Searching, b"--" => self.state = AtEnd, - _ => return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("unexpected bytes following multipart boundary: {:X} {:X}", - last_two[0], last_two[1]) - )), + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "unexpected bytes following multipart boundary: {:X} {:X}", + last_two[0], last_two[1] + ), + )) + } } consume_amt }; - trace!("Consuming {} bytes, remaining buf: {:?}", - consume_amt, - String::from_utf8_lossy(self.source.buffer())); + trace!( + "Consuming {} bytes, remaining buf: {:?}", + consume_amt, + String::from_utf8_lossy(self.source.buffer()) + ); self.source.consume(consume_amt); - if cfg!(debug_assertions) { - - } + if cfg!(debug_assertions) {} self.search_idx = 0; - trace!("Consumed boundary (state: {:?}), remaining buf: {:?}", self.state, - String::from_utf8_lossy(self.source.buffer())); + trace!( + "Consumed boundary (state: {:?}), remaining buf: {:?}", + self.state, + String::from_utf8_lossy(self.source.buffer()) + ); Ok(self.state != AtEnd) } @@ -205,7 +231,7 @@ fn find_boundary(buf: &[u8], boundary: &[u8]) -> Result { let search_start = buf.len().saturating_sub(boundary.len()); // search for just the boundary fragment - for i in search_start .. buf.len() { + for i in search_start..buf.len() { if boundary.starts_with(&buf[i..]) { return Err(i); } @@ -234,7 +260,10 @@ impl Borrow for BoundaryReader { } } -impl Read for BoundaryReader where R: Read { +impl Read for BoundaryReader +where + R: Read, +{ fn read(&mut self, out: &mut [u8]) -> io::Result { let read = { let mut buf = self.read_to_boundary()?; @@ -247,7 +276,10 @@ impl Read for BoundaryReader where R: Read { } } -impl BufRead for BoundaryReader where R: Read { +impl BufRead for BoundaryReader +where + R: Read, +{ fn fill_buf(&mut self) -> io::Result<&[u8]> { self.read_to_boundary() } @@ -275,7 +307,7 @@ mod test { --boundary\r\n\ dashed-value-2\r\n\ --boundary--"; - + #[test] fn test_boundary() { ::init_log(); @@ -286,7 +318,7 @@ mod test { let mut reader = BoundaryReader::from_reader(src, BOUNDARY); let mut buf = String::new(); - + test_boundary_reader(&mut reader, &mut buf); } @@ -299,7 +331,7 @@ mod test { fn split(data: &'a [u8], at: usize) -> SplitReader<'a> { let (left, right) = data.split_at(at); - SplitReader { + SplitReader { left: left, right: right, } @@ -329,9 +361,9 @@ mod test { debug!("Testing boundary (split)"); let mut buf = String::new(); - + // Substitute for `.step_by()` being unstable. - for split_at in 0 .. TEST_VAL.len(){ + for split_at in 0..TEST_VAL.len() { debug!("Testing split at: {}", split_at); let src = SplitReader::split(TEST_VAL.as_bytes(), split_at); @@ -405,7 +437,6 @@ mod test { let ref mut buf = String::new(); let mut reader = BoundaryReader::from_reader(&mut body, BOUNDARY); - debug!("Consume 1"); assert_eq!(reader.consume_boundary().unwrap(), true); diff --git a/multipart/src/server/hyper.rs b/multipart/src/server/hyper.rs index 57022dc6d..2c9f5e2de 100644 --- a/multipart/src/server/hyper.rs +++ b/multipart/src/server/hyper.rs @@ -9,16 +9,16 @@ //! //! Also contains an implementation of [`HttpRequest`](../trait.HttpRequest.html) //! for `hyper::server::Request` and `&mut hyper::server::Request`. -use hyper::net::Fresh; use hyper::header::ContentType; use hyper::method::Method; +use hyper::net::Fresh; use hyper::server::{Handler, Request, Response}; pub use hyper::server::Request as HyperRequest; -use hyper::mime::{Mime, TopLevel, SubLevel, Attr, Value}; +use hyper::mime::{Attr, Mime, SubLevel, TopLevel, Value}; -use super::{Multipart, HttpRequest}; +use super::{HttpRequest, Multipart}; /// A container that implements `hyper::server::Handler` which will switch /// the handler implementation depending on if the incoming request is multipart or not. @@ -32,7 +32,11 @@ pub struct Switch { multipart: M, } -impl Switch where H: Handler, M: MultipartHandler { +impl Switch +where + H: Handler, + M: MultipartHandler, +{ /// Create a new `Switch` instance where /// `normal` handles normal Hyper requests and `multipart` handles Multipart requests pub fn new(normal: H, multipart: M) -> Switch { @@ -40,7 +44,11 @@ impl Switch where H: Handler, M: MultipartHandler { } } -impl Handler for Switch where H: Handler, M: MultipartHandler { +impl Handler for Switch +where + H: Handler, + M: MultipartHandler, +{ fn handle<'a, 'k>(&'a self, req: Request<'a, 'k>, res: Response<'a, Fresh>) { match Multipart::from_request(req) { Ok(multi) => self.multipart.handle_multipart(multi, res), @@ -55,16 +63,23 @@ impl Handler for Switch where H: Handler, M: MultipartHandler { /// and subsequently static functions. pub trait MultipartHandler: Send + Sync { /// Generate a response from this multipart request. - fn handle_multipart<'a, 'k>(&self, - multipart: Multipart>, - response: Response<'a, Fresh>); + fn handle_multipart<'a, 'k>( + &self, + multipart: Multipart>, + response: Response<'a, Fresh>, + ); } -impl MultipartHandler for F -where F: Fn(Multipart, Response), F: Send + Sync { - fn handle_multipart<'a, 'k>(&self, - multipart: Multipart>, - response: Response<'a, Fresh>) { +impl MultipartHandler for F +where + F: Fn(Multipart, Response), + F: Send + Sync, +{ + fn handle_multipart<'a, 'k>( + &self, + multipart: Multipart>, + response: Response<'a, Fresh>, + ) { (*self)(multipart, response); } } @@ -84,17 +99,16 @@ impl<'a, 'b> HttpRequest for HyperRequest<'a, 'b> { _ => return None, }; - params.iter().find(|&&(ref name, _)| - match *name { + params + .iter() + .find(|&&(ref name, _)| match *name { Attr::Boundary => true, _ => false, - } - ).and_then(|&(_, ref val)| - match *val { + }) + .and_then(|&(_, ref val)| match *val { Value::Ext(ref val) => Some(&**val), _ => None, - } - ) + }) }) } @@ -118,17 +132,16 @@ impl<'r, 'a, 'b> HttpRequest for &'r mut HyperRequest<'a, 'b> { _ => return None, }; - params.iter().find(|&&(ref name, _)| - match *name { + params + .iter() + .find(|&&(ref name, _)| match *name { Attr::Boundary => true, _ => false, - } - ).and_then(|&(_, ref val)| - match *val { + }) + .and_then(|&(_, ref val)| match *val { Value::Ext(ref val) => Some(&**val), _ => None, - } - ) + }) }) } @@ -136,4 +149,3 @@ impl<'r, 'a, 'b> HttpRequest for &'r mut HyperRequest<'a, 'b> { self } } - diff --git a/multipart/src/server/iron.rs b/multipart/src/server/iron.rs index c4bef5712..cd93157e0 100644 --- a/multipart/src/server/iron.rs +++ b/multipart/src/server/iron.rs @@ -4,7 +4,7 @@ //! iron::Request`. use iron::headers::ContentType; -use iron::mime::{Mime, TopLevel, SubLevel}; +use iron::mime::{Mime, SubLevel, TopLevel}; use iron::request::{Body as IronBody, Request as IronRequest}; use iron::typemap::Key; use iron::{BeforeMiddleware, IronError, IronResult}; @@ -13,9 +13,9 @@ use std::path::PathBuf; use std::{error, fmt, io}; use tempfile; -use super::{FieldHeaders, HttpRequest, Multipart}; -use super::save::{Entries, PartialReason, TempDir}; use super::save::SaveResult::*; +use super::save::{Entries, PartialReason, TempDir}; +use super::{FieldHeaders, HttpRequest, Multipart}; impl<'r, 'a, 'b> HttpRequest for &'r mut IronRequest<'a, 'b> { type Body = &'r mut IronBody<'a, 'b>; @@ -101,22 +101,34 @@ pub struct Intercept { impl Intercept { /// Set the `temp_dir_path` for this middleware. pub fn temp_dir_path>(self, path: P) -> Self { - Intercept { temp_dir_path: Some(path.into()), .. self } + Intercept { + temp_dir_path: Some(path.into()), + ..self + } } /// Set the `file_size_limit` for this middleware. pub fn file_size_limit(self, limit: u64) -> Self { - Intercept { file_size_limit: limit, .. self } + Intercept { + file_size_limit: limit, + ..self + } } /// Set the `file_count_limit` for this middleware. pub fn file_count_limit(self, limit: u32) -> Self { - Intercept { file_count_limit: limit, .. self } + Intercept { + file_count_limit: limit, + ..self + } } /// Set the `limit_behavior` for this middleware. pub fn limit_behavior(self, behavior: LimitBehavior) -> Self { - Intercept { limit_behavior: behavior, .. self } + Intercept { + limit_behavior: behavior, + ..self + } } fn read_request(&self, req: &mut IronRequest) -> IronResult> { @@ -125,12 +137,18 @@ impl Intercept { Err(_) => return Ok(None), }; - let tempdir = self.temp_dir_path.as_ref() - .map_or_else( - || tempfile::Builder::new().prefix("multipart-iron").tempdir(), - |path| tempfile::Builder::new().prefix("multipart-iron").tempdir_in(path) - ) - .map_err(|e| io_to_iron(e, "Error opening temporary directory for request."))?; + let tempdir = self + .temp_dir_path + .as_ref() + .map_or_else( + || tempfile::Builder::new().prefix("multipart-iron").tempdir(), + |path| { + tempfile::Builder::new() + .prefix("multipart-iron") + .tempdir_in(path) + }, + ) + .map_err(|e| io_to_iron(e, "Error opening temporary directory for request."))?; match self.limit_behavior { LimitBehavior::ThrowError => self.read_request_strict(multipart, tempdir), @@ -138,42 +156,66 @@ impl Intercept { } } - fn read_request_strict(&self, mut multipart: IronMultipart, tempdir: TempDir) -> IronResult> { - match multipart.save().size_limit(self.file_size_limit) - .count_limit(self.file_count_limit) - .with_temp_dir(tempdir) { + fn read_request_strict( + &self, + mut multipart: IronMultipart, + tempdir: TempDir, + ) -> IronResult> { + match multipart + .save() + .size_limit(self.file_size_limit) + .count_limit(self.file_count_limit) + .with_temp_dir(tempdir) + { Full(entries) => Ok(Some(entries)), Partial(_, PartialReason::Utf8Error(_)) => unreachable!(), - Partial(_, PartialReason::IoError(err)) => Err(io_to_iron(err, "Error midway through request")), - Partial(_, PartialReason::CountLimit) => Err(FileCountLimitError(self.file_count_limit).into()), - Partial(partial, PartialReason::SizeLimit) => { + Partial(_, PartialReason::IoError(err)) => { + Err(io_to_iron(err, "Error midway through request")) + } + Partial(_, PartialReason::CountLimit) => { + Err(FileCountLimitError(self.file_count_limit).into()) + } + Partial(partial, PartialReason::SizeLimit) => { let partial = partial.partial.expect(EXPECT_PARTIAL_FILE); - Err( - FileSizeLimitError { - field: partial.source.headers, - }.into() - ) - }, + Err(FileSizeLimitError { + field: partial.source.headers, + } + .into()) + } Error(err) => Err(io_to_iron(err, "Error at start of request")), } } - fn read_request_lenient(&self, mut multipart: IronMultipart, tempdir: TempDir) -> IronResult> { - let mut entries = match multipart.save().size_limit(self.file_size_limit) - .count_limit(self.file_count_limit) - .with_temp_dir(tempdir) { + fn read_request_lenient( + &self, + mut multipart: IronMultipart, + tempdir: TempDir, + ) -> IronResult> { + let mut entries = match multipart + .save() + .size_limit(self.file_size_limit) + .count_limit(self.file_count_limit) + .with_temp_dir(tempdir) + { Full(entries) => return Ok(Some(entries)), - Partial(_, PartialReason::IoError(err)) => return Err(io_to_iron(err, "Error midway through request")), - Partial(partial, _) => partial.keep_partial(), + Partial(_, PartialReason::IoError(err)) => { + return Err(io_to_iron(err, "Error midway through request")) + } + Partial(partial, _) => partial.keep_partial(), Error(err) => return Err(io_to_iron(err, "Error at start of request")), }; loop { - entries = match multipart.save().size_limit(self.file_size_limit) - .count_limit(self.file_count_limit) - .with_entries(entries) { + entries = match multipart + .save() + .size_limit(self.file_size_limit) + .count_limit(self.file_count_limit) + .with_entries(entries) + { Full(entries) => return Ok(Some(entries)), - Partial(_, PartialReason::IoError(err)) => return Err(io_to_iron(err, "Error midway through request")), + Partial(_, PartialReason::IoError(err)) => { + return Err(io_to_iron(err, "Error midway through request")) + } Partial(partial, _) => partial.keep_partial(), Error(err) => return Err(io_to_iron(err, "Error at start of request")), }; @@ -242,8 +284,16 @@ impl error::Error for FileSizeLimitError { impl fmt::Display for FileSizeLimitError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.field.filename { - Some(ref filename) => write!(f, "File size limit reached for field \"{}\" (filename: \"{}\")", self.field.name, filename), - None => write!(f, "File size limit reached for field \"{}\" (no filename)", self.field.name), + Some(ref filename) => write!( + f, + "File size limit reached for field \"{}\" (filename: \"{}\")", + self.field.name, filename + ), + None => write!( + f, + "File size limit reached for field \"{}\" (no filename)", + self.field.name + ), } } } diff --git a/multipart/src/server/mod.rs b/multipart/src/server/mod.rs index 20fedc0ae..b0bd13d88 100644 --- a/multipart/src/server/mod.rs +++ b/multipart/src/server/mod.rs @@ -16,14 +16,14 @@ extern crate httparse; extern crate twoway; use std::borrow::Borrow; -use std::io::prelude::*; use std::io; +use std::io::prelude::*; use self::boundary::BoundaryReader; use self::field::PrivReadEntry; -pub use self::field::{FieldHeaders, MultipartField, MultipartData, ReadEntry, ReadEntryResult}; +pub use self::field::{FieldHeaders, MultipartData, MultipartField, ReadEntry, ReadEntryResult}; use self::save::SaveBuilder; @@ -48,12 +48,12 @@ macro_rules! try_opt ( ); macro_rules! try_read_entry { - ($self_:expr; $try:expr) => ( + ($self_:expr; $try:expr) => { match $try { Ok(res) => res, Err(err) => return crate::server::ReadEntryResult::Error($self_, err), } - ) + }; } mod boundary; @@ -91,8 +91,8 @@ impl Multipart<()> { None => return Err(req), }; - Ok(Multipart::with_body(req.body(), boundary)) - } + Ok(Multipart::with_body(req.body(), boundary)) + } } impl Multipart { @@ -110,7 +110,7 @@ impl Multipart { info!("Multipart::with_boundary(_, {:?})", boundary); - Multipart { + Multipart { reader: BoundaryReader::from_reader(body, boundary), } } @@ -132,12 +132,15 @@ impl Multipart { } /// Call `f` for each entry in the multipart request. - /// + /// /// This is a substitute for Rust not supporting streaming iterators (where the return value /// from `next()` borrows the iterator for a bound lifetime). /// /// Returns `Ok(())` when all fields have been read, or the first error. - pub fn foreach_entry(&mut self, mut foreach: F) -> io::Result<()> where F: FnMut(MultipartField<&mut Self>) { + pub fn foreach_entry(&mut self, mut foreach: F) -> io::Result<()> + where + F: FnMut(MultipartField<&mut Self>), + { loop { match self.read_entry() { Ok(Some(field)) => foreach(field), @@ -216,7 +219,9 @@ fn issue_104() { let request = Cursor::new(body); let mut multipart = Multipart::with_body(request, "boundary"); - multipart.foreach_entry(|_field| {/* Do nothing */}).unwrap_err(); + multipart + .foreach_entry(|_field| { /* Do nothing */ }) + .unwrap_err(); } #[test] @@ -226,7 +231,9 @@ fn issue_114() { fn consume_all(mut rdr: R) { loop { let consume = rdr.fill_buf().unwrap().len(); - if consume == 0 { return; } + if consume == 0 { + return; + } rdr.consume(consume); } } @@ -249,42 +256,51 @@ fn issue_114() { let mut multipart = Multipart::with_body(request, "------------------------c616e5fded96a3c7"); // one error if you do nothing - multipart.foreach_entry(|_entry| { /* do nothing */}).unwrap(); + multipart + .foreach_entry(|_entry| { /* do nothing */ }) + .unwrap(); // a different error if you skip the first field - multipart.foreach_entry(|entry| if *entry.headers.name != *"key1" { consume_all(entry.data); }) + multipart + .foreach_entry(|entry| { + if *entry.headers.name != *"key1" { + consume_all(entry.data); + } + }) .unwrap(); - - multipart.foreach_entry(|_entry| () /* match entry.headers.name.as_str() { - "file" => { - let mut vec = Vec::new(); - entry.data.read_to_end(&mut vec).expect("can't read"); - // message.file = String::from_utf8(vec).ok(); - println!("key file got"); - } - - "key1" => { - let mut vec = Vec::new(); - entry.data.read_to_end(&mut vec).expect("can't read"); - // message.key1 = String::from_utf8(vec).ok(); - println!("key1 got"); - } - - "key2" => { - let mut vec = Vec::new(); - entry.data.read_to_end(&mut vec).expect("can't read"); - // message.key2 = String::from_utf8(vec).ok(); - println!("key2 got"); - } - - _ => { - // as multipart has a bug https://github.com/abonander/multipart/issues/114 - // we manually do read_to_end here - //let mut _vec = Vec::new(); - //entry.data.read_to_end(&mut _vec).expect("can't read"); - println!("key neglected"); - } - }*/) - .expect("Unable to iterate multipart?") + multipart + .foreach_entry( + |_entry| (), /* match entry.headers.name.as_str() { + "file" => { + let mut vec = Vec::new(); + entry.data.read_to_end(&mut vec).expect("can't read"); + // message.file = String::from_utf8(vec).ok(); + println!("key file got"); + } + + "key1" => { + let mut vec = Vec::new(); + entry.data.read_to_end(&mut vec).expect("can't read"); + // message.key1 = String::from_utf8(vec).ok(); + println!("key1 got"); + } + + "key2" => { + let mut vec = Vec::new(); + entry.data.read_to_end(&mut vec).expect("can't read"); + // message.key2 = String::from_utf8(vec).ok(); + println!("key2 got"); + } + + _ => { + // as multipart has a bug https://github.com/abonander/multipart/issues/114 + // we manually do read_to_end here + //let mut _vec = Vec::new(); + //entry.data.read_to_end(&mut _vec).expect("can't read"); + println!("key neglected"); + } + }*/ + ) + .expect("Unable to iterate multipart?") } diff --git a/multipart/src/server/nickel.rs b/multipart/src/server/nickel.rs index 1cec1e1eb..1b4a603f8 100644 --- a/multipart/src/server/nickel.rs +++ b/multipart/src/server/nickel.rs @@ -1,11 +1,11 @@ //! Support for `multipart/form-data` bodies in [Nickel](https://nickel.rs). pub extern crate nickel; -use self::nickel::hyper; use self::hyper::header::ContentType; +use self::nickel::hyper; -pub use self::nickel::Request as NickelRequest; pub use self::nickel::hyper::server::Request as HyperRequest; +pub use self::nickel::Request as NickelRequest; use crate::server::{HttpRequest, Multipart}; @@ -43,27 +43,34 @@ impl<'mw, 'server, D: 'mw> MultipartBody<'mw, 'server> for NickelRequest<'mw, 's } } -impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> AsRef<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { +impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> AsRef<&'r mut NickelRequest<'mw, 'server, D>> + for Maybe<'r, 'mw, 'server, D> +{ fn as_ref(&self) -> &&'r mut NickelRequest<'mw, 'server, D> { &self.0 } } -impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> AsMut<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { +impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> AsMut<&'r mut NickelRequest<'mw, 'server, D>> + for Maybe<'r, 'mw, 'server, D> +{ fn as_mut(&mut self) -> &mut &'r mut NickelRequest<'mw, 'server, D> { &mut self.0 } } -impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> Into<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { +impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> Into<&'r mut NickelRequest<'mw, 'server, D>> + for Maybe<'r, 'mw, 'server, D> +{ fn into(self) -> &'r mut NickelRequest<'mw, 'server, D> { self.0 } } -impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> From<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { +impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> From<&'r mut NickelRequest<'mw, 'server, D>> + for Maybe<'r, 'mw, 'server, D> +{ fn from(req: &'r mut NickelRequest<'mw, 'server, D>) -> Self { Maybe(req) } } - diff --git a/multipart/src/server/save.rs b/multipart/src/server/save.rs index 7df9171cc..d39ee0993 100644 --- a/multipart/src/server/save.rs +++ b/multipart/src/server/save.rs @@ -11,18 +11,20 @@ pub use crate::server::buffer_redux::BufReader; pub use crate::tempfile::TempDir; use std::collections::HashMap; -use std::io::prelude::*; use std::fs::{self, File, OpenOptions}; +use std::io::prelude::*; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::{cmp, env, io, mem, str, u32, u64}; use tempfile; -use crate::server::field::{FieldHeaders, MultipartField, MultipartData, ReadEntry, ReadEntryResult}; +use crate::server::field::{ + FieldHeaders, MultipartData, MultipartField, ReadEntry, ReadEntryResult, +}; +use self::PartialReason::*; use self::SaveResult::*; use self::TextPolicy::*; -use self::PartialReason::*; const RANDOM_FILENAME_LEN: usize = 12; @@ -65,7 +67,7 @@ enum TextPolicy { /// Attempt to read a text field as text, returning any errors Force, /// Don't try to read text - Ignore + Ignore, } /// A builder for saving a file or files to the local filesystem. @@ -185,7 +187,10 @@ impl SaveBuilder { /// If `0`, forces fields to save directly to the filesystem. /// If `u64::MAX`, effectively forces fields to always save to memory. pub fn memory_threshold(self, memory_threshold: u64) -> Self { - Self { memory_threshold, ..self } + Self { + memory_threshold, + ..self + } } /// When encountering a field that is apparently text, try to read it to a string or fall @@ -196,7 +201,10 @@ impl SaveBuilder { /// /// Has no effect once `memory_threshold` has been reached. pub fn try_text(self) -> Self { - Self { text_policy: TextPolicy::Try, ..self } + Self { + text_policy: TextPolicy::Try, + ..self + } } /// When encountering a field that is apparently text, read it to a string or return an error. @@ -206,17 +214,26 @@ impl SaveBuilder { /// /// (RFC: should this continue to validate UTF-8 when writing to the filesystem?) pub fn force_text(self) -> Self { - Self { text_policy: TextPolicy::Force, ..self} + Self { + text_policy: TextPolicy::Force, + ..self + } } /// Don't try to read or validate any field data as UTF-8. pub fn ignore_text(self) -> Self { - Self { text_policy: TextPolicy::Ignore, ..self } + Self { + text_policy: TextPolicy::Ignore, + ..self + } } } /// Save API for whole multipart requests. -impl SaveBuilder where M: ReadEntry { +impl SaveBuilder +where + M: ReadEntry, +{ /// Set the maximum number of fields to process. /// /// Can be `u32` or `Option`. If `None` or `u32::MAX`, clears the limit. @@ -287,8 +304,12 @@ impl SaveBuilder where M: ReadEntry { /// reaches `u32::MAX`, but this would be an extremely degenerate case. pub fn with_entries(self, mut entries: Entries) -> EntriesSaveResult { let SaveBuilder { - savable, open_opts, count_limit, size_limit, - memory_threshold, text_policy + savable, + open_opts, + count_limit, + size_limit, + memory_threshold, + text_policy, } = self; let mut res = ReadEntry::read_entry(savable); @@ -299,8 +320,12 @@ impl SaveBuilder where M: ReadEntry { let text_policy = if field.is_text() { text_policy } else { Ignore }; let mut saver = SaveBuilder { - savable: &mut field.data, open_opts: open_opts.clone(), - count_limit, size_limit, memory_threshold, text_policy + savable: &mut field.data, + open_opts: open_opts.clone(), + count_limit, + size_limit, + memory_threshold, + text_policy, }; saver.with_dir(entries.save_dir.as_path()) @@ -310,13 +335,15 @@ impl SaveBuilder where M: ReadEntry { let mut field: MultipartField = match res { ReadEntryResult::Entry(field) => field, ReadEntryResult::End(_) => return Full(entries), // normal exit point - ReadEntryResult::Error(_, e) => return Partial ( - PartialEntries { - entries, - partial: None, - }, - e.into(), - ) + ReadEntryResult::Error(_, e) => { + return Partial( + PartialEntries { + entries, + partial: None, + }, + e.into(), + ) + } }; let (dest, reason) = match save_field(&mut field, &entries) { @@ -324,7 +351,7 @@ impl SaveBuilder where M: ReadEntry { entries.push_field(field.headers, saved); res = ReadEntry::read_entry(field.data.into_inner()); continue; - }, + } Partial(saved, reason) => (Some(saved), reason), Error(error) => (None, PartialReason::IoError(error)), }; @@ -337,7 +364,7 @@ impl SaveBuilder where M: ReadEntry { dest, }), }, - reason + reason, ); } @@ -346,13 +373,16 @@ impl SaveBuilder where M: ReadEntry { entries, partial: None, }, - PartialReason::CountLimit + PartialReason::CountLimit, ) } } /// Save API for individual fields. -impl<'m, M: 'm> SaveBuilder<&'m mut MultipartData> where MultipartData: BufRead { +impl<'m, M: 'm> SaveBuilder<&'m mut MultipartData> +where + MultipartData: BufRead, +{ /// Save the field data, potentially using a file with a random name in the /// OS temporary directory. /// @@ -407,7 +437,7 @@ impl<'m, M: 'm> SaveBuilder<&'m mut MultipartData> where MultipartData: Bu match reason { SizeLimit if !self.cmp_size_limit(bytes.len()) => (), - other => return Partial(bytes.into(), other) + other => return Partial(bytes.into(), other), } let path = path.into(); @@ -417,15 +447,14 @@ impl<'m, M: 'm> SaveBuilder<&'m mut MultipartData> where MultipartData: Bu Err(e) => return Error(e), }; - let data = try_full!( - try_write_all(&bytes, &mut file) - .map(move |size| SavedData::File(path, size as u64)) - ); + let data = + try_full!(try_write_all(&bytes, &mut file) + .map(move |size| SavedData::File(path, size as u64))); - self.write_to(file).map(move |written| data.add_size(written)) + self.write_to(file) + .map(move |written| data.add_size(written)) } - /// Write out the field data to `dest`, truncating if a limit was set. /// /// Returns the number of bytes copied, and whether or not the limit was reached @@ -434,7 +463,11 @@ impl<'m, M: 'm> SaveBuilder<&'m mut MultipartData> where MultipartData: Bu /// Retries on interrupts. pub fn write_to(&mut self, mut dest: W) -> SaveResult { if self.size_limit < u64::MAX { - try_copy_limited(&mut self.savable, |buf| try_write_all(buf, &mut dest), self.size_limit) + try_copy_limited( + &mut self.savable, + |buf| try_write_all(buf, &mut dest), + self.size_limit, + ) } else { try_read_buf(&mut self.savable, |buf| try_write_all(buf, &mut dest)) } @@ -442,31 +475,49 @@ impl<'m, M: 'm> SaveBuilder<&'m mut MultipartData> where MultipartData: Bu fn save_mem(&mut self, mut bytes: Vec) -> SaveResult, Vec> { let pre_read = bytes.len() as u64; - match self.read_mem(|buf| { bytes.extend_from_slice(buf); Full(buf.len()) }, pre_read) { + match self.read_mem( + |buf| { + bytes.extend_from_slice(buf); + Full(buf.len()) + }, + pre_read, + ) { Full(_) => Full(bytes), Partial(_, reason) => Partial(bytes, reason), - Error(e) => if !bytes.is_empty() { Partial(bytes, e.into()) } - else { Error(e) } + Error(e) => { + if !bytes.is_empty() { + Partial(bytes, e.into()) + } else { + Error(e) + } + } } - } fn save_text(&mut self) -> SaveResult { let mut string = String::new(); // incrementally validate UTF-8 to do as much work as possible during network activity - let res = self.read_mem(|buf| { - match str::from_utf8(buf) { - Ok(s) => { string.push_str(s); Full(buf.len()) }, - // buffer should always be bigger - Err(e) => if buf.len() < 4 { - Partial(0, e.into()) - } else { - string.push_str(str::from_utf8(&buf[..e.valid_up_to()]).unwrap()); - Full(e.valid_up_to()) + let res = self.read_mem( + |buf| { + match str::from_utf8(buf) { + Ok(s) => { + string.push_str(s); + Full(buf.len()) } - } - }, 0); + // buffer should always be bigger + Err(e) => { + if buf.len() < 4 { + Partial(0, e.into()) + } else { + string.push_str(str::from_utf8(&buf[..e.valid_up_to()]).unwrap()); + Full(e.valid_up_to()) + } + } + } + }, + 0, + ); match res { Full(_) => Full(string), @@ -475,9 +526,12 @@ impl<'m, M: 'm> SaveBuilder<&'m mut MultipartData> where MultipartData: Bu } } - fn read_mem SaveResult>(&mut self, with_buf: Wb, pre_read: u64) -> SaveResult { - let limit = cmp::min(self.size_limit, self.memory_threshold) - .saturating_sub(pre_read); + fn read_mem SaveResult>( + &mut self, + with_buf: Wb, + pre_read: u64, + ) -> SaveResult { + let limit = cmp::min(self.size_limit, self.memory_threshold).saturating_sub(pre_read); try_copy_limited(&mut self.savable, with_buf, limit) } @@ -549,7 +603,7 @@ impl SavedData { match self { File(path, size) => File(path, size.saturating_add(add)), - other => other + other => other, } } } @@ -616,7 +670,7 @@ pub struct Entries { /// Each vector is guaranteed not to be empty unless externally modified. // Even though individual fields might only have one entry, it's better to limit the // size of a value type in `HashMap` to improve cache efficiency in lookups. - pub fields: HashMap, Vec>, + pub fields: HashMap, Vec>, /// The directory that the entries in `fields` were saved into. pub save_dir: SaveDir, fields_count: u32, @@ -660,12 +714,14 @@ impl Entries { use std::collections::hash_map::Entry::*; match self.fields.entry(headers.name.clone()) { - Vacant(vacant) => { vacant.insert(vec![SavedField { headers, data }]); }, + Vacant(vacant) => { + vacant.insert(vec![SavedField { headers, data }]); + } Occupied(occupied) => { // dedup the field name by reusing the key's `Arc` headers.name = occupied.key().clone(); occupied.into_mut().push(SavedField { headers, data }); - }, + } } self.fields_count = self.fields_count.saturating_add(1); @@ -686,7 +742,11 @@ impl Entries { for (idx, field) in entries.iter().enumerate() { let mut data = field.data.readable()?; let headers = &field.headers; - writeln!(writer, "{}: {:?} ({:?}):", idx, headers.filename, headers.content_type)?; + writeln!( + writer, + "{}: {:?} ({:?}):", + idx, headers.filename, headers.content_type + )?; io::copy(&mut data, &mut writer)?; } } @@ -907,7 +967,10 @@ impl EntriesSaveResult { } } -impl SaveResult where P: Into { +impl SaveResult +where + P: Into, +{ /// Convert `self` to `Option`; there may still have been an error. pub fn okish(self) -> Option { self.into_opt_both().0 @@ -915,7 +978,10 @@ impl SaveResult where P: Into { /// Map the `Full` or `Partial` values to a new type, retaining the reason /// in the `Partial` case. - pub fn map(self, map: Map) -> SaveResult where Map: FnOnce(S) -> T { + pub fn map(self, map: Map) -> SaveResult + where + Map: FnOnce(S) -> T, + { match self { Full(full) => Full(map(full)), Partial(partial, reason) => Partial(map(partial.into()), reason), @@ -926,7 +992,7 @@ impl SaveResult where P: Into { /// Decompose `self` to `(Option, Option)` pub fn into_opt_both(self) -> (Option, Option) { match self { - Full(full) => (Some(full), None), + Full(full) => (Some(full), None), Partial(partial, IoError(e)) => (Some(partial.into()), Some(e)), Partial(partial, _) => (Some(partial.into()), None), Error(error) => (None, Some(error)), @@ -964,23 +1030,35 @@ fn create_dir_all(path: &Path) -> io::Result<()> { fs::create_dir_all(parent) } else { // RFC: return an error instead? - warn!("Attempting to save file in what looks like a root directory. File path: {:?}", path); + warn!( + "Attempting to save file in what looks like a root directory. File path: {:?}", + path + ); Ok(()) } } -fn try_copy_limited SaveResult>(src: R, mut with_buf: Wb, limit: u64) -> SaveResult { +fn try_copy_limited SaveResult>( + src: R, + mut with_buf: Wb, + limit: u64, +) -> SaveResult { let mut copied = 0u64; try_read_buf(src, |buf| { let new_copied = copied.saturating_add(buf.len() as u64); - if new_copied > limit { return Partial(0, PartialReason::SizeLimit) } + if new_copied > limit { + return Partial(0, PartialReason::SizeLimit); + } copied = new_copied; with_buf(buf) }) } -fn try_read_buf SaveResult>(mut src: R, mut with_buf: Wb) -> SaveResult { +fn try_read_buf SaveResult>( + mut src: R, + mut with_buf: Wb, +) -> SaveResult { let mut total_copied = 0u64; macro_rules! try_here ( @@ -997,16 +1075,22 @@ fn try_read_buf SaveResult>(mut sr loop { let res = { let buf = try_here!(src.fill_buf()); - if buf.is_empty() { break; } + if buf.is_empty() { + break; + } with_buf(buf) }; match res { - Full(copied) => { src.consume(copied); total_copied += copied as u64; } + Full(copied) => { + src.consume(copied); + total_copied += copied as u64; + } Partial(copied, reason) => { - src.consume(copied); total_copied += copied as u64; + src.consume(copied); + total_copied += copied as u64; return Partial(total_copied, reason); - }, + } Error(err) => { return Partial(total_copied, err.into()); } @@ -1032,12 +1116,14 @@ fn try_write_all(mut buf: &[u8], mut dest: W) -> SaveResult try_here!(Err(io::Error::new(io::ErrorKind::WriteZero, - "failed to write whole buffer"))), + 0 => try_here!(Err(io::Error::new( + io::ErrorKind::WriteZero, + "failed to write whole buffer" + ))), copied => { buf = &buf[copied..]; total_copied += copied; - }, + } } } From 683c73ac441b4cda03d43b0f9dbb9b0b6934ac3c Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 14 Jun 2025 01:36:29 +0000 Subject: [PATCH 7/7] Rename `multipart` to `rouille-multipart` In preparation for republishing this crate as a version that suit Rouille's needs, rename it. --- Cargo.toml | 4 +- multipart/fuzz/Cargo.lock | 206 ------------------ {multipart => rouille-multipart}/.gitignore | 0 {multipart => rouille-multipart}/.travis.yml | 0 {multipart => rouille-multipart}/Cargo.toml | 10 +- {multipart => rouille-multipart}/LICENSE | 0 .../LICENSE-APACHE | 0 {multipart => rouille-multipart}/LICENSE-MIT | 0 {multipart => rouille-multipart}/README.md | 0 .../examples/README.md | 0 .../examples/hyper_client.rs | 0 .../examples/hyper_reqbuilder.rs | 0 .../examples/hyper_server.rs | 0 .../examples/iron.rs | 0 .../examples/iron_intercept.rs | 0 .../examples/nickel.rs | 0 .../examples/rocket.rs | 0 .../examples/tiny_http.rs | 0 .../fuzz/.gitignore | 0 .../fuzz/Cargo.toml | 0 .../fuzz/fuzzer_dict | 0 .../fuzz/fuzzers/logger.rs | 0 .../fuzz/fuzzers/server_basic.rs | 0 .../fuzz_server.sh | 0 .../lorem_ipsum.txt | 0 .../src/bin/form_test.rs | 0 .../src/bin/read_file.rs | 0 .../src/bin/test_form.html | 0 .../src/client/hyper.rs | 0 .../src/client/lazy.rs | 0 .../src/client/mod.rs | 0 .../src/client/sized.rs | 0 {multipart => rouille-multipart}/src/lib.rs | 0 .../src/local_test.rs | 0 {multipart => rouille-multipart}/src/mock.rs | 0 .../src/server/boundary.rs | 0 .../src/server/field.rs | 0 .../src/server/hyper.rs | 0 .../src/server/iron.rs | 0 .../src/server/mod.rs | 0 .../src/server/nickel.rs | 0 .../src/server/save.rs | 0 .../src/server/tiny_http.rs | 0 src/input/multipart.rs | 6 +- src/lib.rs | 2 +- 45 files changed, 7 insertions(+), 221 deletions(-) delete mode 100644 multipart/fuzz/Cargo.lock rename {multipart => rouille-multipart}/.gitignore (100%) rename {multipart => rouille-multipart}/.travis.yml (100%) rename {multipart => rouille-multipart}/Cargo.toml (98%) rename {multipart => rouille-multipart}/LICENSE (100%) rename {multipart => rouille-multipart}/LICENSE-APACHE (100%) rename {multipart => rouille-multipart}/LICENSE-MIT (100%) rename {multipart => rouille-multipart}/README.md (100%) rename {multipart => rouille-multipart}/examples/README.md (100%) rename {multipart => rouille-multipart}/examples/hyper_client.rs (100%) rename {multipart => rouille-multipart}/examples/hyper_reqbuilder.rs (100%) rename {multipart => rouille-multipart}/examples/hyper_server.rs (100%) rename {multipart => rouille-multipart}/examples/iron.rs (100%) rename {multipart => rouille-multipart}/examples/iron_intercept.rs (100%) rename {multipart => rouille-multipart}/examples/nickel.rs (100%) rename {multipart => rouille-multipart}/examples/rocket.rs (100%) rename {multipart => rouille-multipart}/examples/tiny_http.rs (100%) rename {multipart => rouille-multipart}/fuzz/.gitignore (100%) rename {multipart => rouille-multipart}/fuzz/Cargo.toml (100%) rename {multipart => rouille-multipart}/fuzz/fuzzer_dict (100%) rename {multipart => rouille-multipart}/fuzz/fuzzers/logger.rs (100%) rename {multipart => rouille-multipart}/fuzz/fuzzers/server_basic.rs (100%) rename {multipart => rouille-multipart}/fuzz_server.sh (100%) rename {multipart => rouille-multipart}/lorem_ipsum.txt (100%) rename {multipart => rouille-multipart}/src/bin/form_test.rs (100%) rename {multipart => rouille-multipart}/src/bin/read_file.rs (100%) rename {multipart => rouille-multipart}/src/bin/test_form.html (100%) rename {multipart => rouille-multipart}/src/client/hyper.rs (100%) rename {multipart => rouille-multipart}/src/client/lazy.rs (100%) rename {multipart => rouille-multipart}/src/client/mod.rs (100%) rename {multipart => rouille-multipart}/src/client/sized.rs (100%) rename {multipart => rouille-multipart}/src/lib.rs (100%) rename {multipart => rouille-multipart}/src/local_test.rs (100%) rename {multipart => rouille-multipart}/src/mock.rs (100%) rename {multipart => rouille-multipart}/src/server/boundary.rs (100%) rename {multipart => rouille-multipart}/src/server/field.rs (100%) rename {multipart => rouille-multipart}/src/server/hyper.rs (100%) rename {multipart => rouille-multipart}/src/server/iron.rs (100%) rename {multipart => rouille-multipart}/src/server/mod.rs (100%) rename {multipart => rouille-multipart}/src/server/nickel.rs (100%) rename {multipart => rouille-multipart}/src/server/save.rs (100%) rename {multipart => rouille-multipart}/src/server/tiny_http.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 2945ed388..13050dac0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ brotli = { version = "3.3.2", optional = true } chrono = { version = "0.4.19", default-features = false, features = ["clock"] } filetime = "0.2.0" deflate = { version = "1.0.0", optional = true, features = ["gzip"] } -multipart = { version = "0.18", path = "multipart", default-features = false, features = ["server"] } +rouille-multipart = { version = "0.18", path = "rouille-multipart", default-features = false, features = ["server"] } percent-encoding = "2" rand = "0.8" serde = "1" @@ -39,4 +39,4 @@ postgres = { version = "0.19", default-features = false } log = "0.4" [workspace] -members = ["multipart"] +members = ["rouille-multipart"] diff --git a/multipart/fuzz/Cargo.lock b/multipart/fuzz/Cargo.lock deleted file mode 100644 index 748229dd6..000000000 --- a/multipart/fuzz/Cargo.lock +++ /dev/null @@ -1,206 +0,0 @@ -[root] -name = "multipart-fuzz" -version = "0.0.1" -dependencies = [ - "libfuzzer-sys 0.1.0 (git+https://github.com/rust-fuzz/libfuzzer-sys.git)", - "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "multipart 0.11.0", -] - -[[package]] -name = "buf_redux" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "safemem 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "gcc" -version = "0.3.45" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "httparse" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "libc" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "libfuzzer-sys" -version = "0.1.0" -source = "git+https://github.com/rust-fuzz/libfuzzer-sys.git#36a3928eef5c3c38eb0f251962395bb510c39d46" -dependencies = [ - "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "log" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "memchr" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "mime" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "mime_guess" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "mime 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", - "phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "multipart" -version = "0.11.0" -dependencies = [ - "buf_redux 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "mime_guess 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "twoway 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "phf" -version = "0.7.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "phf_codegen" -version = "0.7.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", - "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "phf_generator" -version = "0.7.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "phf_shared" -version = "0.7.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rustc_version" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "safemem" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "safemem" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "semver" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "siphasher" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "tempdir" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "twoway" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "unicase" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[metadata] -"checksum buf_redux 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e1497634c131ba13483b6e8123f69e219253b018bb32949eefd55c6b5051585d" -"checksum gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)" = "40899336fb50db0c78710f53e87afc54d8c7266fb76262fecc78ca1a7f09deae" -"checksum httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e7a63e511f9edffbab707141fbb8707d1a3098615fb2adbd5769cdfcc9b17d" -"checksum libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "88ee81885f9f04bff991e306fea7c1c60a5f0f9e409e99f6b40e3311a3363135" -"checksum libfuzzer-sys 0.1.0 (git+https://github.com/rust-fuzz/libfuzzer-sys.git)" = "" -"checksum log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5141eca02775a762cc6cd564d8d2c50f67c0ea3a372cbf1c51592b3e029e10ad" -"checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" -"checksum mime 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5514f038123342d01ee5f95129e4ef1e0470c93bc29edf058a46f9ee3ba6737e" -"checksum mime_guess 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76da6df85047af8c0edfa53f48eb1073012ce1cc95c8fedc0a374f659a89dd65" -"checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc" -"checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f" -"checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03" -"checksum phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "07e24b0ca9643bdecd0632f2b3da6b1b89bbb0030e0b992afc1113b23a7bc2f2" -"checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d" -"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" -"checksum safemem 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "725b3bf47ae40b4abcd27b5f0a9540369426a29f7b905649b3e1468e13e22009" -"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" -"checksum semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" -"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537" -"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" -"checksum twoway 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e267e178055eb3b081224bbef62d4f508ae3c9f000b6ae6ccdb04a0d9c34b77f" -"checksum unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a5906ca2b98c799f4b1ab4557b76367ebd6ae5ef14930ec841c74aed5f3764" diff --git a/multipart/.gitignore b/rouille-multipart/.gitignore similarity index 100% rename from multipart/.gitignore rename to rouille-multipart/.gitignore diff --git a/multipart/.travis.yml b/rouille-multipart/.travis.yml similarity index 100% rename from multipart/.travis.yml rename to rouille-multipart/.travis.yml diff --git a/multipart/Cargo.toml b/rouille-multipart/Cargo.toml similarity index 98% rename from multipart/Cargo.toml rename to rouille-multipart/Cargo.toml index 7f3dadbec..7511a801b 100644 --- a/multipart/Cargo.toml +++ b/rouille-multipart/Cargo.toml @@ -1,20 +1,12 @@ [package] -name = "multipart" - +name = "rouille-multipart" version = "0.18.0" - authors = ["Austin Bonander "] - description = "A backend-agnostic extension for HTTP libraries that provides support for POST multipart/form-data requests on both client and server." - keywords = ["form-data", "hyper", "iron", "http", "upload"] - repository = "http://github.com/abonander/multipart" - documentation = "http://docs.rs/multipart/" - license = "MIT OR Apache-2.0" - readme = "README.md" autobins = false edition = "2021" diff --git a/multipart/LICENSE b/rouille-multipart/LICENSE similarity index 100% rename from multipart/LICENSE rename to rouille-multipart/LICENSE diff --git a/multipart/LICENSE-APACHE b/rouille-multipart/LICENSE-APACHE similarity index 100% rename from multipart/LICENSE-APACHE rename to rouille-multipart/LICENSE-APACHE diff --git a/multipart/LICENSE-MIT b/rouille-multipart/LICENSE-MIT similarity index 100% rename from multipart/LICENSE-MIT rename to rouille-multipart/LICENSE-MIT diff --git a/multipart/README.md b/rouille-multipart/README.md similarity index 100% rename from multipart/README.md rename to rouille-multipart/README.md diff --git a/multipart/examples/README.md b/rouille-multipart/examples/README.md similarity index 100% rename from multipart/examples/README.md rename to rouille-multipart/examples/README.md diff --git a/multipart/examples/hyper_client.rs b/rouille-multipart/examples/hyper_client.rs similarity index 100% rename from multipart/examples/hyper_client.rs rename to rouille-multipart/examples/hyper_client.rs diff --git a/multipart/examples/hyper_reqbuilder.rs b/rouille-multipart/examples/hyper_reqbuilder.rs similarity index 100% rename from multipart/examples/hyper_reqbuilder.rs rename to rouille-multipart/examples/hyper_reqbuilder.rs diff --git a/multipart/examples/hyper_server.rs b/rouille-multipart/examples/hyper_server.rs similarity index 100% rename from multipart/examples/hyper_server.rs rename to rouille-multipart/examples/hyper_server.rs diff --git a/multipart/examples/iron.rs b/rouille-multipart/examples/iron.rs similarity index 100% rename from multipart/examples/iron.rs rename to rouille-multipart/examples/iron.rs diff --git a/multipart/examples/iron_intercept.rs b/rouille-multipart/examples/iron_intercept.rs similarity index 100% rename from multipart/examples/iron_intercept.rs rename to rouille-multipart/examples/iron_intercept.rs diff --git a/multipart/examples/nickel.rs b/rouille-multipart/examples/nickel.rs similarity index 100% rename from multipart/examples/nickel.rs rename to rouille-multipart/examples/nickel.rs diff --git a/multipart/examples/rocket.rs b/rouille-multipart/examples/rocket.rs similarity index 100% rename from multipart/examples/rocket.rs rename to rouille-multipart/examples/rocket.rs diff --git a/multipart/examples/tiny_http.rs b/rouille-multipart/examples/tiny_http.rs similarity index 100% rename from multipart/examples/tiny_http.rs rename to rouille-multipart/examples/tiny_http.rs diff --git a/multipart/fuzz/.gitignore b/rouille-multipart/fuzz/.gitignore similarity index 100% rename from multipart/fuzz/.gitignore rename to rouille-multipart/fuzz/.gitignore diff --git a/multipart/fuzz/Cargo.toml b/rouille-multipart/fuzz/Cargo.toml similarity index 100% rename from multipart/fuzz/Cargo.toml rename to rouille-multipart/fuzz/Cargo.toml diff --git a/multipart/fuzz/fuzzer_dict b/rouille-multipart/fuzz/fuzzer_dict similarity index 100% rename from multipart/fuzz/fuzzer_dict rename to rouille-multipart/fuzz/fuzzer_dict diff --git a/multipart/fuzz/fuzzers/logger.rs b/rouille-multipart/fuzz/fuzzers/logger.rs similarity index 100% rename from multipart/fuzz/fuzzers/logger.rs rename to rouille-multipart/fuzz/fuzzers/logger.rs diff --git a/multipart/fuzz/fuzzers/server_basic.rs b/rouille-multipart/fuzz/fuzzers/server_basic.rs similarity index 100% rename from multipart/fuzz/fuzzers/server_basic.rs rename to rouille-multipart/fuzz/fuzzers/server_basic.rs diff --git a/multipart/fuzz_server.sh b/rouille-multipart/fuzz_server.sh similarity index 100% rename from multipart/fuzz_server.sh rename to rouille-multipart/fuzz_server.sh diff --git a/multipart/lorem_ipsum.txt b/rouille-multipart/lorem_ipsum.txt similarity index 100% rename from multipart/lorem_ipsum.txt rename to rouille-multipart/lorem_ipsum.txt diff --git a/multipart/src/bin/form_test.rs b/rouille-multipart/src/bin/form_test.rs similarity index 100% rename from multipart/src/bin/form_test.rs rename to rouille-multipart/src/bin/form_test.rs diff --git a/multipart/src/bin/read_file.rs b/rouille-multipart/src/bin/read_file.rs similarity index 100% rename from multipart/src/bin/read_file.rs rename to rouille-multipart/src/bin/read_file.rs diff --git a/multipart/src/bin/test_form.html b/rouille-multipart/src/bin/test_form.html similarity index 100% rename from multipart/src/bin/test_form.html rename to rouille-multipart/src/bin/test_form.html diff --git a/multipart/src/client/hyper.rs b/rouille-multipart/src/client/hyper.rs similarity index 100% rename from multipart/src/client/hyper.rs rename to rouille-multipart/src/client/hyper.rs diff --git a/multipart/src/client/lazy.rs b/rouille-multipart/src/client/lazy.rs similarity index 100% rename from multipart/src/client/lazy.rs rename to rouille-multipart/src/client/lazy.rs diff --git a/multipart/src/client/mod.rs b/rouille-multipart/src/client/mod.rs similarity index 100% rename from multipart/src/client/mod.rs rename to rouille-multipart/src/client/mod.rs diff --git a/multipart/src/client/sized.rs b/rouille-multipart/src/client/sized.rs similarity index 100% rename from multipart/src/client/sized.rs rename to rouille-multipart/src/client/sized.rs diff --git a/multipart/src/lib.rs b/rouille-multipart/src/lib.rs similarity index 100% rename from multipart/src/lib.rs rename to rouille-multipart/src/lib.rs diff --git a/multipart/src/local_test.rs b/rouille-multipart/src/local_test.rs similarity index 100% rename from multipart/src/local_test.rs rename to rouille-multipart/src/local_test.rs diff --git a/multipart/src/mock.rs b/rouille-multipart/src/mock.rs similarity index 100% rename from multipart/src/mock.rs rename to rouille-multipart/src/mock.rs diff --git a/multipart/src/server/boundary.rs b/rouille-multipart/src/server/boundary.rs similarity index 100% rename from multipart/src/server/boundary.rs rename to rouille-multipart/src/server/boundary.rs diff --git a/multipart/src/server/field.rs b/rouille-multipart/src/server/field.rs similarity index 100% rename from multipart/src/server/field.rs rename to rouille-multipart/src/server/field.rs diff --git a/multipart/src/server/hyper.rs b/rouille-multipart/src/server/hyper.rs similarity index 100% rename from multipart/src/server/hyper.rs rename to rouille-multipart/src/server/hyper.rs diff --git a/multipart/src/server/iron.rs b/rouille-multipart/src/server/iron.rs similarity index 100% rename from multipart/src/server/iron.rs rename to rouille-multipart/src/server/iron.rs diff --git a/multipart/src/server/mod.rs b/rouille-multipart/src/server/mod.rs similarity index 100% rename from multipart/src/server/mod.rs rename to rouille-multipart/src/server/mod.rs diff --git a/multipart/src/server/nickel.rs b/rouille-multipart/src/server/nickel.rs similarity index 100% rename from multipart/src/server/nickel.rs rename to rouille-multipart/src/server/nickel.rs diff --git a/multipart/src/server/save.rs b/rouille-multipart/src/server/save.rs similarity index 100% rename from multipart/src/server/save.rs rename to rouille-multipart/src/server/save.rs diff --git a/multipart/src/server/tiny_http.rs b/rouille-multipart/src/server/tiny_http.rs similarity index 100% rename from multipart/src/server/tiny_http.rs rename to rouille-multipart/src/server/tiny_http.rs diff --git a/src/input/multipart.rs b/src/input/multipart.rs index b15bba188..da2b72ba2 100644 --- a/src/input/multipart.rs +++ b/src/input/multipart.rs @@ -18,11 +18,11 @@ use std::fmt; use Request; use RequestBody; -use multipart::server::Multipart as InnerMultipart; +use rouille_multipart::server::Multipart as InnerMultipart; // TODO: provide wrappers around these -pub use multipart::server::MultipartData; -pub use multipart::server::MultipartField; +pub use rouille_multipart::server::MultipartData; +pub use rouille_multipart::server::MultipartField; /// Error that can happen when decoding multipart data. #[derive(Clone, Debug)] diff --git a/src/lib.rs b/src/lib.rs index 37cf6cca6..21e8040b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,8 +63,8 @@ extern crate chrono; #[cfg(feature = "gzip")] extern crate deflate; extern crate filetime; -extern crate multipart; extern crate rand; +extern crate rouille_multipart; extern crate serde; #[macro_use] extern crate serde_derive;