├── .gitattributes ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── hyper.rs ├── tiny_http.rs └── warp.rs ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ └── fuzz_form_data.rs ├── src ├── async.rs ├── error.rs ├── field.rs ├── form.rs ├── lib.rs ├── limits.rs ├── state.rs ├── sync.rs └── utils.rs └── tests ├── fixtures ├── empty.txt ├── filename-with-space.txt ├── files │ ├── empty.dat │ ├── large.jpg │ ├── medium.dat │ ├── small0.dat │ ├── small1.dat │ ├── tiny0.dat │ └── tiny1.dat ├── graphql.txt ├── headers.txt ├── issue-6.txt ├── many-noend.txt ├── many.txt ├── rfc7578-example.txt ├── sample.lf.txt └── sample.txt ├── form-data.rs ├── hyper-body.rs ├── lib ├── incoming_body.rs ├── limited.rs └── mod.rs └── tiny-body.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/fixtures/*.txt text eol=crlf 2 | tests/fixtures/*.lf.txt text eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | CARGO_INCREMENTAL: 0 11 | CARGO_NET_RETRY: 10 12 | CARGO_TERM_COLOR: always 13 | RUST_BACKTRACE: 1 14 | RUSTFLAGS: -D warnings 15 | RUSTUP_MAX_RETRIES: 10 16 | 17 | jobs: 18 | test: 19 | name: Test (${{ matrix.os }}) 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | os: 24 | - ubuntu-latest 25 | - macos-latest 26 | - windows-latest 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: dtolnay/rust-toolchain@nightly 31 | - run: cargo test --test form-data --test hyper-body 32 | - run: cargo test --test tiny-body --features="sync" --no-default-features 33 | 34 | clippy: 35 | name: Clippy 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: dtolnay/rust-toolchain@clippy 40 | - run: cargo clippy --tests -- -Dclippy::all -Dclippy::pedantic 41 | 42 | fmt: 43 | name: Fmt 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: dtolnay/rust-toolchain@stable 48 | - run: cargo fmt --all -- --check 49 | 50 | docs: 51 | name: Doc 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v4 55 | - uses: dtolnay/rust-toolchain@nightly 56 | - run: RUSTDOCFLAGS="-D warnings --cfg docsrs" cargo doc --no-deps 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | tests/fixtures/tmp/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "form-data" 3 | version = "0.6.0" 4 | authors = ["Fangdun Tsai "] 5 | description = "AsyncRead/AsyncWrite/Stream `multipart/form-data`" 6 | repository = "https://github.com/viz-rs/form-data" 7 | keywords = ["async", "form-data", "multipart", "http", "hyper"] 8 | categories = ["asynchronous", "web-programming", "web-programming::http-server"] 9 | license = "MIT OR Apache-2.0" 10 | readme = "README.md" 11 | edition = "2021" 12 | 13 | include = [ 14 | "Cargo.toml", 15 | "LICENSE-APACHE", 16 | "LICENSE-MIT", 17 | "README.md", 18 | "src/*.rs", 19 | ] 20 | 21 | [features] 22 | default = ["async"] 23 | 24 | async = ["futures-util/io"] 25 | sync = [] 26 | 27 | [dependencies] 28 | bytes = "1.9" 29 | http = "1.2" 30 | httparse = "1.9" 31 | mime = "0.3" 32 | memchr = "2.7" 33 | tracing = "0.1" 34 | thiserror = "2.0" 35 | serde = { version = "1.0", features = ["derive"] } 36 | 37 | [dependencies.futures-util] 38 | version = "0.3" 39 | default-features = false 40 | optional = true 41 | 42 | [dev-dependencies] 43 | anyhow = "1.0" 44 | async-fs = "2.1" 45 | http-body = "1.0" 46 | http-body-util = "0.1" 47 | hyper = { version = "1.5", features = ["server", "http1"] } 48 | hyper-util = { version = "0.1", features = ["tokio"] } 49 | rand = "0.8" 50 | tempfile = "3.14" 51 | tiny_http = "0.12" 52 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 53 | tokio-util = { version = "0.7", features = ["io"] } 54 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 55 | # warp = "0.3" 56 | 57 | [[example]] 58 | name = "hyper" 59 | path = "examples/hyper.rs" 60 | 61 | #[[example]] 62 | #name = "warp" 63 | #path = "examples/warp.rs" 64 | 65 | [[example]] 66 | name = "tiny_http" 67 | path = "examples/tiny_http.rs" 68 | required-features = ["sync"] 69 | 70 | [[test]] 71 | name = "form-data" 72 | path = "tests/form-data.rs" 73 | required-features = ["async"] 74 | 75 | [[test]] 76 | name = "hyper-body" 77 | path = "tests/hyper-body.rs" 78 | required-features = ["async"] 79 | 80 | [[test]] 81 | name = "tiny-body" 82 | path = "tests/tiny-body.rs" 83 | required-features = ["sync"] 84 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019-present Fangdun Tsai 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Fangdun Tsai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

form-data

2 | 3 |
4 |

AsyncRead/AsyncWrite/Stream for `multipart/form-data` rfc7578

5 |
6 | 7 |
8 | 9 | 10 | Safety! 12 | 13 | 14 | Docs.rs docs 16 | 17 | 18 | Crates.io version 20 | 21 | 22 | Download 24 | 25 | 26 | Twitter: @_fundon 28 |
29 | 30 | ## Features 31 | 32 | - **Stream**: `Form`, `Field` 33 | 34 | - **AsyncRead/Read**: `Field`, so easy `read`/`copy` field data to anywhere. 35 | 36 | - **Fast**: Hyper supports bigger buffer by defaults, over 8KB, up to 512KB possible. 37 | 38 | AsyncRead is limited to **8KB**. So if we want to receive large buffer, 39 | and save them to writer or file. See [hyper example](examples/hyper.rs): 40 | 41 | - Set `max_buf_size` to FormData, `form_data.set_max_buf_size(512 * 1024)?;` 42 | 43 | - Use `copy_to`, copy bigger buffer to a writer(`AsyncRead`), `field.copy_to(&mut writer)` 44 | 45 | - Use `copy_to_file`, copy bigger buffer to a file(`File`), `field.copy_to_file(&mut file)` 46 | 47 | - Preparse headers of part 48 | 49 | ## Example 50 | 51 | Request payload, the example from [jaydenseric/graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec#request-payload-2). 52 | 53 | ```txt 54 | --------------------------627436eaefdbc285 55 | Content-Disposition: form-data; name="operations" 56 | 57 | [{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }, { "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }] 58 | --------------------------627436eaefdbc285 59 | Content-Disposition: form-data; name="map" 60 | 61 | { "0": ["0.variables.file"], "1": ["1.variables.files.0"], "2": ["1.variables.files.1"] } 62 | --------------------------627436eaefdbc285 63 | Content-Disposition: form-data; name="0"; filename="a.txt" 64 | Content-Type: text/plain 65 | 66 | Alpha file content. 67 | 68 | --------------------------627436eaefdbc285 69 | Content-Disposition: form-data; name="1"; filename="b.txt" 70 | Content-Type: text/plain 71 | 72 | Bravo file content. 73 | 74 | --------------------------627436eaefdbc285 75 | Content-Disposition: form-data; name="2"; filename="c.txt" 76 | Content-Type: text/plain 77 | 78 | Charlie file content. 79 | 80 | --------------------------627436eaefdbc285-- 81 | ``` 82 | 83 | [tests/hyper-body.rs](hyper-body) 84 | 85 | ```rust 86 | use anyhow::Result; 87 | use async_fs::File; 88 | use bytes::BytesMut; 89 | use tempfile::tempdir; 90 | 91 | use futures_util::{ 92 | io::{self, AsyncReadExt, AsyncWriteExt}, 93 | stream::{self, TryStreamExt}, 94 | }; 95 | use http_body_util::StreamBody; 96 | 97 | use form_data::*; 98 | 99 | #[path = "./lib/mod.rs"] 100 | mod lib; 101 | 102 | use lib::{tracing_init, Limited}; 103 | 104 | #[tokio::test] 105 | async fn hyper_body() -> Result<()> { 106 | tracing_init()?; 107 | 108 | let payload = File::open("tests/fixtures/graphql.txt").await?; 109 | let stream = Limited::random_with(payload, 256); 110 | let limit = stream.limit(); 111 | 112 | let body = StreamBody::new(stream); 113 | let mut form = FormData::new(body, "------------------------627436eaefdbc285"); 114 | form.set_max_buf_size(limit)?; 115 | 116 | while let Some(mut field) = form.try_next().await? { 117 | assert!(!field.consumed()); 118 | assert_eq!(field.length, 0); 119 | 120 | match field.index { 121 | 0 => { 122 | assert_eq!(field.name, "operations"); 123 | assert_eq!(field.filename, None); 124 | assert_eq!(field.content_type, None); 125 | 126 | // reads chunks 127 | let mut buffer = BytesMut::new(); 128 | while let Some(buf) = field.try_next().await? { 129 | buffer.extend_from_slice(&buf); 130 | } 131 | 132 | assert_eq!(buffer, "[{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }, { \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }]"); 133 | assert_eq!(field.length, buffer.len()); 134 | 135 | assert!(field.consumed()); 136 | 137 | tracing::info!("{:#?}", field); 138 | } 139 | 1 => { 140 | assert_eq!(field.name, "map"); 141 | assert_eq!(field.filename, None); 142 | assert_eq!(field.content_type, None); 143 | 144 | // reads bytes 145 | let buffer = field.bytes().await?; 146 | 147 | assert_eq!(buffer, "{ \"0\": [\"0.variables.file\"], \"1\": [\"1.variables.files.0\"], \"2\": [\"1.variables.files.1\"] }"); 148 | assert_eq!(field.length, buffer.len()); 149 | 150 | assert!(field.consumed()); 151 | 152 | tracing::info!("{:#?}", field); 153 | } 154 | 2 => { 155 | tracing::info!("{:#?}", field); 156 | 157 | assert_eq!(field.name, "0"); 158 | assert_eq!(field.filename, Some("a.txt".into())); 159 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 160 | 161 | let dir = tempdir()?; 162 | 163 | let filename = field.filename.as_ref().unwrap(); 164 | let filepath = dir.path().join(filename); 165 | 166 | let mut writer = File::create(&filepath).await?; 167 | 168 | let bytes = io::copy(field, &mut writer).await?; 169 | writer.close().await?; 170 | 171 | // async ? 172 | let metadata = std::fs::metadata(&filepath)?; 173 | assert_eq!(metadata.len(), bytes); 174 | 175 | let mut reader = File::open(&filepath).await?; 176 | let mut contents = Vec::new(); 177 | reader.read_to_end(&mut contents).await?; 178 | assert_eq!(contents, "Alpha file content.\r\n".as_bytes()); 179 | 180 | dir.close()?; 181 | } 182 | 3 => { 183 | assert_eq!(field.name, "1"); 184 | assert_eq!(field.filename, Some("b.txt".into())); 185 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 186 | 187 | let mut buffer = Vec::with_capacity(4); 188 | let bytes = field.read_to_end(&mut buffer).await?; 189 | 190 | assert_eq!(buffer, "Bravo file content.\r\n".as_bytes()); 191 | assert_eq!(field.length, bytes); 192 | assert_eq!(field.length, buffer.len()); 193 | 194 | tracing::info!("{:#?}", field); 195 | } 196 | 4 => { 197 | assert_eq!(field.name, "2"); 198 | assert_eq!(field.filename, Some("c.txt".into())); 199 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 200 | 201 | let mut string = String::new(); 202 | let bytes = field.read_to_string(&mut string).await?; 203 | 204 | assert_eq!(string, "Charlie file content.\r\n"); 205 | assert_eq!(field.length, bytes); 206 | assert_eq!(field.length, string.len()); 207 | 208 | tracing::info!("{:#?}", field); 209 | } 210 | _ => {} 211 | } 212 | } 213 | 214 | let state = form.state(); 215 | let state = state 216 | .try_lock() 217 | .map_err(|e| Error::TryLockError(e.to_string()))?; 218 | 219 | assert!(state.eof()); 220 | assert_eq!(state.total(), 5); 221 | assert_eq!(state.len(), 1027); 222 | 223 | Ok(()) 224 | } 225 | ``` 226 | 227 | ## License 228 | 229 | 230 | Licensed under either of Apache License, Version 231 | 2.0 or MIT license at your option. 232 | 233 | 234 |
235 | 236 | 237 | Unless you explicitly state otherwise, any contribution intentionally submitted 238 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 239 | be dual licensed as above, without any additional terms or conditions. 240 | 241 | -------------------------------------------------------------------------------- /examples/hyper.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Run with 3 | //! 4 | //! Max buffer size is 8KB by defaults. 5 | //! 6 | //! ``` 7 | //! // 8KB 8 | //! $ RUST_LOG=info cargo run --example hyper -- --nocapture --size=8 9 | //! 10 | //! // 64KB 11 | //! $ RUST_LOG=info cargo run --example hyper -- --nocapture --size=64 12 | //! 13 | //! // 512KB 14 | //! $ RUST_LOG=info cargo run --example hyper -- --nocapture --size=512 15 | //! ``` 16 | //! 17 | //! Fish shell 18 | //! ``` 19 | //! $ set files tests/fixtures/files/*; for i in (seq (count $files) | sort -R); echo "-F "(string split . (basename $files[$i]))[1]=@$files[$i]; end | string join ' ' | xargs time curl -vvv http://127.0.0.1:3000 -F crate=form-data 20 | //! ``` 21 | 22 | #![deny(warnings)] 23 | 24 | use std::{env, net::SocketAddr}; 25 | 26 | use anyhow::Result; 27 | use async_fs::File; 28 | use bytes::Bytes; 29 | use futures_util::{ 30 | io::{copy, AsyncWriteExt}, 31 | stream::TryStreamExt, 32 | }; 33 | use http_body_util::Full; 34 | use hyper::{body::Incoming, header, server::conn::http1, service::service_fn, Request, Response}; 35 | use hyper_util::rt::TokioIo; 36 | use tempfile::tempdir; 37 | use tokio::net::TcpListener; 38 | 39 | use form_data::{Error, FormData}; 40 | 41 | #[path = "../tests/lib/mod.rs"] 42 | mod lib; 43 | 44 | use lib::IncomingBody; 45 | 46 | async fn hello(size: usize, req: Request) -> Result>, Error> { 47 | let dir = tempdir()?; 48 | let mut txt = String::new(); 49 | 50 | txt.push_str(&dir.path().to_string_lossy()); 51 | txt.push_str("\r\n"); 52 | 53 | let m = req 54 | .headers() 55 | .get(header::CONTENT_TYPE) 56 | .and_then(|val| val.to_str().ok()) 57 | .and_then(|val| val.parse::().ok()) 58 | .ok_or(Error::InvalidHeader)?; 59 | 60 | let mut form = FormData::new( 61 | req.map(|body| IncomingBody::new(Some(body))).into_body(), 62 | m.get_param(mime::BOUNDARY).unwrap().as_str(), 63 | ); 64 | 65 | // 512KB for hyper lager buffer 66 | form.set_max_buf_size(size)?; 67 | 68 | while let Some(mut field) = form.try_next().await? { 69 | let name = field.name.to_owned(); 70 | let mut bytes: u64 = 0; 71 | 72 | assert_eq!(bytes as usize, field.length); 73 | 74 | if let Some(filename) = &field.filename { 75 | let filepath = dir.path().join(filename); 76 | 77 | match filepath.extension().and_then(|s| s.to_str()) { 78 | Some("txt") => { 79 | // buffer <= 8KB 80 | let mut writer = File::create(&filepath).await?; 81 | bytes = copy(&mut field, &mut writer).await?; 82 | writer.close().await?; 83 | } 84 | Some("iso") => { 85 | field.ignore().await?; 86 | } 87 | _ => { 88 | // 8KB <= buffer <= 512KB 89 | // let mut writer = File::create(&filepath).await?; 90 | // bytes = field.copy_to(&mut writer).await?; 91 | 92 | let mut writer = std::fs::File::create(&filepath)?; 93 | bytes = field.copy_to_file(&mut writer).await?; 94 | } 95 | } 96 | 97 | tracing::info!("file {} {}", name, bytes); 98 | txt.push_str(&format!("file {name} {bytes}\r\n")); 99 | } else { 100 | let buffer = field.bytes().await?; 101 | bytes = buffer.len() as u64; 102 | tracing::info!("text {} {}", name, bytes); 103 | txt.push_str(&format!("text {name} {bytes}\r\n")); 104 | } 105 | 106 | tracing::info!("{:?}", field); 107 | 108 | assert_eq!( 109 | bytes, 110 | match name.as_str() { 111 | "empty" => 0, 112 | "tiny1" => 7, 113 | "tiny0" => 122, 114 | "small1" => 315, 115 | "small0" => 1_778, 116 | "medium" => 13_196, 117 | "large" => 2_413_677, 118 | "book" => 400_797_393, 119 | "crate" => 9, 120 | _ => bytes, 121 | } 122 | ); 123 | } 124 | 125 | dir.close()?; 126 | 127 | Ok(Response::new(Full::from(Into::::into(txt)))) 128 | } 129 | 130 | #[tokio::main] 131 | pub async fn main() -> Result<(), Box> { 132 | tracing_subscriber::fmt() 133 | // From env var: `RUST_LOG` 134 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 135 | .try_init() 136 | .map_err(|e| anyhow::anyhow!(e))?; 137 | 138 | let mut arg = env::args() 139 | .find(|a| a.starts_with("--size=")) 140 | .unwrap_or_else(|| "--size=8".to_string()); 141 | 142 | // 512 143 | // 8 * 2 144 | // 8 145 | let size = arg.split_off(7).parse::().unwrap_or(8) * 1024; 146 | let addr: SocketAddr = ([127, 0, 0, 1], 3000).into(); 147 | 148 | println!("Listening on http://{addr}"); 149 | println!("FormData max buffer size is {}KB", size / 1024); 150 | 151 | let listener = TcpListener::bind(addr).await?; 152 | 153 | loop { 154 | let (stream, _) = listener.accept().await?; 155 | 156 | tokio::task::spawn(async move { 157 | if let Err(err) = http1::Builder::new() 158 | .max_buf_size(size) 159 | .serve_connection( 160 | TokioIo::new(stream), 161 | service_fn(|req: Request| hello(size, req)), 162 | ) 163 | .await 164 | { 165 | println!("Error serving connection: {err:?}"); 166 | } 167 | }); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /examples/tiny_http.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | fs::File, 4 | io::{copy, Cursor, Read, Write}, 5 | sync::Arc, 6 | thread::spawn, 7 | }; 8 | 9 | use anyhow::Result; 10 | use form_data::{Field, FormData}; 11 | use tempfile::tempdir; 12 | use tiny_http::{Header, Response, Server}; 13 | 14 | fn hello(size: usize, boundary: &str, reader: &mut dyn Read) -> Result>>> { 15 | let dir = tempdir()?; 16 | let mut txt = String::new(); 17 | 18 | txt.push_str(&dir.path().to_string_lossy()); 19 | txt.push_str("\r\n"); 20 | 21 | let mut form = FormData::new(reader, boundary); 22 | 23 | // 512KB for hyper lager buffer 24 | form.set_max_buf_size(size)?; 25 | 26 | while let Some(item) = form.next() { 27 | let mut field = item?; 28 | let name = field.name.to_owned(); 29 | let mut bytes: u64 = 0; 30 | 31 | assert_eq!(bytes as usize, field.length); 32 | 33 | if let Some(filename) = &field.filename { 34 | let filepath = dir.path().join(filename); 35 | 36 | match filepath.extension().and_then(|s| s.to_str()) { 37 | Some("txt") => { 38 | // buffer <= 8KB 39 | let mut writer = File::create(&filepath)?; 40 | bytes = copy(&mut field, &mut writer)?; 41 | writer.flush()?; 42 | } 43 | Some("iso") => { 44 | field.ignore()?; 45 | } 46 | _ => { 47 | // 8KB <= buffer <= 512KB 48 | // let mut writer = File::create(&filepath).await?; 49 | // bytes = field.copy_to(&mut writer).await?; 50 | 51 | let mut writer = File::create(&filepath)?; 52 | bytes = field.copy_to_file(&mut writer)?; 53 | } 54 | } 55 | 56 | tracing::info!("file {} {}", name, bytes); 57 | txt.push_str(&format!("file {} {}\r\n", name, bytes)); 58 | } else { 59 | let buffer = Field::bytes(&mut field)?; 60 | bytes = buffer.len() as u64; 61 | tracing::info!("text {} {}", name, bytes); 62 | txt.push_str(&format!("text {} {}\r\n", name, bytes)); 63 | } 64 | 65 | tracing::info!("{:?}", field); 66 | 67 | assert_eq!( 68 | bytes, 69 | match name.as_str() { 70 | "empty" => 0, 71 | "tiny1" => 7, 72 | "tiny0" => 122, 73 | "small1" => 315, 74 | "small0" => 1_778, 75 | "medium" => 13_196, 76 | "large" => 2_413_677, 77 | "book" => 400_797_393, 78 | "crate" => 9, 79 | _ => bytes, 80 | } 81 | ); 82 | } 83 | 84 | dir.close()?; 85 | 86 | Ok(Response::from_string(txt)) 87 | } 88 | 89 | fn main() -> Result<()> { 90 | tracing_subscriber::fmt() 91 | // From env var: `RUST_LOG` 92 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 93 | .try_init() 94 | .map_err(|e| anyhow::anyhow!(e))?; 95 | 96 | let mut arg = env::args() 97 | .find(|a| a.starts_with("--size=")) 98 | .unwrap_or_else(|| "--size=8".to_string()); 99 | 100 | // 512 101 | // 8 * 2 102 | // 8 103 | let size = arg.split_off(7).parse::().unwrap_or(8) * 1024; 104 | let server = Arc::new(Server::http("0.0.0.0:3000").unwrap()); 105 | println!("Now listening on port 3000"); 106 | 107 | for mut request in server.incoming_requests() { 108 | spawn(move || { 109 | let m = request 110 | .headers() 111 | .iter() 112 | .find(|h: &&Header| h.field.equiv("Content-Type")) 113 | .map(|h| h.value.clone()) 114 | .and_then(|val| val.as_str().parse::().ok()) 115 | .unwrap(); 116 | let boundary = m.get_param(mime::BOUNDARY).unwrap().as_str(); 117 | let reader = request.as_reader(); 118 | let response = hello(size, boundary, reader).unwrap(); 119 | let _ = request.respond(response); 120 | }); 121 | } 122 | 123 | Ok(()) 124 | } 125 | -------------------------------------------------------------------------------- /examples/warp.rs: -------------------------------------------------------------------------------- 1 | // #![deny(warnings)] 2 | // 3 | // use anyhow::Result; 4 | // use async_fs::File; 5 | // use bytes::Buf; 6 | // use form_data::FormData; 7 | // use futures_util::{ 8 | // io::{self, AsyncWriteExt}, 9 | // stream::{Stream, TryStreamExt}, 10 | // }; 11 | // use hyper::{Body, Response}; 12 | // use tempfile::tempdir; 13 | // use warp::Filter; 14 | // 15 | // async fn form( 16 | // m: mime::Mime, 17 | // body: impl Stream> + Unpin, 18 | // ) -> Result { 19 | // let dir = tempdir()?; 20 | // let mut txt = String::new(); 21 | // 22 | // txt.push_str(&dir.path().to_string_lossy()); 23 | // txt.push_str("\r\n"); 24 | // 25 | // let mut form = FormData::new( 26 | // body.map_ok(|mut b| b.copy_to_bytes(b.remaining())), 27 | // m.get_param(mime::BOUNDARY).unwrap().as_str(), 28 | // ); 29 | // 30 | // // 512KB for hyper lager buffer 31 | // // form.set_max_buf_size(size)?; 32 | // 33 | // while let Some(mut field) = form.try_next().await? { 34 | // let name = field.name.to_owned(); 35 | // let mut bytes: u64 = 0; 36 | // 37 | // assert_eq!(bytes as usize, field.length); 38 | // 39 | // if let Some(filename) = &field.filename { 40 | // let filepath = dir.path().join(filename); 41 | // 42 | // match filepath.extension().and_then(|s| s.to_str()) { 43 | // Some("txt") => { 44 | // // buffer <= 8KB 45 | // let mut writer = File::create(&filepath).await?; 46 | // bytes = io::copy(&mut field, &mut writer).await?; 47 | // writer.close().await?; 48 | // } 49 | // Some("iso") => { 50 | // field.ignore().await?; 51 | // } 52 | // _ => { 53 | // // 8KB <= buffer <= 512KB 54 | // // let mut writer = File::create(&filepath).await?; 55 | // // bytes = field.copy_to(&mut writer).await?; 56 | // 57 | // let mut writer = std::fs::File::create(&filepath)?; 58 | // bytes = field.copy_to_file(&mut writer).await?; 59 | // } 60 | // } 61 | // 62 | // tracing::info!("file {} {}", name, bytes); 63 | // txt.push_str(&format!("file {name} {bytes}\r\n")); 64 | // } else { 65 | // let buffer = field.bytes().await?; 66 | // bytes = buffer.len() as u64; 67 | // tracing::info!("text {} {}", name, bytes); 68 | // txt.push_str(&format!("text {name} {bytes}\r\n")); 69 | // } 70 | // 71 | // tracing::info!("{:?}", field); 72 | // 73 | // assert_eq!( 74 | // bytes, 75 | // match name.as_str() { 76 | // "empty" => 0, 77 | // "tiny1" => 7, 78 | // "tiny0" => 122, 79 | // "small1" => 315, 80 | // "small0" => 1_778, 81 | // "medium" => 13_196, 82 | // "large" => 2_413_677, 83 | // "book" => 400_797_393, 84 | // "crate" => 9, 85 | // _ => bytes, 86 | // } 87 | // ); 88 | // } 89 | // 90 | // dir.close()?; 91 | // 92 | // Ok(Response::new(Body::from(txt))) 93 | // } 94 | // 95 | // #[tokio::main] 96 | // async fn main() { 97 | // let routes = warp::post() 98 | // .and(warp::header::("Content-Type")) 99 | // .and(warp::body::stream()) 100 | // .and_then(|h, b| async { 101 | // let r = form(h, b).await; 102 | // r.map_err(|e| { 103 | // dbg!(e); 104 | // warp::reject::reject() 105 | // }) 106 | // }); 107 | // 108 | // warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 109 | // } 110 | 111 | #[tokio::main] 112 | async fn main() {} 113 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "form-data-fuzz" 3 | version = "0.0.0" 4 | authors = ["Automatically generated"] 5 | publish = false 6 | edition = "2021" 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [dependencies] 12 | bytes = "1.1" 13 | futures-util = "0.3" 14 | libfuzzer-sys = "0.4" 15 | tokio = { version = "1", features = ["rt", "time"] } 16 | hyper = { version = "0.14", features = ["stream"] } 17 | 18 | [dependencies.form-data] 19 | path = ".." 20 | 21 | # Prevent this from interfering with workspaces 22 | [workspace] 23 | members = ["."] 24 | 25 | [[bin]] 26 | name = "fuzz_form_data" 27 | path = "fuzz_targets/fuzz_form_data.rs" 28 | test = false 29 | doc = false 30 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_form_data.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | use std::convert::Infallible; 5 | use std::time::Duration; 6 | 7 | use bytes::Bytes; 8 | use form_data::FormData; 9 | use futures_util::stream::{once, TryStreamExt}; 10 | use tokio::{runtime, time::timeout}; 11 | 12 | const FIELD_TIMEOUT: Duration = Duration::from_millis(10); 13 | 14 | fuzz_target!(|data: &[u8]| { 15 | let data = data.to_vec(); 16 | let stream = once(async move { Result::::Ok(Bytes::from(data)) }); 17 | 18 | let body = hyper::Body::wrap_stream(stream); 19 | let rt = runtime::Builder::new_current_thread() 20 | .enable_time() 21 | .build() 22 | .expect("runtime"); 23 | 24 | let form_data = FormData::new(body, "BOUNDARY"); 25 | 26 | async fn run(mut form_data: FormData) -> Result<(), Infallible> { 27 | while let Ok(Some(mut field)) = form_data.try_next().await { 28 | let _ = timeout(FIELD_TIMEOUT, field.ignore()).await; 29 | } 30 | Ok(()) 31 | } 32 | 33 | rt.block_on(async move { run(form_data).await.unwrap() }) 34 | }); 35 | -------------------------------------------------------------------------------- /src/async.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error as StdError, 3 | fs::File, 4 | io::Write, 5 | pin::Pin, 6 | task::{Context, Poll}, 7 | }; 8 | 9 | use bytes::{Bytes, BytesMut}; 10 | use futures_util::{ 11 | io::{self, AsyncRead, AsyncWrite, AsyncWriteExt}, 12 | stream::{Stream, TryStreamExt}, 13 | }; 14 | use http::{ 15 | header::{CONTENT_DISPOSITION, CONTENT_TYPE}, 16 | HeaderValue, 17 | }; 18 | use tracing::trace; 19 | 20 | use crate::{ 21 | utils::{parse_content_disposition, parse_content_type, parse_part_headers}, 22 | Error, Field, Flag, FormData, Result, State, 23 | }; 24 | 25 | impl Stream for State 26 | where 27 | T: Stream> + Unpin, 28 | B: Into, 29 | E: Into>, 30 | { 31 | type Item = Result; 32 | 33 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 34 | loop { 35 | if self.is_readable { 36 | // part 37 | trace!("attempting to decode a part"); 38 | 39 | // field 40 | if let Some(data) = self.decode() { 41 | trace!("part decoded from buffer"); 42 | return Poll::Ready(Some(Ok(data))); 43 | } 44 | 45 | // field stream is ended 46 | if Flag::Next == self.flag { 47 | return Poll::Ready(None); 48 | } 49 | 50 | // whole stream is ended 51 | if Flag::Eof == self.flag { 52 | self.length -= self.buffer.len() as u64; 53 | self.buffer.clear(); 54 | self.eof = true; 55 | return Poll::Ready(None); 56 | } 57 | 58 | self.is_readable = false; 59 | } 60 | 61 | trace!("polling data from stream"); 62 | 63 | if self.eof { 64 | self.is_readable = true; 65 | continue; 66 | } 67 | 68 | self.buffer.reserve(1); 69 | let bytect = match Pin::new(self.io_mut()).poll_next(cx) { 70 | Poll::Pending => { 71 | return Poll::Pending; 72 | } 73 | Poll::Ready(Some(Ok(b))) => { 74 | let b = b.into(); 75 | let l = b.len() as u64; 76 | 77 | if let Some(max) = self.limits.checked_stream_size(self.length + l) { 78 | return Poll::Ready(Some(Err(Error::PayloadTooLarge(max)))); 79 | } 80 | 81 | self.buffer.extend_from_slice(&b); 82 | self.length += l; 83 | l 84 | } 85 | Poll::Ready(Some(Err(e))) => { 86 | return Poll::Ready(Some(Err(Error::BoxError(e.into())))) 87 | } 88 | Poll::Ready(None) => 0, 89 | }; 90 | 91 | if bytect == 0 { 92 | self.eof = true; 93 | } 94 | 95 | self.is_readable = true; 96 | } 97 | } 98 | } 99 | 100 | impl Field 101 | where 102 | T: Stream> + Unpin, 103 | B: Into, 104 | E: Into>, 105 | { 106 | /// Reads field data to bytes. 107 | pub async fn bytes(&mut self) -> Result { 108 | let mut bytes = BytesMut::new(); 109 | while let Some(buf) = self.try_next().await? { 110 | bytes.extend_from_slice(&buf); 111 | } 112 | Ok(bytes.freeze()) 113 | } 114 | 115 | /// Copys large buffer to `AsyncRead`, hyper can support large buffer, 116 | /// 8KB <= buffer <= 512KB, so if we want to handle large buffer. 117 | /// `Form::set_max_buf_size(512 * 1024);` 118 | /// 3~4x performance improvement over the 8KB limitation of `AsyncRead`. 119 | pub async fn copy_to(&mut self, writer: &mut W) -> Result 120 | where 121 | W: AsyncWrite + Send + Unpin + 'static, 122 | { 123 | let mut n = 0; 124 | while let Some(buf) = self.try_next().await? { 125 | writer.write_all(&buf).await?; 126 | n += buf.len(); 127 | } 128 | writer.flush().await?; 129 | Ok(n as u64) 130 | } 131 | 132 | /// Copys large buffer to File, hyper can support large buffer, 133 | /// 8KB <= buffer <= 512KB, so if we want to handle large buffer. 134 | /// `Form::set_max_buf_size(512 * 1024);` 135 | /// 4x+ performance improvement over the 8KB limitation of `AsyncRead`. 136 | pub async fn copy_to_file(&mut self, file: &mut File) -> Result { 137 | let mut n = 0; 138 | while let Some(buf) = self.try_next().await? { 139 | n += file.write(&buf)?; 140 | } 141 | file.flush()?; 142 | Ok(n as u64) 143 | } 144 | 145 | /// Ignores current field data, pass it. 146 | pub async fn ignore(&mut self) -> Result<()> { 147 | while let Some(buf) = self.try_next().await? { 148 | drop(buf); 149 | } 150 | Ok(()) 151 | } 152 | } 153 | 154 | /// Reads payload data from part, then puts them to anywhere 155 | impl AsyncRead for Field 156 | where 157 | T: Stream> + Unpin, 158 | B: Into, 159 | E: Into>, 160 | { 161 | fn poll_read( 162 | self: Pin<&mut Self>, 163 | cx: &mut Context<'_>, 164 | mut buf: &mut [u8], 165 | ) -> Poll> { 166 | match self.poll_next(cx) { 167 | Poll::Pending => Poll::Pending, 168 | Poll::Ready(None) => Poll::Ready(Ok(0)), 169 | Poll::Ready(Some(Ok(b))) => Poll::Ready(Ok(buf.write(&b)?)), 170 | Poll::Ready(Some(Err(e))) => Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e))), 171 | } 172 | } 173 | } 174 | 175 | /// Reads payload data from part, then yields them 176 | impl Stream for Field 177 | where 178 | T: Stream> + Unpin, 179 | B: Into, 180 | E: Into>, 181 | { 182 | type Item = Result; 183 | 184 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 185 | trace!("polling {} {}", self.index, self.state.is_some()); 186 | 187 | let Some(state) = self.state.clone() else { 188 | return Poll::Ready(None); 189 | }; 190 | 191 | let is_file = self.filename.is_some(); 192 | let mut state = state 193 | .try_lock() 194 | .map_err(|e| Error::TryLockError(e.to_string()))?; 195 | 196 | match Pin::new(&mut *state).poll_next(cx)? { 197 | Poll::Pending => Poll::Pending, 198 | Poll::Ready(res) => match res { 199 | None => { 200 | if let Some(waker) = state.waker_mut().take() { 201 | waker.wake(); 202 | } 203 | trace!("polled {}", self.index); 204 | drop(self.state.take()); 205 | Poll::Ready(None) 206 | } 207 | Some(buf) => { 208 | let l = buf.len(); 209 | 210 | if is_file { 211 | if let Some(max) = state.limits.checked_file_size(self.length + l) { 212 | return Poll::Ready(Some(Err(Error::FileTooLarge(max)))); 213 | } 214 | } else if let Some(max) = state.limits.checked_field_size(self.length + l) { 215 | return Poll::Ready(Some(Err(Error::FieldTooLarge(max)))); 216 | } 217 | 218 | self.length += l; 219 | trace!("polled bytes {}/{}", buf.len(), self.length); 220 | Poll::Ready(Some(Ok(buf))) 221 | } 222 | }, 223 | } 224 | } 225 | } 226 | 227 | /// Reads form-data from request payload body, then yields `Field` 228 | impl Stream for FormData 229 | where 230 | T: Stream> + Unpin, 231 | B: Into, 232 | E: Into>, 233 | { 234 | type Item = Result>; 235 | 236 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 237 | let mut state = self 238 | .state 239 | .try_lock() 240 | .map_err(|e| Error::TryLockError(e.to_string()))?; 241 | 242 | if state.waker().is_some() { 243 | return Poll::Pending; 244 | } 245 | 246 | match Pin::new(&mut *state).poll_next(cx)? { 247 | Poll::Pending => Poll::Pending, 248 | Poll::Ready(res) => match res { 249 | None => { 250 | trace!("parse eof"); 251 | Poll::Ready(None) 252 | } 253 | Some(buf) => { 254 | trace!("parse part"); 255 | 256 | // too many parts 257 | if let Some(max) = state.limits.checked_parts(state.total + 1) { 258 | return Poll::Ready(Some(Err(Error::PartsTooMany(max)))); 259 | } 260 | 261 | // invalid part header 262 | let Ok(mut headers) = parse_part_headers(&buf) else { 263 | return Poll::Ready(Some(Err(Error::InvalidHeader))); 264 | }; 265 | 266 | // invalid content disposition 267 | let Some((name, filename)) = headers 268 | .remove(CONTENT_DISPOSITION) 269 | .as_ref() 270 | .map(HeaderValue::as_bytes) 271 | .map(parse_content_disposition) 272 | .and_then(Result::ok) 273 | else { 274 | return Poll::Ready(Some(Err(Error::InvalidContentDisposition))); 275 | }; 276 | 277 | // field name is too long 278 | if let Some(max) = state.limits.checked_field_name_size(name.len()) { 279 | return Poll::Ready(Some(Err(Error::FieldNameTooLong(max)))); 280 | } 281 | 282 | if filename.is_some() { 283 | // files too many 284 | if let Some(max) = state.limits.checked_files(state.files + 1) { 285 | return Poll::Ready(Some(Err(Error::FilesTooMany(max)))); 286 | } 287 | state.files += 1; 288 | } else { 289 | // fields too many 290 | if let Some(max) = state.limits.checked_fields(state.fields + 1) { 291 | return Poll::Ready(Some(Err(Error::FieldsTooMany(max)))); 292 | } 293 | state.fields += 1; 294 | } 295 | 296 | // yields `Field` 297 | let mut field = Field::empty(); 298 | 299 | field.name = name; 300 | field.filename = filename; 301 | field.index = state.index(); 302 | field.content_type = parse_content_type(headers.remove(CONTENT_TYPE).as_ref()); 303 | field.state_mut().replace(self.state()); 304 | 305 | if !headers.is_empty() { 306 | field.headers_mut().replace(headers); 307 | } 308 | 309 | // clone waker, if field is polled data, wake it. 310 | state.waker_mut().replace(cx.waker().clone()); 311 | 312 | Poll::Ready(Some(Ok(field))) 313 | } 314 | }, 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Form-data Error 4 | #[derive(Debug, Error)] 5 | pub enum Error { 6 | /// IO Error 7 | #[error(transparent)] 8 | Stream(#[from] std::io::Error), 9 | 10 | /// Box Error 11 | #[error(transparent)] 12 | BoxError(#[from] Box), 13 | 14 | /// Invalid part header 15 | #[error("invalid part header")] 16 | InvalidHeader, 17 | 18 | /// Invalid content disposition 19 | #[error("invalid content disposition")] 20 | InvalidContentDisposition, 21 | 22 | /// Payload too large 23 | #[error("payload is too large, limit to `{0}`")] 24 | PayloadTooLarge(u64), 25 | 26 | /// File too large 27 | #[error("file is too large, limit to `{0}`")] 28 | FileTooLarge(usize), 29 | 30 | /// Field too large 31 | #[error("field is too large, limit to `{0}`")] 32 | FieldTooLarge(usize), 33 | 34 | /// Parts too many 35 | #[error("parts is too many, limit to `{0}`")] 36 | PartsTooMany(usize), 37 | 38 | /// Fields too many 39 | #[error("fields is too many, limit to `{0}`")] 40 | FieldsTooMany(usize), 41 | 42 | /// Files too many 43 | #[error("files is too many, limit to `{0}`")] 44 | FilesTooMany(usize), 45 | 46 | /// Field name is too long 47 | #[error("field name is too long, limit to `{0}`")] 48 | FieldNameTooLong(usize), 49 | 50 | /// Try Lock Error 51 | #[error("`{0}`")] 52 | TryLockError(String), 53 | } 54 | -------------------------------------------------------------------------------- /src/field.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | sync::{Arc, Mutex}, 4 | }; 5 | 6 | use crate::State; 7 | 8 | /// Field 9 | pub struct Field { 10 | /// The payload size of Field. 11 | pub length: usize, 12 | /// The index of Field. 13 | pub index: usize, 14 | /// The name of Field. 15 | pub name: String, 16 | /// The filename of Field, optinal. 17 | pub filename: Option, 18 | /// The `content_type` of Field, optinal. 19 | pub content_type: Option, 20 | /// The extras headers of Field, optinal. 21 | pub headers: Option, 22 | pub(crate) state: Option>>>, 23 | } 24 | 25 | impl Field { 26 | /// Creates an empty field. 27 | #[must_use] 28 | pub fn empty() -> Self { 29 | Self { 30 | index: 0, 31 | length: 0, 32 | name: String::new(), 33 | filename: None, 34 | content_type: None, 35 | headers: None, 36 | state: None, 37 | } 38 | } 39 | 40 | /// Gets mutable headers. 41 | #[must_use] 42 | pub fn headers_mut(&mut self) -> &mut Option { 43 | &mut self.headers 44 | } 45 | 46 | /// Gets mutable state. 47 | #[must_use] 48 | pub fn state_mut(&mut self) -> &mut Option>>> { 49 | &mut self.state 50 | } 51 | 52 | /// Gets the status of state. 53 | #[must_use] 54 | pub fn consumed(&self) -> bool { 55 | self.state.is_none() 56 | } 57 | } 58 | 59 | impl fmt::Debug for Field { 60 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 61 | f.debug_struct("Field") 62 | .field("name", &self.name) 63 | .field("filename", &self.filename) 64 | .field("content_type", &self.content_type) 65 | .field("index", &self.index) 66 | .field("length", &self.length) 67 | .field("headers", &self.headers) 68 | .field("consumed", &self.state.is_none()) 69 | .finish() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/form.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_name_repetitions)] 2 | 3 | use std::sync::{Arc, Mutex}; 4 | 5 | use crate::{Error, Limits, Result, State}; 6 | 7 | /// `FormData` 8 | pub struct FormData { 9 | pub(crate) state: Arc>>, 10 | } 11 | 12 | impl FormData { 13 | /// Creates new `FormData` with boundary. 14 | #[must_use] 15 | pub fn new(t: T, boundary: &str) -> Self { 16 | Self { 17 | state: Arc::new(Mutex::new(State::new( 18 | t, 19 | boundary.as_bytes(), 20 | Limits::default(), 21 | ))), 22 | } 23 | } 24 | 25 | /// Creates new `FormData` with boundary and limits. 26 | #[must_use] 27 | pub fn with_limits(t: T, boundary: &str, limits: Limits) -> Self { 28 | Self { 29 | state: Arc::new(Mutex::new(State::new(t, boundary.as_bytes(), limits))), 30 | } 31 | } 32 | 33 | /// Gets the state. 34 | #[must_use] 35 | pub fn state(&self) -> Arc>> { 36 | self.state.clone() 37 | } 38 | 39 | /// Sets Buffer max size for reading. 40 | pub fn set_max_buf_size(&self, max: usize) -> Result<()> { 41 | self.state 42 | .try_lock() 43 | .map_err(|e| Error::TryLockError(e.to_string()))? 44 | .limits_mut() 45 | .buffer_size = max; 46 | 47 | Ok(()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! form-data implemented [rfc7578] 2 | //! 3 | //! # Example 4 | //! 5 | //! ```no_run 6 | //! #![deny(warnings)] 7 | //! 8 | //! use std::{env, net::SocketAddr}; 9 | //! 10 | //! use anyhow::Result; 11 | //! use async_fs::File; 12 | //! use bytes::Bytes; 13 | //! use futures_util::{ 14 | //! io::{copy, AsyncWriteExt}, 15 | //! stream::TryStreamExt, 16 | //! }; 17 | //! use http_body_util::Full; 18 | //! use hyper::{body::Incoming, header, server::conn::http1, service::service_fn, Request, Response}; 19 | //! use hyper_util::rt::TokioIo; 20 | //! use tempfile::tempdir; 21 | //! use tokio::net::TcpListener; 22 | //! 23 | //! use form_data::{Error, FormData}; 24 | //! 25 | //! #[path = "../tests/lib/mod.rs"] 26 | //! mod lib; 27 | //! 28 | //! use lib::IncomingBody; 29 | //! 30 | //! async fn hello(size: usize, req: Request) -> Result>, Error> { 31 | //! let dir = tempdir()?; 32 | //! let mut txt = String::new(); 33 | //! 34 | //! txt.push_str(&dir.path().to_string_lossy()); 35 | //! txt.push_str("\r\n"); 36 | //! 37 | //! let m = req 38 | //! .headers() 39 | //! .get(header::CONTENT_TYPE) 40 | //! .and_then(|val| val.to_str().ok()) 41 | //! .and_then(|val| val.parse::().ok()) 42 | //! .ok_or(Error::InvalidHeader)?; 43 | //! 44 | //! let mut form = FormData::new( 45 | //! req.map(|body| IncomingBody::new(Some(body))).into_body(), 46 | //! m.get_param(mime::BOUNDARY).unwrap().as_str(), 47 | //! ); 48 | //! 49 | //! // 512KB for hyper lager buffer 50 | //! form.set_max_buf_size(size)?; 51 | //! 52 | //! while let Some(mut field) = form.try_next().await? { 53 | //! let name = field.name.to_owned(); 54 | //! let mut bytes: u64 = 0; 55 | //! 56 | //! assert_eq!(bytes as usize, field.length); 57 | //! 58 | //! if let Some(filename) = &field.filename { 59 | //! let filepath = dir.path().join(filename); 60 | //! 61 | //! match filepath.extension().and_then(|s| s.to_str()) { 62 | //! Some("txt") => { 63 | //! // buffer <= 8KB 64 | //! let mut writer = File::create(&filepath).await?; 65 | //! bytes = copy(&mut field, &mut writer).await?; 66 | //! writer.close().await?; 67 | //! } 68 | //! Some("iso") => { 69 | //! field.ignore().await?; 70 | //! } 71 | //! _ => { 72 | //! // 8KB <= buffer <= 512KB 73 | //! // let mut writer = File::create(&filepath).await?; 74 | //! // bytes = field.copy_to(&mut writer).await?; 75 | //! 76 | //! let mut writer = std::fs::File::create(&filepath)?; 77 | //! bytes = field.copy_to_file(&mut writer).await?; 78 | //! } 79 | //! } 80 | //! 81 | //! tracing::info!("file {} {}", name, bytes); 82 | //! txt.push_str(&format!("file {name} {bytes}\r\n")); 83 | //! } else { 84 | //! let buffer = field.bytes().await?; 85 | //! bytes = buffer.len() as u64; 86 | //! tracing::info!("text {} {}", name, bytes); 87 | //! txt.push_str(&format!("text {name} {bytes}\r\n")); 88 | //! } 89 | //! 90 | //! tracing::info!("{:?}", field); 91 | //! 92 | //! assert_eq!( 93 | //! bytes, 94 | //! match name.as_str() { 95 | //! "empty" => 0, 96 | //! "tiny1" => 7, 97 | //! "tiny0" => 122, 98 | //! "small1" => 315, 99 | //! "small0" => 1_778, 100 | //! "medium" => 13_196, 101 | //! "large" => 2_413_677, 102 | //! "book" => 400_797_393, 103 | //! "crate" => 9, 104 | //! _ => bytes, 105 | //! } 106 | //! ); 107 | //! } 108 | //! 109 | //! dir.close()?; 110 | //! 111 | //! Ok(Response::new(Full::from(Into::::into(txt)))) 112 | //! } 113 | //! 114 | //! #[tokio::main] 115 | //! pub async fn main() -> Result<(), Box> { 116 | //! tracing_subscriber::fmt() 117 | //! // From env var: `RUST_LOG` 118 | //! .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 119 | //! .try_init() 120 | //! .map_err(|e| anyhow::anyhow!(e))?; 121 | //! 122 | //! let mut arg = env::args() 123 | //! .find(|a| a.starts_with("--size=")) 124 | //! .unwrap_or_else(|| "--size=8".to_string()); 125 | //! 126 | //! // 512 127 | //! // 8 * 2 128 | //! // 8 129 | //! let size = arg.split_off(7).parse::().unwrap_or(8) * 1024; 130 | //! let addr: SocketAddr = ([127, 0, 0, 1], 3000).into(); 131 | //! 132 | //! println!("Listening on http://{addr}"); 133 | //! println!("FormData max buffer size is {}KB", size / 1024); 134 | //! 135 | //! let listener = TcpListener::bind(addr).await?; 136 | //! 137 | //! loop { 138 | //! let (stream, _) = listener.accept().await?; 139 | //! 140 | //! tokio::task::spawn(async move { 141 | //! if let Err(err) = http1::Builder::new() 142 | //! .max_buf_size(size) 143 | //! .serve_connection( 144 | //! TokioIo::new(stream), 145 | //! service_fn(|req: Request| hello(size, req)), 146 | //! ) 147 | //! .await 148 | //! { 149 | //! println!("Error serving connection: {:?}", err); 150 | //! } 151 | //! }); 152 | //! } 153 | //! } 154 | //! ``` 155 | //! 156 | //! [rfc7578]: 157 | 158 | #![forbid(unsafe_code)] 159 | #![deny(nonstandard_style)] 160 | #![warn(missing_docs, unreachable_pub)] 161 | #![allow(clippy::missing_errors_doc)] 162 | 163 | mod error; 164 | pub use error::Error; 165 | 166 | mod field; 167 | pub use field::Field; 168 | 169 | mod form; 170 | pub use form::FormData; 171 | 172 | mod limits; 173 | pub use limits::Limits; 174 | 175 | mod state; 176 | pub use state::*; 177 | 178 | mod utils; 179 | 180 | pub(crate) type Result = std::result::Result; 181 | 182 | #[cfg(all(feature = "async", not(feature = "sync")))] 183 | mod r#async; 184 | #[cfg(all(feature = "sync", not(feature = "async")))] 185 | mod sync; 186 | -------------------------------------------------------------------------------- /src/limits.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Various limits on incoming data 4 | #[derive(Debug, Clone, Deserialize, Serialize)] 5 | pub struct Limits { 6 | /// Max field name size 7 | pub field_name_size: Option, 8 | /// Max field value size 9 | pub field_size: Option, 10 | /// Max number of non-file fields 11 | pub fields: Option, 12 | /// Max file size 13 | pub file_size: Option, 14 | /// Max number of file fields 15 | pub files: Option, 16 | /// Max number of parts (fields + files) 17 | pub parts: Option, 18 | /// Max number of whole stream 19 | pub stream_size: Option, 20 | /// Max number of buffer size 21 | pub buffer_size: usize, 22 | } 23 | 24 | impl Default for Limits { 25 | fn default() -> Self { 26 | Self { 27 | field_name_size: Some(Self::DEFAULT_FIELD_NAME_SIZE), 28 | field_size: Some(Self::DEFAULT_FIELD_SIZE), 29 | fields: None, 30 | file_size: Some(Self::DEFAULT_FILE_SIZE), 31 | files: None, 32 | parts: None, 33 | stream_size: Some(Self::DEFAULT_STREAM_SIZE), 34 | buffer_size: Self::DEFAULT_BUFFER_SIZE, 35 | } 36 | } 37 | } 38 | 39 | impl Limits { 40 | /// Max number of field name size, defaults to 100. 41 | pub const DEFAULT_FIELD_NAME_SIZE: usize = 100; 42 | 43 | /// Max number of field value size, defaults to 100KB. 44 | pub const DEFAULT_FIELD_SIZE: usize = 100 * 1024; 45 | 46 | /// Max number of file size, defaults to 10MB. 47 | pub const DEFAULT_FILE_SIZE: usize = 10 * 1024 * 1024; 48 | 49 | /// Max number of stream size, defaults to 200MB. 50 | pub const DEFAULT_STREAM_SIZE: u64 = 200 * 1024 * 1024; 51 | 52 | /// Max number of buffer size, defaults to 8KB 53 | pub const DEFAULT_BUFFER_SIZE: usize = 8 * 1024; 54 | 55 | /// Max field name size 56 | #[must_use] 57 | pub fn field_name_size(mut self, max: usize) -> Self { 58 | self.field_name_size.replace(max); 59 | self 60 | } 61 | 62 | /// Max field value size 63 | #[must_use] 64 | pub fn field_size(mut self, max: usize) -> Self { 65 | self.field_size.replace(max); 66 | self 67 | } 68 | 69 | /// Max number of non-file fields 70 | #[must_use] 71 | pub fn fields(mut self, max: usize) -> Self { 72 | self.fields.replace(max); 73 | self 74 | } 75 | 76 | /// Max file size 77 | #[must_use] 78 | pub fn file_size(mut self, max: usize) -> Self { 79 | self.file_size.replace(max); 80 | self 81 | } 82 | 83 | /// Max number of file fields 84 | #[must_use] 85 | pub fn files(mut self, max: usize) -> Self { 86 | self.files.replace(max); 87 | self 88 | } 89 | 90 | /// Max number of parts (fields + files) 91 | #[must_use] 92 | pub fn parts(mut self, max: usize) -> Self { 93 | self.parts.replace(max); 94 | self 95 | } 96 | 97 | /// Max number of buffer size 98 | /// 99 | /// # Panics 100 | /// 101 | /// If `max` is greater than or equal to `Limits::DEFAULT_BUFFER_SIZE`. 102 | #[must_use] 103 | pub fn buffer_size(mut self, max: usize) -> Self { 104 | assert!( 105 | max >= Self::DEFAULT_BUFFER_SIZE, 106 | "The max_buffer_size cannot be smaller than {}.", 107 | Self::DEFAULT_BUFFER_SIZE, 108 | ); 109 | 110 | self.buffer_size = max; 111 | self 112 | } 113 | 114 | /// Max number of whole stream size 115 | #[must_use] 116 | pub fn stream_size(mut self, max: u64) -> Self { 117 | self.stream_size.replace(max); 118 | self 119 | } 120 | 121 | /// Check parts 122 | #[must_use] 123 | pub fn checked_parts(&self, rhs: usize) -> Option { 124 | self.parts.filter(|max| rhs > *max) 125 | } 126 | 127 | /// Check fields 128 | #[must_use] 129 | pub fn checked_fields(&self, rhs: usize) -> Option { 130 | self.fields.filter(|max| rhs > *max) 131 | } 132 | 133 | /// Check files 134 | #[must_use] 135 | pub fn checked_files(&self, rhs: usize) -> Option { 136 | self.files.filter(|max| rhs > *max) 137 | } 138 | 139 | /// Check stream size 140 | #[must_use] 141 | pub fn checked_stream_size(&self, rhs: u64) -> Option { 142 | self.stream_size.filter(|max| rhs > *max) 143 | } 144 | 145 | /// Check file size 146 | #[must_use] 147 | pub fn checked_file_size(&self, rhs: usize) -> Option { 148 | self.file_size.filter(|max| rhs > *max) 149 | } 150 | 151 | /// Check field size 152 | #[must_use] 153 | pub fn checked_field_size(&self, rhs: usize) -> Option { 154 | self.field_size.filter(|max| rhs > *max) 155 | } 156 | 157 | /// Check field name size 158 | #[must_use] 159 | pub fn checked_field_name_size(&self, rhs: usize) -> Option { 160 | self.field_name_size.filter(|max| rhs > *max) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[cfg(feature = "async")] 4 | use std::task::Waker; 5 | 6 | use bytes::{Buf, Bytes, BytesMut}; 7 | use memchr::memmem; 8 | 9 | use crate::{ 10 | utils::{CRLF, CRLFS, DASHES}, 11 | Limits, 12 | }; 13 | 14 | #[derive(Debug, PartialEq)] 15 | pub(crate) enum Flag { 16 | Delimiting(bool), 17 | Heading(usize), 18 | Headed, 19 | Header, 20 | Next, 21 | Eof, 22 | } 23 | 24 | /// IO State 25 | pub struct State { 26 | io: T, 27 | pub(crate) eof: bool, 28 | pub(crate) flag: Flag, 29 | pub(crate) length: u64, 30 | pub(crate) buffer: BytesMut, 31 | delimiter: Bytes, 32 | pub(crate) is_readable: bool, 33 | #[cfg(feature = "async")] 34 | waker: Option, 35 | pub(crate) total: usize, 36 | pub(crate) files: usize, 37 | pub(crate) fields: usize, 38 | pub(crate) limits: Limits, 39 | } 40 | 41 | impl State { 42 | /// Creates new State. 43 | pub fn new(io: T, boundary: &[u8], limits: Limits) -> Self { 44 | // `\r\n--boundary` 45 | let mut delimiter = BytesMut::with_capacity(4 + boundary.len()); 46 | delimiter.extend_from_slice(&CRLF); 47 | delimiter.extend_from_slice(&DASHES); 48 | delimiter.extend_from_slice(boundary); 49 | 50 | // `\r\n` 51 | let mut buffer = BytesMut::with_capacity(limits.buffer_size); 52 | buffer.extend_from_slice(&CRLF); 53 | 54 | Self { 55 | io, 56 | limits, 57 | total: 0, 58 | files: 0, 59 | fields: 0, 60 | length: 0, 61 | 62 | #[cfg(feature = "async")] 63 | waker: None, 64 | eof: false, 65 | is_readable: false, 66 | 67 | buffer, 68 | flag: Flag::Delimiting(false), 69 | delimiter: delimiter.freeze(), 70 | } 71 | } 72 | 73 | /// Gets io. 74 | pub fn io_mut(&mut self) -> &mut T { 75 | &mut self.io 76 | } 77 | 78 | /// Gets waker. 79 | #[cfg(feature = "async")] 80 | pub fn waker(&self) -> Option<&Waker> { 81 | self.waker.as_ref() 82 | } 83 | 84 | /// Gets waker. 85 | #[cfg(feature = "async")] 86 | pub fn waker_mut(&mut self) -> &mut Option { 87 | &mut self.waker 88 | } 89 | 90 | /// Gets limits. 91 | pub fn limits_mut(&mut self) -> &mut Limits { 92 | &mut self.limits 93 | } 94 | 95 | /// Splits buffer. 96 | pub fn split_buffer(&mut self, n: usize) -> Bytes { 97 | self.buffer.split_to(n).freeze() 98 | } 99 | 100 | /// Gets the index of the field. 101 | pub fn index(&mut self) -> usize { 102 | let index = self.total; 103 | self.total += 1; 104 | index 105 | } 106 | 107 | /// Gets the length of the form-data. 108 | pub fn len(&self) -> u64 { 109 | self.length 110 | } 111 | 112 | /// Gets bool of the form-data's length is zero. 113 | pub fn is_empty(&self) -> bool { 114 | self.length == 0 115 | } 116 | 117 | /// Gets EOF. 118 | pub fn eof(&self) -> bool { 119 | self.eof 120 | } 121 | 122 | /// Counts the fields. 123 | pub fn total(&self) -> usize { 124 | self.total 125 | } 126 | 127 | /// Gets the boundary. 128 | pub fn boundary(&self) -> &[u8] { 129 | &self.delimiter[4..] 130 | } 131 | 132 | pub(crate) fn decode(&mut self) -> Option { 133 | if let Flag::Delimiting(boding) = self.flag { 134 | if let Some(n) = memmem::find(&self.buffer, &self.delimiter) { 135 | self.flag = Flag::Heading(n); 136 | } else { 137 | // Empty Request Body 138 | if self.eof && self.buffer.len() == 2 && self.buffer[..2] == CRLF { 139 | self.buffer.advance(2); 140 | self.flag = Flag::Eof; 141 | return None; 142 | } 143 | 144 | // Empty Part Body 145 | if memmem::find(&self.buffer, &self.delimiter[2..]).is_some() { 146 | self.flag = Flag::Next; 147 | self.buffer.advance(self.delimiter.len() - 2); 148 | return None; 149 | } 150 | 151 | // Reading Part Body 152 | if boding { 153 | // Returns buffer with `max_buf_size` 154 | if self.limits.buffer_size + self.delimiter.len() < self.buffer.len() { 155 | return Some(self.buffer.split_to(self.limits.buffer_size).freeze()); 156 | } 157 | } 158 | } 159 | } 160 | 161 | if let Flag::Heading(ref mut n) = self.flag { 162 | // first part 163 | if self.total == 0 { 164 | if *n > 0 { 165 | // consume data 166 | self.buffer.advance(*n); 167 | } 168 | self.buffer.advance(self.delimiter.len()); 169 | self.flag = Flag::Headed; 170 | } else { 171 | // prev part is ended 172 | if *n == 0 { 173 | // field'stream need to stop 174 | self.flag = Flag::Next; 175 | self.buffer.advance(self.delimiter.len()); 176 | return None; 177 | } 178 | // prev part last data 179 | let buf = self.buffer.split_to(*n).freeze(); 180 | *n = 0; 181 | return Some(buf); 182 | } 183 | } 184 | 185 | if Flag::Next == self.flag { 186 | self.flag = Flag::Headed; 187 | } 188 | 189 | if Flag::Headed == self.flag && self.buffer.len() > 1 { 190 | if self.buffer[..2] == CRLF { 191 | self.buffer.advance(2); 192 | self.flag = Flag::Header; 193 | } else if self.buffer[..2] == DASHES { 194 | self.buffer.advance(2); 195 | self.flag = Flag::Eof; 196 | return None; 197 | } else { 198 | // We dont parse other format, like `\n` 199 | self.length -= (self.delimiter.len() - 2) as u64; 200 | self.flag = Flag::Eof; 201 | return None; 202 | } 203 | } 204 | 205 | if Flag::Header == self.flag { 206 | if let Some(n) = memmem::find(&self.buffer, &CRLFS) { 207 | self.flag = Flag::Delimiting(true); 208 | return Some(self.buffer.split_to(n + CRLFS.len()).freeze()); 209 | } 210 | } 211 | 212 | None 213 | } 214 | } 215 | 216 | impl fmt::Debug for State { 217 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 218 | f.debug_struct("State") 219 | .field("eof", &self.eof) 220 | .field("flag", &self.flag) 221 | .field("total", &self.total) 222 | .field("files", &self.files) 223 | .field("fields", &self.fields) 224 | .field("length", &self.length) 225 | .field("limits", &self.limits) 226 | .field("is_readable", &self.is_readable) 227 | .field("boundary", &String::from_utf8_lossy(self.boundary())) 228 | .finish_non_exhaustive() 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/sync.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{Error as IoError, ErrorKind, Read, Write}, 4 | }; 5 | 6 | use bytes::{Bytes, BytesMut}; 7 | use http::{ 8 | header::{CONTENT_DISPOSITION, CONTENT_TYPE}, 9 | HeaderValue, 10 | }; 11 | use tracing::trace; 12 | 13 | use crate::{ 14 | utils::{parse_content_disposition, parse_content_type, parse_part_headers}, 15 | Error, Field, Flag, FormData, Result, State, 16 | }; 17 | 18 | impl Read for State 19 | where 20 | T: Read, 21 | { 22 | fn read(&mut self, buf: &mut [u8]) -> Result { 23 | self.io_mut().read(buf) 24 | } 25 | } 26 | 27 | impl Iterator for State 28 | where 29 | T: Read, 30 | { 31 | type Item = Result; 32 | 33 | fn next(&mut self) -> Option { 34 | loop { 35 | if self.is_readable { 36 | // part 37 | trace!("attempting to decode a part"); 38 | 39 | // field 40 | if let Some(data) = self.decode() { 41 | trace!("part decoded from buffer"); 42 | return Some(Ok(data)); 43 | } 44 | 45 | // field stream is ended 46 | if Flag::Next == self.flag { 47 | return None; 48 | } 49 | 50 | // whole stream is ended 51 | if Flag::Eof == self.flag { 52 | self.length -= self.buffer.len() as u64; 53 | self.buffer.clear(); 54 | self.eof = true; 55 | return None; 56 | } 57 | 58 | self.is_readable = false; 59 | } 60 | 61 | trace!("polling data from stream"); 62 | 63 | if self.eof { 64 | self.is_readable = true; 65 | continue; 66 | } 67 | 68 | self.buffer.reserve(1); 69 | let mut b = BytesMut::new(); 70 | b.resize(self.limits.buffer_size, 0); 71 | let bytect = match self.read(&mut b) { 72 | Err(e) => return Some(Err(e.into())), 73 | Ok(s) => { 74 | let l = s as u64; 75 | if let Some(max) = self.limits.checked_stream_size(self.length + l) { 76 | return Some(Err(Error::PayloadTooLarge(max))); 77 | } 78 | 79 | self.buffer.extend_from_slice(&b.split_to(s)); 80 | self.length += l; 81 | l 82 | } 83 | }; 84 | 85 | if bytect == 0 { 86 | self.eof = true; 87 | } 88 | 89 | self.is_readable = true; 90 | } 91 | } 92 | } 93 | 94 | impl Read for Field 95 | where 96 | T: Read, 97 | { 98 | fn read(&mut self, mut buf: &mut [u8]) -> Result { 99 | match self.next() { 100 | None => Ok(0), 101 | Some(Ok(b)) => buf.write(&b), 102 | Some(Err(e)) => Err(IoError::new(ErrorKind::Other, e)), 103 | } 104 | } 105 | } 106 | 107 | impl Field 108 | where 109 | T: Read, 110 | { 111 | /// Reads field data to bytes. 112 | pub fn bytes(&mut self) -> Result { 113 | let mut bytes = BytesMut::new(); 114 | while let Some(buf) = self.next() { 115 | bytes.extend_from_slice(&buf?); 116 | } 117 | Ok(bytes.freeze()) 118 | } 119 | 120 | /// Copys bytes to a writer. 121 | pub fn copy_to(&mut self, writer: &mut W) -> Result 122 | where 123 | W: Write + Send + Unpin + 'static, 124 | { 125 | let mut n = 0; 126 | while let Some(buf) = self.next() { 127 | let b = buf?; 128 | writer.write_all(&b)?; 129 | n += b.len(); 130 | } 131 | writer.flush()?; 132 | Ok(n as u64) 133 | } 134 | 135 | /// Copys bytes to a File. 136 | pub fn copy_to_file(&mut self, file: &mut File) -> Result { 137 | let mut n = 0; 138 | while let Some(buf) = self.next() { 139 | n += file.write(&buf?)?; 140 | } 141 | file.flush()?; 142 | Ok(n as u64) 143 | } 144 | 145 | /// Ignores current field data, pass it. 146 | pub fn ignore(&mut self) -> Result<()> { 147 | while let Some(buf) = self.next() { 148 | drop(buf?); 149 | } 150 | Ok(()) 151 | } 152 | } 153 | 154 | impl Iterator for Field 155 | where 156 | T: Read, 157 | { 158 | type Item = Result; 159 | 160 | fn next(&mut self) -> Option { 161 | trace!("polling {} {}", self.index, self.state.is_some()); 162 | 163 | let state = self.state.clone()?; 164 | let mut state = state 165 | .try_lock() 166 | .map_err(|e| Error::TryLockError(e.to_string())) 167 | .ok()?; 168 | let is_file = self.filename.is_some(); 169 | 170 | match state.next().and_then(Result::ok) { 171 | None => { 172 | trace!("polled {}", self.index); 173 | drop(self.state.take()); 174 | None 175 | } 176 | Some(buf) => { 177 | let l = buf.len(); 178 | 179 | if is_file { 180 | if let Some(max) = state.limits.checked_file_size(self.length + l) { 181 | return Some(Err(Error::FileTooLarge(max))); 182 | } 183 | } else if let Some(max) = state.limits.checked_field_size(self.length + l) { 184 | return Some(Err(Error::FieldTooLarge(max))); 185 | } 186 | 187 | self.length += l; 188 | trace!("polled bytes {}/{}", buf.len(), self.length); 189 | Some(Ok(buf)) 190 | } 191 | } 192 | } 193 | } 194 | 195 | /// Reads form-data from request payload body, then yields `Field` 196 | impl Iterator for FormData 197 | where 198 | T: Read, 199 | { 200 | type Item = Result>; 201 | 202 | fn next(&mut self) -> Option { 203 | let mut state = self 204 | .state 205 | .try_lock() 206 | .map_err(|e| Error::TryLockError(e.to_string())) 207 | .ok()?; 208 | 209 | match state.next()? { 210 | Err(e) => Some(Err(e)), 211 | Ok(buf) => { 212 | trace!("parse part"); 213 | 214 | // too many parts 215 | if let Some(max) = state.limits.checked_parts(state.total + 1) { 216 | return Some(Err(Error::PartsTooMany(max))); 217 | } 218 | 219 | // invalid part header 220 | let Ok(mut headers) = parse_part_headers(&buf) else { 221 | return Some(Err(Error::InvalidHeader)); 222 | }; 223 | 224 | // invalid content disposition 225 | let Some((name, filename)) = headers 226 | .remove(CONTENT_DISPOSITION) 227 | .as_ref() 228 | .map(HeaderValue::as_bytes) 229 | .map(parse_content_disposition) 230 | .and_then(Result::ok) 231 | else { 232 | return Some(Err(Error::InvalidContentDisposition)); 233 | }; 234 | 235 | // field name is too long 236 | if let Some(max) = state.limits.checked_field_name_size(name.len()) { 237 | return Some(Err(Error::FieldNameTooLong(max))); 238 | } 239 | 240 | if filename.is_some() { 241 | // files too many 242 | if let Some(max) = state.limits.checked_files(state.files + 1) { 243 | return Some(Err(Error::FilesTooMany(max))); 244 | } 245 | state.files += 1; 246 | } else { 247 | // fields too many 248 | if let Some(max) = state.limits.checked_fields(state.fields + 1) { 249 | return Some(Err(Error::FieldsTooMany(max))); 250 | } 251 | state.fields += 1; 252 | } 253 | 254 | // yields `Field` 255 | let mut field = Field::empty(); 256 | 257 | field.name = name; 258 | field.filename = filename; 259 | field.index = state.index(); 260 | field.content_type = parse_content_type(headers.remove(CONTENT_TYPE).as_ref()); 261 | field.state_mut().replace(self.state()); 262 | 263 | if !headers.is_empty() { 264 | field.headers_mut().replace(headers); 265 | } 266 | 267 | Some(Ok(field)) 268 | } 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use http::header::{HeaderMap, HeaderName, HeaderValue}; 2 | use httparse::{parse_headers, Status, EMPTY_HEADER}; 3 | 4 | use crate::{Error, Result}; 5 | 6 | pub(crate) const MAX_HEADERS: usize = 8 * 2; 7 | pub(crate) const DASHES: [u8; 2] = [b'-', b'-']; // `--` 8 | pub(crate) const CRLF: [u8; 2] = [b'\r', b'\n']; // `\r\n` 9 | pub(crate) const CRLFS: [u8; 4] = [b'\r', b'\n', b'\r', b'\n']; // `\r\n\r\n` 10 | 11 | const NAME: &[u8; 4] = b"name"; 12 | const FILE_NAME: &[u8; 8] = b"filename"; 13 | const FORM_DATA: &[u8; 9] = b"form-data"; 14 | const SHORTEST_CONTENT_DISPOSITION: &[u8; 19] = b"form-data; name=\"s\""; 15 | 16 | pub(crate) fn parse_content_type(header: Option<&HeaderValue>) -> Option { 17 | header 18 | .map(HeaderValue::to_str) 19 | .and_then(Result::ok) 20 | .map(str::parse) 21 | .and_then(Result::ok) 22 | } 23 | 24 | pub(crate) fn parse_part_headers(bytes: &[u8]) -> Result { 25 | let mut headers = [EMPTY_HEADER; MAX_HEADERS]; 26 | match parse_headers(bytes, &mut headers) { 27 | Ok(Status::Complete((_, hs))) => { 28 | let len = hs.len(); 29 | let mut header_map = HeaderMap::with_capacity(len); 30 | for h in hs.iter().take(len) { 31 | header_map.append( 32 | HeaderName::from_bytes(h.name.as_bytes()).map_err(|_| Error::InvalidHeader)?, 33 | HeaderValue::from_bytes(h.value).map_err(|_| Error::InvalidHeader)?, 34 | ); 35 | } 36 | Ok(header_map) 37 | } 38 | Ok(Status::Partial) | Err(_) => Err(Error::InvalidHeader), 39 | } 40 | } 41 | 42 | #[allow(clippy::many_single_char_names)] 43 | pub(crate) fn parse_content_disposition(hv: &[u8]) -> Result<(String, Option)> { 44 | if hv.len() < SHORTEST_CONTENT_DISPOSITION.len() { 45 | return Err(Error::InvalidContentDisposition); 46 | } 47 | 48 | let mut i = 9; 49 | let form_data = &hv[0..i]; 50 | 51 | if form_data != FORM_DATA { 52 | return Err(Error::InvalidContentDisposition); 53 | } 54 | 55 | let mut j = i; 56 | let mut p = 0; 57 | let mut v = Vec::<(&[u8], &[u8])>::with_capacity(2); 58 | 59 | v.push((form_data, &[])); 60 | 61 | loop { 62 | if i == hv.len() { 63 | if p == 1 { 64 | if let Some(e) = v.last_mut() { 65 | e.1 = &hv[if hv[j] == b'"' && hv[i - 1] == b'"' { 66 | j + 1..i - 1 67 | } else { 68 | j..i 69 | }]; 70 | } 71 | } 72 | break; 73 | } 74 | 75 | let b = hv[i]; 76 | 77 | match b { 78 | b';' => { 79 | if p == 1 { 80 | if let Some(e) = v.last_mut() { 81 | e.1 = &hv[if hv[j] == b'"' && hv[i - 1] == b'"' { 82 | j + 1..i - 1 83 | } else { 84 | j..i 85 | }]; 86 | } 87 | p = 0; 88 | } 89 | i += 1; 90 | j = i; 91 | } 92 | b' ' => { 93 | i += 1; 94 | if p == 0 { 95 | j = i; 96 | } 97 | } 98 | b'=' => { 99 | v.push((&hv[j..i], &[])); 100 | i += 1; 101 | j = i; 102 | p = 1; 103 | } 104 | // b'\r' => { 105 | // if p == 1 { 106 | // if let Some(mut e) = v.last_mut() { 107 | // e.1 = &hv[j..i]; 108 | // } 109 | // p = 0; 110 | // } 111 | // i += 1; 112 | // } 113 | // b'\n' => { 114 | // if i - j == 1 { 115 | // break; 116 | // } 117 | // } 118 | _ => { 119 | i += 1; 120 | } 121 | } 122 | } 123 | 124 | // name 125 | if v[1].0 == NAME && !v[1].1.is_empty() { 126 | return Ok(( 127 | String::from_utf8_lossy(v[1].1).to_string(), 128 | if v.len() > 2 && v[2].0 == FILE_NAME { 129 | Some(String::from_utf8_lossy(v[2].1).to_string()) 130 | } else { 131 | None 132 | }, 133 | )); 134 | } 135 | 136 | Err(Error::InvalidContentDisposition) 137 | } 138 | -------------------------------------------------------------------------------- /tests/fixtures/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viz-rs/form-data/40f44b407f3d24dc5636d7edcb917b02e8d8911e/tests/fixtures/empty.txt -------------------------------------------------------------------------------- /tests/fixtures/filename-with-space.txt: -------------------------------------------------------------------------------- 1 | --------------------------d74496d66958873e 2 | Content-Disposition: form-data; name="person" 3 | 4 | anonymous 5 | --------------------------d74496d66958873e 6 | Content-Disposition: form-data; name="secret"; filename="foo bar.txt" 7 | Content-Type: text/plain 8 | 9 | contents of the file 10 | --------------------------d74496d66958873e-- -------------------------------------------------------------------------------- /tests/fixtures/files/empty.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viz-rs/form-data/40f44b407f3d24dc5636d7edcb917b02e8d8911e/tests/fixtures/files/empty.dat -------------------------------------------------------------------------------- /tests/fixtures/files/large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viz-rs/form-data/40f44b407f3d24dc5636d7edcb917b02e8d8911e/tests/fixtures/files/large.jpg -------------------------------------------------------------------------------- /tests/fixtures/files/medium.dat: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | ########################################################################## 3 | ########################################################################## 4 | ########################################################################## 5 | ########################################################################## 6 | 7 | .M 8 | .:AMMO: 9 | .:AMMMMMHIIIHMMM. 10 | .... .AMMMMMMMMMMMHHHMHHMMMML:AMF" 11 | .:MMMMMLAMMMMMMMHMMMMMMHHIHHIIIHMMMML. 12 | "WMMMMMMMMMMMMMMMMMMH:::::HMMMMMMHII:. 13 | .AMMMMMMMHHHMMMMMMMMMMHHHHHMMMMMMMMMAMMMHHHHL. 14 | .MMMMMMMMMMHHMMMMMMMMHHHHMMMMMMMMMMMMMHTWMHHHHHML 15 | .MMMMMMMMMMMMMMMMMMMHHHHHHHHHMHMMHHHHIII:::HMHHHHMM. 16 | .MMMMMMMMMMMMMMMMMMMMMMHHHHHHMHHHHHHIIIIIIIIHMHHHHHM. 17 | MMMMMMMMMMMMMMMMMHHMMHHHHHIIIHHH::IIHHII:::::IHHHHHHHL 18 | "MMMMMMMMMMMMMMMMHIIIHMMMMHHIIHHLI::IIHHHHIIIHHHHHHHHML 19 | .MMMMMMMMMMMMMM"WMMMHHHMMMMMMMMMMMLHHHMMMMMMHHHHHHHHHHH 20 | .MMMMMMMMMMMWWMW ""YYHMMMMMMMMMMMMF""HMMMMMMMMMHHHHHHHH. 21 | .MMMMMMMMMM W" V W"WMMMMMHHHHHHHHHH 22 | "MMMMMMMMMM". "WHHHMH"HHHHHHL 23 | MMMMMMMMMMF . IHHHHH. 24 | MMMMMMMMMM . . HHHHHHH 25 | MMMMMMMMMF. . . . HHHHHHH. 26 | MMMMMMMMM . ,AWMMMMML. .. . . HHHHHHH. 27 | :MMMMMMMMM". . F"' 'WM:. ,::HMMA, . . HHHHMMM 28 | :MMMMMMMMF. . ." WH.. AMM"' " . . HHHMMMM 29 | MMMMMMMM . . ,;AAAHHWL".. .:' HHHHHHH 30 | MMMMMMM:. . . -MK"OTO L :I.. ...:HMA-. "HHHHHH 31 | ,:IIIILTMMMMI::. L,,,,. ::I.. .. K"OTO"ML 'HHHHHH 32 | LHT::LIIIIMMI::. . '""'.IHH:.. .. :.,,,, ' HMMMH: HLI' 33 | ILTT::"IIITMII::. . .IIII. . '"""" ' MMMFT:::. 34 | HML:::WMIINMHI:::.. . .:I. . . . . ' .M"'.....I. 35 | "HWHINWI:.'.HHII::.. .HHI .II. . . . . :M.',, ..I: 36 | "MLI"ML': :HHII::... MMHHL ::::: . :.. .'.'.'HHTML.II: 37 | "MMLIHHWL:IHHII::....:I:" :MHHWHI:...:W,," '':::. ..' ":.HH:II: 38 | "MMMHITIIHHH:::::IWF" """T99"' '"" '.':II:..'.'..' I'.HHIHI' 39 | YMMHII:IHHHH:::IT.. . . ... . . ''THHI::.'.' .;H.""."H" 40 | HHII:MHHI"::IWWL . . . . . HH"HHHIIHHH":HWWM" 41 | """ MMHI::HY""ML, ... . .. :" :HIIIIIILTMH" 42 | MMHI:.' 'HL,,,,,,,,..,,,......,:" . ''::HH "HWW 43 | 'MMH:.. . 'MMML,: """MM""""MMM" .'.IH'"MH" 44 | "MMHL.. .. "MMMMMML,MM,HMMMF . .IHM" 45 | "MMHHL .. "MMMMMMMMMMMM" . . '.IHF' 46 | 'MMMML .. "MMMMMMMM" . .'HMF 47 | HHHMML. .'MMF" 48 | IHHHHHMML. .'HMF" 49 | HHHHHHITMML. .'IF.. 50 | "HHHHHHIITML,. ..:F... 51 | 'HHHHHHHHHMMWWWWWW::"...... 52 | HHHHHHHMMMMMMF"'........ 53 | HHHHHHHHHH............ 54 | HHHHHHHH........... 55 | HHHHIII.......... 56 | HHIII.......... 57 | HII......... 58 | "H........ 59 | ...... 60 | 61 | ########################################################################## 62 | ########################################################################## 63 | ########################################################################## 64 | ########################################################################## 65 | ########################################################################## 66 | 67 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 68 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!` `4!!!!!!!!!!~4!!!!!!!!!!!!!!!!! 69 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! <~: ~!!!~ .. 4!!!!!!!!!!!!!!! 70 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ~~~~~~~ ' ud$$$$$ !!!!!!!!!!!!!!! 71 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ~~~~~~~~~: ?$$$$$$$$$ !!!!!!!!!!!!!! 72 | !!!!!!!!!!!` ``~!!!!!!!!!!!!!! ~~~~~ "*$$$$$k `!!!!!!!!!!!!! 73 | !!!!!!!!!! $$$$$bu. '~!~` . '~~~~ :~~~~ `4!!!!!!!!!!! 74 | !!!!!!!!! $$$$$$$$$$$c .zW$$$$$E ~~~~ ~~~~~~~~ ~~~~~: '!!!!!!!!!! 75 | !!!!!!!!! d$$$$$$$$$$$$$$$$$$$$$$E ~~~~~ '~~~~~~~~ ~~~~~ !!!!!!!!!! 76 | !!!!!!!!> 9$$$$$$$$$$$$$$$$$$$$$$$ '~~~~~~~ '~~~~~~~~ ~~~~ !!!!!!!!!! 77 | !!!!!!!!> $$$$$$$$$$$$$$$$$$$$$$$$b ~~~ '~~~~~~~ '~~~ '!!!!!!!!!! 78 | !!!!!!!!> $$$$$$$$$$$$$$$$$$$$$$$$$$$cuuue$$N. ~ ~~~ !!!!!!!!!!! 79 | !!!!!!!!! **$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$Ne ~~~~~~~~ `!!!!!!!!!!! 80 | !!!!!!!!! J$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$N ~~~~~ zL '!!!!!!!!!! 81 | !!!!!!!! d$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$c z$$$c `!!!!!!!!! 82 | !!!!!!!> <$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$> 4!!!!!!!! 83 | !!!!!!! $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ !!!!!!!! 84 | !!!!!!! <$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$*" ....:!! 85 | !!!!!!~ 9$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$e@$N '!!!!!!! 86 | !!!!!! 9$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ !!!!!!! 87 | !!!!!! $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$""$$$$$$$$$$$~ ~~4!!!! 88 | !!!!!! 9$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$Lue :::!!!! 89 | !!!!!!> 9$$$$$$$$$$$$" '$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$ !!!!!!! 90 | !!!!!!! '$$*$$$$$$$$E '$$$$$$$$$$$$$$$$$$$$$$$$$$$u.@$$$$$$$$$E '!!!!!!! 91 | !!!!~` .eeW$$$$$$$$ :$$$$$$$$$$$$$***$$$$$$$$$$$$$$$$$$$$u. `~!!!!! 92 | !!> .:!h '$$$$$$$$$$$$ed$$$$$$$$$$$$Fz$$b $$$$$$$$$$$$$$$$$$$$$F '!h. !!! 93 | !!!!!!!!L '$**$$$$$$$$$$$$$$$$$$$$$$ *$$$ $$$$$$$$$$$$$$$$$$$$F !!!!!!!!! 94 | !!!!!!!!! d$$$$$$$$$$$$$$$$$$$$$$$$buud$$$$$$$$$$$$$$$$$$$$" !!!!!!!!!! 95 | !!!!!!! . | 11 | . \ \ | | / /__\ \ . | _/ . 12 | . ________> | | | . / \ | |\ \_______ . 13 | | / | | / ______ \ | | \ | 14 | |___________/ |___| /____/ \____\ |___| \__________| . 15 | . ____ __ . _____ ____ . __________ . _________ 16 | \ \ / \ / / / \ | \ / | . 17 | \ \/ \/ / / \ | ___ | / ______| . 18 | \ / / /\ \ . | |___> | \ \ 19 | . \ / / /__\ \ | _/. \ \ + 20 | \ /\ / / \ | |\ \______> | . 21 | \ / \ / / ______ \ | | \ / . 22 | . . \/ \/ /____/ \____\ |___| \____________/ LS 23 | . . 24 | . . . . . 25 | . . . 26 | -------------------------------------------------------------------------------- /tests/fixtures/files/small1.dat: -------------------------------------------------------------------------------- 1 | ____ 2 | \__/ # ## 3 | `( `^=_ p _###_ 4 | c / ) | / 5 | _____- //^---~ _c 3 6 | / ----^\ /^_\ / --,- 7 | ( | | O_| \\_/ ,/ 8 | | | | / \| `-- / 9 | (((G |-----| 10 | //-----\\ 11 | // \\ 12 | / | | ^| 13 | | | | | 14 | |____| |____| 15 | /______) (_____\ -------------------------------------------------------------------------------- /tests/fixtures/files/tiny0.dat: -------------------------------------------------------------------------------- 1 | ROFL:ROFL:ROFL:ROFL 2 | _^___ 3 | L __/ [] \ 4 | LOL===__ \ 5 | L \________] 6 | I I 7 | --------/ -------------------------------------------------------------------------------- /tests/fixtures/files/tiny1.dat: -------------------------------------------------------------------------------- 1 | Result<()> { 18 | let body = Limited::random(File::open("tests/fixtures/rfc7578-example.txt").await?); 19 | let mut form = FormData::new(body, "AaB03x"); 20 | 21 | while let Some(mut field) = form.try_next().await? { 22 | let mut buffer = BytesMut::new(); 23 | while let Some(buf) = field.try_next().await? { 24 | buffer.extend_from_slice(&buf); 25 | } 26 | assert_eq!(buffer.len(), "Joe owes =E2=82=AC100.".len()); 27 | } 28 | 29 | let state = form.state(); 30 | let state = state 31 | .try_lock() 32 | .map_err(|e| Error::TryLockError(e.to_string()))?; 33 | 34 | assert!(state.eof()); 35 | assert_eq!(state.total(), 1); 36 | assert_eq!(state.len(), 178); 37 | 38 | Ok(()) 39 | } 40 | 41 | #[tokio::test] 42 | async fn empty() -> Result<()> { 43 | let body = Limited::random(File::open("tests/fixtures/empty.txt").await?); 44 | 45 | let mut form = FormData::new(body, ""); 46 | 47 | while let Some(mut field) = form.try_next().await? { 48 | let mut buffer = BytesMut::new(); 49 | while let Some(buf) = field.try_next().await? { 50 | buffer.extend_from_slice(&buf); 51 | } 52 | assert_eq!(buffer.len(), 0); 53 | } 54 | 55 | let state = form.state(); 56 | let state = state 57 | .try_lock() 58 | .map_err(|e| Error::TryLockError(e.to_string()))?; 59 | 60 | assert!(state.eof()); 61 | assert_eq!(state.total(), 0); 62 | assert_eq!(state.len(), 0); 63 | 64 | Ok(()) 65 | } 66 | 67 | #[tokio::test] 68 | async fn filename_with_space() -> Result<()> { 69 | let body = Limited::random(File::open("tests/fixtures/filename-with-space.txt").await?); 70 | let limit = body.limit(); 71 | 72 | let mut form = FormData::new(body, "------------------------d74496d66958873e"); 73 | form.set_max_buf_size(limit)?; 74 | 75 | while let Some(mut field) = form.try_next().await? { 76 | assert!(!field.consumed()); 77 | assert_eq!(field.length, 0); 78 | 79 | let mut buffer = BytesMut::new(); 80 | while let Some(buf) = field.try_next().await? { 81 | buffer.extend_from_slice(&buf); 82 | 83 | match field.index { 84 | 0 => { 85 | assert_eq!(field.name, "person"); 86 | assert_eq!(field.content_type, None); 87 | assert_eq!(field.length, 9); 88 | assert_eq!(buffer, "anonymous"); 89 | } 90 | 1 => { 91 | assert_eq!(field.name, "secret"); 92 | assert_eq!(field.filename, Some("foo bar.txt".to_string())); 93 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 94 | assert_eq!(field.length, 20); 95 | assert_eq!(buffer, "contents of the file"); 96 | } 97 | _ => {} 98 | } 99 | } 100 | } 101 | 102 | let state = form.state(); 103 | let state = state 104 | .try_lock() 105 | .map_err(|e| Error::TryLockError(e.to_string()))?; 106 | 107 | assert!(state.eof()); 108 | assert_eq!(state.total(), 2); 109 | assert_eq!(state.len(), 313); 110 | 111 | Ok(()) 112 | } 113 | 114 | #[tokio::test] 115 | async fn many() -> Result<()> { 116 | let body = Limited::random(File::open("tests/fixtures/many.txt").await?); 117 | let limit = body.limit(); 118 | 119 | let mut form = FormData::new(body, "----WebKitFormBoundaryWLHCs9qmcJJoyjKR"); 120 | form.set_max_buf_size(limit)?; 121 | 122 | while let Some(mut field) = form.try_next().await? { 123 | assert!(!field.consumed()); 124 | assert_eq!(field.length, 0); 125 | 126 | let mut buffer = BytesMut::new(); 127 | while let Some(buf) = field.try_next().await? { 128 | buffer.extend_from_slice(&buf); 129 | } 130 | 131 | match field.index { 132 | 0 => { 133 | assert_eq!(field.name, "_method"); 134 | assert_eq!(field.filename, None); 135 | assert_eq!(field.content_type, None); 136 | assert_eq!(field.length, 3); 137 | assert_eq!(buffer, "put"); 138 | } 139 | 1 => { 140 | assert_eq!(field.name, "profile[blog]"); 141 | assert_eq!(field.filename, None); 142 | assert_eq!(field.content_type, None); 143 | assert_eq!(field.length, 0); 144 | assert_eq!(buffer, ""); 145 | } 146 | 2 => { 147 | assert_eq!(field.name, "profile[public_email]"); 148 | assert_eq!(field.filename, None); 149 | assert_eq!(field.content_type, None); 150 | assert_eq!(field.length, 0); 151 | assert_eq!(buffer, ""); 152 | } 153 | 3 => { 154 | assert_eq!(field.name, "profile[interests]"); 155 | assert_eq!(field.filename, None); 156 | assert_eq!(field.content_type, None); 157 | assert_eq!(field.length, 0); 158 | assert_eq!(buffer, ""); 159 | } 160 | 4 => { 161 | assert_eq!(field.name, "profile[bio]"); 162 | assert_eq!(field.filename, None); 163 | assert_eq!(field.content_type, None); 164 | assert_eq!(field.length, 16); 165 | assert_eq!(buffer, "hello\r\n\r\n\"quote\""); 166 | } 167 | 5 => { 168 | assert_eq!(field.name, "media"); 169 | assert_eq!(field.filename, Some(String::new())); 170 | assert_eq!(field.content_type, Some(mime::APPLICATION_OCTET_STREAM)); 171 | assert_eq!(field.length, 0); 172 | assert_eq!(buffer, ""); 173 | } 174 | 6 => { 175 | assert_eq!(field.name, "commit"); 176 | assert_eq!(field.filename, None); 177 | assert_eq!(field.content_type, None); 178 | assert_eq!(field.length, 4); 179 | assert_eq!(buffer, "Save"); 180 | } 181 | _ => {} 182 | } 183 | 184 | assert_eq!(field.length, buffer.len()); 185 | assert!(field.consumed()); 186 | 187 | tracing::info!("{:#?}", field); 188 | } 189 | 190 | let state = form.state(); 191 | let state = state 192 | .try_lock() 193 | .map_err(|e| Error::TryLockError(e.to_string()))?; 194 | 195 | assert!(state.eof()); 196 | assert_eq!(state.total(), 7); 197 | assert_eq!(state.len(), 809); 198 | 199 | Ok(()) 200 | } 201 | 202 | #[tokio::test] 203 | async fn many_noend() -> Result<()> { 204 | let body = Limited::random(File::open("tests/fixtures/many-noend.txt").await?); 205 | let limit = body.limit(); 206 | 207 | let mut form = FormData::new(body, "----WebKitFormBoundaryWLHCs9qmcJJoyjKR"); 208 | form.set_max_buf_size(limit)?; 209 | 210 | while let Some(mut field) = form.try_next().await? { 211 | assert!(!field.consumed()); 212 | assert_eq!(field.length, 0); 213 | 214 | let mut buffer = BytesMut::new(); 215 | while let Some(buf) = field.try_next().await? { 216 | buffer.extend_from_slice(&buf); 217 | } 218 | 219 | match field.index { 220 | 0 => { 221 | assert_eq!(field.name, "_method"); 222 | assert_eq!(field.filename, None); 223 | assert_eq!(field.content_type, None); 224 | assert_eq!(field.length, 3); 225 | assert_eq!(buffer, "put"); 226 | } 227 | 1 => { 228 | assert_eq!(field.name, "profile[blog]"); 229 | assert_eq!(field.filename, None); 230 | assert_eq!(field.content_type, None); 231 | assert_eq!(field.length, 0); 232 | assert_eq!(buffer, ""); 233 | } 234 | 2 => { 235 | assert_eq!(field.name, "profile[public_email]"); 236 | assert_eq!(field.filename, None); 237 | assert_eq!(field.content_type, None); 238 | assert_eq!(field.length, 0); 239 | assert_eq!(buffer, ""); 240 | } 241 | 3 => { 242 | assert_eq!(field.name, "profile[interests]"); 243 | assert_eq!(field.filename, None); 244 | assert_eq!(field.content_type, None); 245 | assert_eq!(field.length, 0); 246 | assert_eq!(buffer, ""); 247 | } 248 | 4 => { 249 | assert_eq!(field.name, "profile[bio]"); 250 | assert_eq!(field.filename, None); 251 | assert_eq!(field.content_type, None); 252 | assert_eq!(field.length, 16); 253 | assert_eq!(buffer, "hello\r\n\r\n\"quote\""); 254 | } 255 | 5 => { 256 | assert_eq!(field.name, "commit"); 257 | assert_eq!(field.filename, None); 258 | assert_eq!(field.content_type, None); 259 | assert_eq!(field.length, 4); 260 | assert_eq!(buffer, "Save"); 261 | } 262 | 6 => { 263 | assert_eq!(field.name, "media"); 264 | assert_eq!(field.filename, Some(String::new())); 265 | assert_eq!(field.content_type, Some(mime::APPLICATION_OCTET_STREAM)); 266 | assert_eq!(field.length, 0); 267 | assert_eq!(buffer, ""); 268 | } 269 | _ => {} 270 | } 271 | 272 | assert_eq!(field.length, buffer.len()); 273 | assert!(field.consumed()); 274 | 275 | tracing::info!("{:#?}", field); 276 | } 277 | 278 | let state = form.state(); 279 | let state = state 280 | .try_lock() 281 | .map_err(|e| Error::TryLockError(e.to_string()))?; 282 | 283 | assert!(state.eof()); 284 | assert_eq!(state.total(), 7); 285 | assert_eq!(state.len(), 767); 286 | 287 | Ok(()) 288 | } 289 | 290 | #[tokio::test] 291 | async fn headers() -> Result<()> { 292 | let body = Limited::random(File::open("tests/fixtures/headers.txt").await?); 293 | let limit = body.limit(); 294 | 295 | let mut form = FormData::new(body, "boundary"); 296 | form.set_max_buf_size(limit)?; 297 | 298 | while let Some(mut field) = form.try_next().await? { 299 | assert!(!field.consumed()); 300 | assert_eq!(field.length, 0); 301 | 302 | let mut buffer = BytesMut::new(); 303 | while let Some(buf) = field.try_next().await? { 304 | buffer.extend_from_slice(&buf); 305 | } 306 | 307 | if field.index == 0 { 308 | assert_eq!(field.name, "operations"); 309 | assert_eq!(field.filename, Some("graphql.json".into())); 310 | assert_eq!(field.content_type, Some(mime::APPLICATION_JSON)); 311 | assert_eq!(field.length, 13); 312 | let mut headers = HeaderMap::new(); 313 | headers.append(http::header::CONTENT_LENGTH, 13.into()); 314 | assert_eq!(field.headers, Some(headers)); 315 | assert_eq!(buffer, "{\"query\": \"\"}"); 316 | } 317 | 318 | assert_eq!(field.length, buffer.len()); 319 | assert!(field.consumed()); 320 | 321 | tracing::info!("{:#?}", field); 322 | } 323 | 324 | let state = form.state(); 325 | let state = state 326 | .try_lock() 327 | .map_err(|e| Error::TryLockError(e.to_string()))?; 328 | 329 | assert!(state.eof()); 330 | assert_eq!(state.total(), 1); 331 | assert_eq!(state.len(), 175); 332 | 333 | Ok(()) 334 | } 335 | 336 | #[tokio::test] 337 | async fn sample() -> Result<()> { 338 | let body = Limited::random(File::open("tests/fixtures/sample.txt").await?); 339 | let limit = body.limit(); 340 | 341 | let mut form = FormData::new(body, "--------------------------434049563556637648550474"); 342 | form.set_max_buf_size(limit)?; 343 | 344 | while let Some(mut field) = form.try_next().await? { 345 | assert!(!field.consumed()); 346 | assert_eq!(field.length, 0); 347 | 348 | let mut buffer = BytesMut::new(); 349 | while let Some(buf) = field.try_next().await? { 350 | buffer.extend_from_slice(&buf); 351 | } 352 | 353 | match field.index { 354 | 0 => { 355 | assert_eq!(field.name, "foo"); 356 | assert_eq!(field.filename, None); 357 | assert_eq!(field.content_type, Some(mime::APPLICATION_OCTET_STREAM)); 358 | assert_eq!(field.length, 3); 359 | assert_eq!(buffer, "foo"); 360 | } 361 | 1 => { 362 | assert_eq!(field.name, "bar"); 363 | assert_eq!(field.filename, None); 364 | assert_eq!(field.content_type, Some(mime::APPLICATION_OCTET_STREAM)); 365 | assert_eq!(field.length, 3); 366 | assert_eq!(buffer, "bar"); 367 | } 368 | 2 => { 369 | assert_eq!(field.name, "file"); 370 | assert_eq!(field.filename, Some("tsconfig.json".into())); 371 | assert_eq!(field.content_type, Some(mime::APPLICATION_OCTET_STREAM)); 372 | assert_eq!(field.length, 233); 373 | assert_eq!( 374 | String::from_utf8_lossy(&buffer).replacen("\r\n", "\n", 12), 375 | r#"{ 376 | "compilerOptions": { 377 | "target": "es2018", 378 | "baseUrl": ".", 379 | "paths": { 380 | "deno": ["./deno.d.ts"], 381 | "https://*": ["../../.deno/deps/https/*"], 382 | "http://*": ["../../.deno/deps/http/*"] 383 | } 384 | } 385 | } 386 | "# 387 | ); 388 | } 389 | 3 => { 390 | assert_eq!(field.name, "file2"); 391 | assert_eq!(field.filename, Some("中文.json".into())); 392 | assert_eq!(field.content_type, Some(mime::APPLICATION_OCTET_STREAM)); 393 | assert_eq!(field.length, 28); 394 | assert_eq!(buffer, "{\r\n \"test\": \"filename\"\r\n}\r\n"); 395 | } 396 | 4 => { 397 | assert_eq!(field.name, "crab"); 398 | assert_eq!(field.filename, None); 399 | assert_eq!(field.content_type, None); 400 | assert_eq!(buffer, ""); 401 | assert_eq!(field.length, 0); 402 | } 403 | _ => {} 404 | } 405 | 406 | assert_eq!(field.length, buffer.len()); 407 | assert!(field.consumed()); 408 | 409 | tracing::info!("{:#?}", field); 410 | } 411 | 412 | let state = form.state(); 413 | let state = state 414 | .try_lock() 415 | .map_err(|e| Error::TryLockError(e.to_string()))?; 416 | 417 | assert!(state.eof()); 418 | assert_eq!(state.total(), 5); 419 | assert_eq!(state.len(), 1043); 420 | 421 | Ok(()) 422 | } 423 | 424 | #[tokio::test] 425 | async fn sample_lf() -> Result<()> { 426 | let body = Limited::random(File::open("tests/fixtures/sample.lf.txt").await?); 427 | let limit = body.limit(); 428 | 429 | let mut form = FormData::new(body, "--------------------------434049563556637648550474"); 430 | form.set_max_buf_size(limit)?; 431 | 432 | while let Some(mut field) = form.try_next().await? { 433 | assert!(!field.consumed()); 434 | assert_eq!(field.length, 0); 435 | 436 | let mut buffer = BytesMut::new(); 437 | while let Some(buf) = field.try_next().await? { 438 | buffer.extend_from_slice(&buf); 439 | } 440 | 441 | assert_eq!(field.length, buffer.len()); 442 | assert!(field.consumed()); 443 | 444 | tracing::info!("{:#?}", field); 445 | } 446 | 447 | let state = form.state(); 448 | let state = state 449 | .try_lock() 450 | .map_err(|e| Error::TryLockError(e.to_string()))?; 451 | 452 | assert!(state.eof()); 453 | assert_eq!(state.total(), 0); 454 | assert_eq!(state.len(), 0); 455 | 456 | Ok(()) 457 | } 458 | 459 | #[tokio::test] 460 | async fn graphql_random() -> Result<()> { 461 | let body = Limited::random(File::open("tests/fixtures/graphql.txt").await?); 462 | let limit = body.limit(); 463 | 464 | let mut form = FormData::new(body, "------------------------627436eaefdbc285"); 465 | form.set_max_buf_size(limit)?; 466 | 467 | while let Some(mut field) = form.try_next().await? { 468 | assert!(!field.consumed()); 469 | assert_eq!(field.length, 0); 470 | 471 | let mut buffer = BytesMut::new(); 472 | while let Some(buf) = field.try_next().await? { 473 | buffer.extend_from_slice(&buf); 474 | } 475 | 476 | match field.index { 477 | 0 => { 478 | assert_eq!(field.name, "operations"); 479 | assert_eq!(field.filename, None); 480 | assert_eq!(field.content_type, None); 481 | assert_eq!(field.length, 236); 482 | assert_eq!(buffer, "[{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }, { \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }]"); 483 | } 484 | 1 => { 485 | assert_eq!(field.name, "map"); 486 | assert_eq!(field.filename, None); 487 | assert_eq!(field.content_type, None); 488 | assert_eq!(field.length, 89); 489 | assert_eq!(buffer, "{ \"0\": [\"0.variables.file\"], \"1\": [\"1.variables.files.0\"], \"2\": [\"1.variables.files.1\"] }"); 490 | } 491 | 2 => { 492 | assert_eq!(field.name, "0"); 493 | assert_eq!(field.filename, Some("a.txt".into())); 494 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 495 | assert_eq!(field.length, 21); 496 | assert_eq!(buffer, "Alpha file content.\r\n"); 497 | } 498 | 3 => { 499 | assert_eq!(field.name, "1"); 500 | assert_eq!(field.filename, Some("b.txt".into())); 501 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 502 | assert_eq!(field.length, 21); 503 | assert_eq!(buffer, "Bravo file content.\r\n"); 504 | } 505 | 4 => { 506 | assert_eq!(field.name, "2"); 507 | assert_eq!(field.filename, Some("c.txt".into())); 508 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 509 | assert_eq!(field.length, 23); 510 | assert_eq!(buffer, "Charlie file content.\r\n"); 511 | } 512 | _ => {} 513 | } 514 | 515 | assert_eq!(field.length, buffer.len()); 516 | assert!(field.consumed()); 517 | 518 | tracing::info!("{:#?}", field); 519 | } 520 | 521 | let state = form.state(); 522 | let state = state 523 | .try_lock() 524 | .map_err(|e| Error::TryLockError(e.to_string()))?; 525 | 526 | assert!(state.eof()); 527 | assert_eq!(state.total(), 5); 528 | assert_eq!(state.len(), 1027); 529 | 530 | Ok(()) 531 | } 532 | 533 | #[tokio::test] 534 | async fn graphql_1024() -> Result<()> { 535 | let body = Limited::random_with(File::open("tests/fixtures/graphql.txt").await?, 1024); 536 | // let body = Limited::new(File::open("tests/fixtures/graphql.txt").await?, 1033); 537 | let limit = body.limit(); 538 | 539 | let mut form = FormData::new(body, "------------------------627436eaefdbc285"); 540 | form.set_max_buf_size(limit)?; 541 | 542 | while let Some(mut field) = form.try_next().await? { 543 | assert!(!field.consumed()); 544 | assert_eq!(field.length, 0); 545 | 546 | let mut buffer = BytesMut::new(); 547 | while let Some(buf) = field.try_next().await? { 548 | buffer.extend_from_slice(&buf); 549 | } 550 | 551 | match field.index { 552 | 0 => { 553 | assert_eq!(field.name, "operations"); 554 | assert_eq!(field.filename, None); 555 | assert_eq!(field.content_type, None); 556 | assert_eq!(field.length, 236); 557 | assert_eq!(buffer, "[{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }, { \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }]"); 558 | } 559 | 1 => { 560 | assert_eq!(field.name, "map"); 561 | assert_eq!(field.filename, None); 562 | assert_eq!(field.content_type, None); 563 | assert_eq!(field.length, 89); 564 | assert_eq!(buffer, "{ \"0\": [\"0.variables.file\"], \"1\": [\"1.variables.files.0\"], \"2\": [\"1.variables.files.1\"] }"); 565 | } 566 | 2 => { 567 | assert_eq!(field.name, "0"); 568 | assert_eq!(field.filename, Some("a.txt".into())); 569 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 570 | assert_eq!(field.length, 21); 571 | assert_eq!(buffer, "Alpha file content.\r\n"); 572 | } 573 | 3 => { 574 | assert_eq!(field.name, "1"); 575 | assert_eq!(field.filename, Some("b.txt".into())); 576 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 577 | assert_eq!(field.length, 21); 578 | assert_eq!(buffer, "Bravo file content.\r\n"); 579 | } 580 | 4 => { 581 | assert_eq!(field.name, "2"); 582 | assert_eq!(field.filename, Some("c.txt".into())); 583 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 584 | assert_eq!(field.length, 23); 585 | assert_eq!(buffer, "Charlie file content.\r\n"); 586 | } 587 | _ => {} 588 | } 589 | 590 | assert_eq!(field.length, buffer.len()); 591 | assert!(field.consumed()); 592 | 593 | tracing::info!("{:#?}", field); 594 | } 595 | 596 | let state = form.state(); 597 | let state = state 598 | .try_lock() 599 | .map_err(|e| Error::TryLockError(e.to_string()))?; 600 | 601 | assert!(state.eof()); 602 | assert_eq!(state.total(), 5); 603 | assert_eq!(state.len(), 1027); 604 | 605 | Ok(()) 606 | } 607 | 608 | #[tokio::test] 609 | async fn graphql_1033() -> Result<()> { 610 | let body = Limited::new(File::open("tests/fixtures/graphql.txt").await?, 1033); 611 | let limit = body.limit(); 612 | 613 | let mut form = FormData::new(body, "------------------------627436eaefdbc285"); 614 | form.set_max_buf_size(limit)?; 615 | 616 | while let Some(mut field) = form.try_next().await? { 617 | assert!(!field.consumed()); 618 | assert_eq!(field.length, 0); 619 | 620 | let mut buffer = BytesMut::new(); 621 | while let Some(buf) = field.try_next().await? { 622 | buffer.extend_from_slice(&buf); 623 | } 624 | 625 | match field.index { 626 | 0 => { 627 | assert_eq!(field.name, "operations"); 628 | assert_eq!(field.filename, None); 629 | assert_eq!(field.content_type, None); 630 | assert_eq!(field.length, 236); 631 | assert_eq!(buffer, "[{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }, { \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }]"); 632 | } 633 | 1 => { 634 | assert_eq!(field.name, "map"); 635 | assert_eq!(field.filename, None); 636 | assert_eq!(field.content_type, None); 637 | assert_eq!(field.length, 89); 638 | assert_eq!(buffer, "{ \"0\": [\"0.variables.file\"], \"1\": [\"1.variables.files.0\"], \"2\": [\"1.variables.files.1\"] }"); 639 | } 640 | 2 => { 641 | assert_eq!(field.name, "0"); 642 | assert_eq!(field.filename, Some("a.txt".into())); 643 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 644 | assert_eq!(field.length, 21); 645 | assert_eq!(buffer, "Alpha file content.\r\n"); 646 | } 647 | 3 => { 648 | assert_eq!(field.name, "1"); 649 | assert_eq!(field.filename, Some("b.txt".into())); 650 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 651 | assert_eq!(field.length, 21); 652 | assert_eq!(buffer, "Bravo file content.\r\n"); 653 | } 654 | 4 => { 655 | assert_eq!(field.name, "2"); 656 | assert_eq!(field.filename, Some("c.txt".into())); 657 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 658 | assert_eq!(field.length, 23); 659 | assert_eq!(buffer, "Charlie file content.\r\n"); 660 | } 661 | _ => {} 662 | } 663 | 664 | assert_eq!(field.length, buffer.len()); 665 | assert!(field.consumed()); 666 | 667 | tracing::info!("{:#?}", field); 668 | } 669 | 670 | let state = form.state(); 671 | let state = state 672 | .try_lock() 673 | .map_err(|e| Error::TryLockError(e.to_string()))?; 674 | 675 | assert!(state.eof()); 676 | assert_eq!(state.total(), 5); 677 | assert_eq!(state.len(), 1027); 678 | 679 | Ok(()) 680 | } 681 | -------------------------------------------------------------------------------- /tests/hyper-body.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use async_fs::File; 3 | use bytes::BytesMut; 4 | use tempfile::tempdir; 5 | 6 | use futures_util::{ 7 | io::{self, AsyncReadExt, AsyncWriteExt}, 8 | stream::{self, TryStreamExt}, 9 | }; 10 | use http_body_util::StreamBody; 11 | 12 | use form_data::{Error, FormData}; 13 | 14 | #[path = "./lib/mod.rs"] 15 | mod lib; 16 | 17 | use lib::{tracing_init, Limited}; 18 | 19 | #[tokio::test] 20 | async fn hyper_body() -> Result<()> { 21 | tracing_init()?; 22 | 23 | let payload = File::open("tests/fixtures/graphql.txt").await?; 24 | let stream = Limited::random_with(payload, 256); 25 | let limit = stream.limit(); 26 | 27 | let body = StreamBody::new(stream); 28 | let mut form = FormData::new(body, "------------------------627436eaefdbc285"); 29 | form.set_max_buf_size(limit)?; 30 | 31 | while let Some(mut field) = form.try_next().await? { 32 | assert!(!field.consumed()); 33 | assert_eq!(field.length, 0); 34 | 35 | match field.index { 36 | 0 => { 37 | assert_eq!(field.name, "operations"); 38 | assert_eq!(field.filename, None); 39 | assert_eq!(field.content_type, None); 40 | 41 | // reads chunks 42 | let mut buffer = BytesMut::new(); 43 | while let Some(buf) = field.try_next().await? { 44 | buffer.extend_from_slice(&buf); 45 | } 46 | 47 | assert_eq!(buffer, "[{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }, { \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }]"); 48 | assert_eq!(field.length, buffer.len()); 49 | 50 | assert!(field.consumed()); 51 | 52 | tracing::info!("{:#?}", field); 53 | } 54 | 1 => { 55 | assert_eq!(field.name, "map"); 56 | assert_eq!(field.filename, None); 57 | assert_eq!(field.content_type, None); 58 | 59 | // reads bytes 60 | let buffer = field.bytes().await?; 61 | 62 | assert_eq!(buffer, "{ \"0\": [\"0.variables.file\"], \"1\": [\"1.variables.files.0\"], \"2\": [\"1.variables.files.1\"] }"); 63 | assert_eq!(field.length, buffer.len()); 64 | 65 | assert!(field.consumed()); 66 | 67 | tracing::info!("{:#?}", field); 68 | } 69 | 2 => { 70 | tracing::info!("{:#?}", field); 71 | 72 | assert_eq!(field.name, "0"); 73 | assert_eq!(field.filename, Some("a.txt".into())); 74 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 75 | 76 | let dir = tempdir()?; 77 | 78 | let filename = field.filename.as_ref().unwrap(); 79 | let filepath = dir.path().join(filename); 80 | 81 | let mut writer = File::create(&filepath).await?; 82 | 83 | let bytes = io::copy(field, &mut writer).await?; 84 | writer.close().await?; 85 | 86 | // async ? 87 | let metadata = std::fs::metadata(&filepath)?; 88 | assert_eq!(metadata.len(), bytes); 89 | 90 | let mut reader = File::open(&filepath).await?; 91 | let mut contents = Vec::new(); 92 | reader.read_to_end(&mut contents).await?; 93 | assert_eq!(contents, "Alpha file content.\r\n".as_bytes()); 94 | 95 | dir.close()?; 96 | } 97 | 3 => { 98 | assert_eq!(field.name, "1"); 99 | assert_eq!(field.filename, Some("b.txt".into())); 100 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 101 | 102 | let mut buffer = Vec::with_capacity(4); 103 | let bytes = field.read_to_end(&mut buffer).await?; 104 | 105 | assert_eq!(buffer, "Bravo file content.\r\n".as_bytes()); 106 | assert_eq!(field.length, bytes); 107 | assert_eq!(field.length, buffer.len()); 108 | 109 | tracing::info!("{:#?}", field); 110 | } 111 | 4 => { 112 | assert_eq!(field.name, "2"); 113 | assert_eq!(field.filename, Some("c.txt".into())); 114 | assert_eq!(field.content_type, Some(mime::TEXT_PLAIN)); 115 | 116 | let mut string = String::new(); 117 | let bytes = field.read_to_string(&mut string).await?; 118 | 119 | assert_eq!(string, "Charlie file content.\r\n"); 120 | assert_eq!(field.length, bytes); 121 | assert_eq!(field.length, string.len()); 122 | 123 | tracing::info!("{:#?}", field); 124 | } 125 | _ => {} 126 | } 127 | } 128 | 129 | let state = form.state(); 130 | let state = state 131 | .try_lock() 132 | .map_err(|e| Error::TryLockError(e.to_string()))?; 133 | 134 | assert!(state.eof()); 135 | assert_eq!(state.total(), 5); 136 | assert_eq!(state.len(), 1027); 137 | 138 | Ok(()) 139 | } 140 | 141 | #[tokio::test] 142 | async fn stream_iter() -> Result<()> { 143 | let chunks: Vec> = vec![ 144 | Ok("--00252461d3ab8ff5"), 145 | Ok("c25834e0bffd6f70"), 146 | Ok("\r\n"), 147 | Ok(r#"Content-Disposition: form-data; name="foo""#), 148 | Ok("\r\n"), 149 | Ok("\r\n"), 150 | Ok("bar"), 151 | Ok("\r\n"), 152 | Ok("--00252461d3ab8ff5c25834e0bffd6f70"), 153 | Ok("\r\n"), 154 | Ok(r#"Content-Disposition: form-data; name="name""#), 155 | Ok("\r\n"), 156 | Ok("\r\n"), 157 | Ok("web"), 158 | Ok("\r\n"), 159 | Ok("--00252461d3ab8ff5c25834e0bffd6f70"), 160 | Ok("--"), 161 | ]; 162 | let body = StreamBody::new(stream::iter(chunks)); 163 | let mut form = FormData::new(body, "00252461d3ab8ff5c25834e0bffd6f70"); 164 | 165 | while let Some(mut field) = form.try_next().await? { 166 | assert!(!field.consumed()); 167 | assert_eq!(field.length, 0); 168 | 169 | let mut buffer = BytesMut::new(); 170 | while let Some(buf) = field.try_next().await? { 171 | buffer.extend_from_slice(&buf); 172 | } 173 | 174 | match field.index { 175 | 0 => { 176 | assert_eq!(field.name, "foo"); 177 | assert_eq!(field.filename, None); 178 | assert_eq!(field.content_type, None); 179 | assert_eq!(field.length, 3); 180 | assert_eq!(buffer, "bar"); 181 | } 182 | 1 => { 183 | assert_eq!(field.name, "name"); 184 | assert_eq!(field.filename, None); 185 | assert_eq!(field.content_type, None); 186 | assert_eq!(field.length, 3); 187 | assert_eq!(buffer, "web"); 188 | } 189 | _ => {} 190 | } 191 | 192 | assert_eq!(field.length, buffer.len()); 193 | assert!(field.consumed()); 194 | 195 | tracing::info!("{:#?}", field); 196 | } 197 | 198 | let state = form.state(); 199 | let state = state 200 | .try_lock() 201 | .map_err(|e| Error::TryLockError(e.to_string()))?; 202 | 203 | assert!(state.eof()); 204 | assert_eq!(state.total(), 2); 205 | assert_eq!(state.len(), 211); 206 | 207 | Ok(()) 208 | } 209 | -------------------------------------------------------------------------------- /tests/lib/incoming_body.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | pin::Pin, 4 | task::{Context, Poll}, 5 | }; 6 | 7 | use anyhow::Result; 8 | use bytes::Bytes; 9 | use futures_util::Stream; 10 | use http_body::{Body, Frame, SizeHint}; 11 | use hyper::body::Incoming; 12 | 13 | /// Incoming Body from request. 14 | pub struct IncomingBody(Option); 15 | 16 | #[allow(dead_code)] 17 | impl IncomingBody { 18 | /// Creates new Incoming Body 19 | pub fn new(inner: Option) -> Self { 20 | Self(inner) 21 | } 22 | 23 | /// Incoming body has been used 24 | pub fn used() -> Self { 25 | Self(None) 26 | } 27 | } 28 | 29 | impl Default for IncomingBody { 30 | fn default() -> Self { 31 | Self::used() 32 | } 33 | } 34 | 35 | impl fmt::Debug for IncomingBody { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | f.debug_struct("IncomingBody").finish() 38 | } 39 | } 40 | 41 | impl Body for IncomingBody { 42 | type Data = Bytes; 43 | type Error = Box; 44 | 45 | #[inline] 46 | fn poll_frame( 47 | self: Pin<&mut Self>, 48 | cx: &mut Context<'_>, 49 | ) -> Poll, Self::Error>>> { 50 | self.get_mut() 51 | .0 52 | .as_mut() 53 | .map_or(Poll::Ready(None), |inner| { 54 | Pin::new(inner).poll_frame(cx).map_err(Into::into) 55 | }) 56 | } 57 | 58 | fn is_end_stream(&self) -> bool { 59 | self.0.as_ref().is_none_or(Body::is_end_stream) 60 | } 61 | 62 | fn size_hint(&self) -> SizeHint { 63 | self.0 64 | .as_ref() 65 | .map_or(SizeHint::with_exact(0), Body::size_hint) 66 | } 67 | } 68 | 69 | impl Stream for IncomingBody { 70 | type Item = Result>; 71 | 72 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 73 | self.get_mut() 74 | .0 75 | .as_mut() 76 | .map_or(Poll::Ready(None), |inner| { 77 | match Pin::new(inner).poll_frame(cx)? { 78 | Poll::Ready(Some(f)) => Poll::Ready(f.into_data().map(Ok).ok()), 79 | Poll::Ready(None) => Poll::Ready(None), 80 | Poll::Pending => Poll::Pending, 81 | } 82 | }) 83 | } 84 | 85 | #[inline] 86 | fn size_hint(&self) -> (usize, Option) { 87 | self.0.as_ref().map_or((0, None), |inner| { 88 | let sh = inner.size_hint(); 89 | ( 90 | usize::try_from(sh.lower()).unwrap_or(usize::MAX), 91 | sh.upper().map(|v| usize::try_from(v).unwrap_or(usize::MAX)), 92 | ) 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/lib/limited.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use anyhow::Result; 4 | 5 | #[cfg(feature = "async")] 6 | use bytes::{Bytes, BytesMut}; 7 | 8 | #[cfg(feature = "async")] 9 | use futures_util::{ 10 | io::{self, AsyncRead}, 11 | stream::Stream, 12 | }; 13 | use rand::Rng; 14 | #[cfg(feature = "async")] 15 | use std::{ 16 | pin::Pin, 17 | task::{Context, Poll}, 18 | }; 19 | 20 | #[cfg(feature = "sync")] 21 | use std::io::{self, Read}; 22 | 23 | pub const LIMITED: usize = 8 * 1024; 24 | 25 | pub struct Limited { 26 | io: T, 27 | limit: usize, 28 | length: u64, 29 | eof: bool, 30 | } 31 | 32 | #[allow(dead_code)] 33 | impl Limited { 34 | pub fn new(io: T, limit: usize) -> Self { 35 | tracing::info!("Limited stream by {}", limit); 36 | 37 | Self { 38 | io, 39 | limit, 40 | length: 0, 41 | eof: false, 42 | } 43 | } 44 | 45 | pub fn random(io: T) -> Self { 46 | Self::new(io, rand::thread_rng().gen_range(1..LIMITED)) 47 | } 48 | 49 | pub fn random_with(io: T, max: usize) -> Self { 50 | Self::new(io, rand::thread_rng().gen_range(1..max)) 51 | } 52 | 53 | pub fn limit(&self) -> usize { 54 | self.limit 55 | } 56 | } 57 | 58 | impl fmt::Debug for Limited { 59 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 60 | f.debug_struct("Limited") 61 | .field("eof", &self.eof) 62 | .field("limit", &self.limit) 63 | .field("length", &self.length) 64 | .finish_non_exhaustive() 65 | } 66 | } 67 | 68 | #[cfg(feature = "async")] 69 | impl Stream for Limited { 70 | type Item = Result; 71 | 72 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 73 | let mut buf = BytesMut::new(); 74 | // zero-fills the space in the read buffer 75 | buf.resize(self.limit, 0); 76 | 77 | match Pin::new(&mut self.io).poll_read(cx, &mut buf[..])? { 78 | Poll::Ready(0) => { 79 | self.eof = true; 80 | Poll::Ready(None) 81 | } 82 | Poll::Ready(n) => { 83 | self.length += n as u64; 84 | buf.truncate(n); 85 | Poll::Ready(Some(Ok(buf.freeze()))) 86 | } 87 | Poll::Pending => Poll::Pending, 88 | } 89 | } 90 | } 91 | 92 | #[cfg(feature = "sync")] 93 | impl Read for Limited 94 | where 95 | T: Read, 96 | { 97 | fn read(&mut self, buf: &mut [u8]) -> Result { 98 | self.io.read(buf) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/lib/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_imports)] 3 | 4 | #[cfg(feature = "async")] 5 | mod incoming_body; 6 | #[cfg(feature = "async")] 7 | pub use incoming_body::IncomingBody; 8 | 9 | mod limited; 10 | pub use limited::Limited; 11 | 12 | pub fn tracing_init() -> anyhow::Result<()> { 13 | tracing_subscriber::fmt() 14 | // From env var: `RUST_LOG` 15 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 16 | .try_init() 17 | .map_err(|e| anyhow::anyhow!(e)) 18 | } 19 | -------------------------------------------------------------------------------- /tests/tiny-body.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! ``` 3 | //! RUST_LOG=trace cargo test --test tiny-body --no-default-features --features="sync" -- --nocapture 4 | //! ``` 5 | 6 | use std::{fs::File, io::Read, str::FromStr}; 7 | 8 | use anyhow::Result; 9 | 10 | use form_data::*; 11 | 12 | #[path = "./lib/mod.rs"] 13 | mod lib; 14 | 15 | use lib::{tracing_init, Limited}; 16 | 17 | #[test] 18 | fn tiny_body() -> Result<()> { 19 | tracing_init()?; 20 | 21 | let payload = File::open("tests/fixtures/issue-6.txt")?; 22 | let stream = Limited::random_with(payload, 256); 23 | let limit = stream.limit(); 24 | tracing::trace!(limit = limit); 25 | 26 | let mut form = FormData::new( 27 | stream, 28 | "---------------------------187056119119472771921673485771", 29 | ); 30 | form.set_max_buf_size(limit)?; 31 | 32 | while let Some(item) = form.next() { 33 | let mut field = item?; 34 | assert!(!field.consumed()); 35 | assert_eq!(field.length, 0); 36 | tracing::trace!("{:?}", field); 37 | 38 | match field.index { 39 | 0 => { 40 | assert_eq!(field.name, "upload_file"); 41 | assert_eq!(field.filename, Some("font.py".into())); 42 | assert_eq!( 43 | field.content_type, 44 | Some(mime::Mime::from_str("text/x-python")?) 45 | ); 46 | } 47 | 1 => { 48 | assert_eq!(field.name, "expire"); 49 | assert_eq!(field.filename, None); 50 | assert_eq!(field.content_type, None); 51 | 52 | let mut value = String::new(); 53 | let size = field.read_to_string(&mut value)?; 54 | 55 | tracing::trace!("value: {}, size: {}", value, size); 56 | 57 | assert_eq!(value, "on"); 58 | } 59 | 2 => { 60 | assert_eq!(field.name, "expireDays"); 61 | assert_eq!(field.filename, None); 62 | assert_eq!(field.content_type, None); 63 | 64 | let mut value = String::new(); 65 | let size = field.read_to_string(&mut value)?; 66 | 67 | tracing::trace!("value: {}, size: {}", value, size); 68 | 69 | assert_eq!(value, "2"); 70 | } 71 | 3 => { 72 | assert_eq!(field.name, "expireHours"); 73 | assert_eq!(field.filename, None); 74 | assert_eq!(field.content_type, None); 75 | 76 | let mut value = String::new(); 77 | let size = field.read_to_string(&mut value)?; 78 | 79 | tracing::trace!("value: {}, size: {}", value, size); 80 | 81 | assert_eq!(value, "0"); 82 | } 83 | 4 => { 84 | assert_eq!(field.name, "expireMins"); 85 | assert_eq!(field.filename, None); 86 | assert_eq!(field.content_type, None); 87 | 88 | let mut value = String::new(); 89 | let size = field.read_to_string(&mut value)?; 90 | 91 | tracing::trace!("value: {}, size: {}", value, size); 92 | 93 | assert_eq!(value, "2"); 94 | } 95 | 5 => { 96 | assert_eq!(field.name, "expireSecs"); 97 | assert_eq!(field.filename, None); 98 | assert_eq!(field.content_type, None); 99 | 100 | let mut value = String::new(); 101 | let size = field.read_to_string(&mut value)?; 102 | 103 | tracing::trace!("value: {}, size: {}", value, size); 104 | 105 | assert_eq!(value, "0"); 106 | } 107 | _ => {} 108 | } 109 | 110 | field.ignore()?; 111 | } 112 | 113 | let state = form.state(); 114 | let state = state 115 | .try_lock() 116 | .map_err(|e| Error::TryLockError(e.to_string()))?; 117 | 118 | assert_eq!(state.eof(), true); 119 | assert_eq!(state.total(), 6); 120 | assert_eq!(state.len(), 1415); 121 | 122 | Ok(()) 123 | } 124 | --------------------------------------------------------------------------------