wreq-rb 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +2688 -0
  3. data/Cargo.toml +6 -0
  4. data/README.md +179 -0
  5. data/ext/wreq_rb/Cargo.toml +39 -0
  6. data/ext/wreq_rb/extconf.rb +22 -0
  7. data/ext/wreq_rb/src/client.rs +565 -0
  8. data/ext/wreq_rb/src/error.rs +25 -0
  9. data/ext/wreq_rb/src/lib.rs +20 -0
  10. data/ext/wreq_rb/src/response.rs +132 -0
  11. data/lib/wreq-rb/version.rb +5 -0
  12. data/lib/wreq-rb.rb +17 -0
  13. data/patches/0001-add-transfer-size-tracking.patch +292 -0
  14. data/vendor/wreq/Cargo.toml +306 -0
  15. data/vendor/wreq/LICENSE +202 -0
  16. data/vendor/wreq/README.md +122 -0
  17. data/vendor/wreq/examples/cert_store.rs +77 -0
  18. data/vendor/wreq/examples/connect_via_lower_priority_tokio_runtime.rs +258 -0
  19. data/vendor/wreq/examples/emulation.rs +118 -0
  20. data/vendor/wreq/examples/form.rs +14 -0
  21. data/vendor/wreq/examples/http1_websocket.rs +37 -0
  22. data/vendor/wreq/examples/http2_websocket.rs +45 -0
  23. data/vendor/wreq/examples/json_dynamic.rs +41 -0
  24. data/vendor/wreq/examples/json_typed.rs +47 -0
  25. data/vendor/wreq/examples/keylog.rs +16 -0
  26. data/vendor/wreq/examples/request_with_emulation.rs +115 -0
  27. data/vendor/wreq/examples/request_with_interface.rs +37 -0
  28. data/vendor/wreq/examples/request_with_local_address.rs +16 -0
  29. data/vendor/wreq/examples/request_with_proxy.rs +13 -0
  30. data/vendor/wreq/examples/request_with_redirect.rs +22 -0
  31. data/vendor/wreq/examples/request_with_version.rs +15 -0
  32. data/vendor/wreq/examples/tor_socks.rs +24 -0
  33. data/vendor/wreq/examples/unix_socket.rs +33 -0
  34. data/vendor/wreq/src/client/body.rs +304 -0
  35. data/vendor/wreq/src/client/conn/conn.rs +231 -0
  36. data/vendor/wreq/src/client/conn/connector.rs +549 -0
  37. data/vendor/wreq/src/client/conn/http.rs +1023 -0
  38. data/vendor/wreq/src/client/conn/proxy/socks.rs +233 -0
  39. data/vendor/wreq/src/client/conn/proxy/tunnel.rs +260 -0
  40. data/vendor/wreq/src/client/conn/proxy.rs +39 -0
  41. data/vendor/wreq/src/client/conn/tls_info.rs +98 -0
  42. data/vendor/wreq/src/client/conn/uds.rs +44 -0
  43. data/vendor/wreq/src/client/conn/verbose.rs +149 -0
  44. data/vendor/wreq/src/client/conn.rs +323 -0
  45. data/vendor/wreq/src/client/core/body/incoming.rs +485 -0
  46. data/vendor/wreq/src/client/core/body/length.rs +118 -0
  47. data/vendor/wreq/src/client/core/body.rs +34 -0
  48. data/vendor/wreq/src/client/core/common/buf.rs +149 -0
  49. data/vendor/wreq/src/client/core/common/rewind.rs +141 -0
  50. data/vendor/wreq/src/client/core/common/watch.rs +76 -0
  51. data/vendor/wreq/src/client/core/common.rs +3 -0
  52. data/vendor/wreq/src/client/core/conn/http1.rs +342 -0
  53. data/vendor/wreq/src/client/core/conn/http2.rs +307 -0
  54. data/vendor/wreq/src/client/core/conn.rs +11 -0
  55. data/vendor/wreq/src/client/core/dispatch.rs +299 -0
  56. data/vendor/wreq/src/client/core/error.rs +435 -0
  57. data/vendor/wreq/src/client/core/ext.rs +201 -0
  58. data/vendor/wreq/src/client/core/http1.rs +178 -0
  59. data/vendor/wreq/src/client/core/http2.rs +483 -0
  60. data/vendor/wreq/src/client/core/proto/h1/conn.rs +988 -0
  61. data/vendor/wreq/src/client/core/proto/h1/decode.rs +1170 -0
  62. data/vendor/wreq/src/client/core/proto/h1/dispatch.rs +684 -0
  63. data/vendor/wreq/src/client/core/proto/h1/encode.rs +580 -0
  64. data/vendor/wreq/src/client/core/proto/h1/io.rs +879 -0
  65. data/vendor/wreq/src/client/core/proto/h1/role.rs +694 -0
  66. data/vendor/wreq/src/client/core/proto/h1.rs +104 -0
  67. data/vendor/wreq/src/client/core/proto/h2/client.rs +650 -0
  68. data/vendor/wreq/src/client/core/proto/h2/ping.rs +539 -0
  69. data/vendor/wreq/src/client/core/proto/h2.rs +379 -0
  70. data/vendor/wreq/src/client/core/proto/headers.rs +138 -0
  71. data/vendor/wreq/src/client/core/proto.rs +58 -0
  72. data/vendor/wreq/src/client/core/rt/bounds.rs +57 -0
  73. data/vendor/wreq/src/client/core/rt/timer.rs +150 -0
  74. data/vendor/wreq/src/client/core/rt/tokio.rs +99 -0
  75. data/vendor/wreq/src/client/core/rt.rs +25 -0
  76. data/vendor/wreq/src/client/core/upgrade.rs +267 -0
  77. data/vendor/wreq/src/client/core.rs +16 -0
  78. data/vendor/wreq/src/client/emulation.rs +161 -0
  79. data/vendor/wreq/src/client/http/client/error.rs +142 -0
  80. data/vendor/wreq/src/client/http/client/exec.rs +29 -0
  81. data/vendor/wreq/src/client/http/client/extra.rs +77 -0
  82. data/vendor/wreq/src/client/http/client/lazy.rs +79 -0
  83. data/vendor/wreq/src/client/http/client/pool.rs +1105 -0
  84. data/vendor/wreq/src/client/http/client/util.rs +104 -0
  85. data/vendor/wreq/src/client/http/client.rs +1003 -0
  86. data/vendor/wreq/src/client/http/future.rs +99 -0
  87. data/vendor/wreq/src/client/http.rs +1629 -0
  88. data/vendor/wreq/src/client/layer/config/options.rs +156 -0
  89. data/vendor/wreq/src/client/layer/config.rs +116 -0
  90. data/vendor/wreq/src/client/layer/cookie.rs +161 -0
  91. data/vendor/wreq/src/client/layer/decoder.rs +139 -0
  92. data/vendor/wreq/src/client/layer/redirect/future.rs +270 -0
  93. data/vendor/wreq/src/client/layer/redirect/policy.rs +63 -0
  94. data/vendor/wreq/src/client/layer/redirect.rs +145 -0
  95. data/vendor/wreq/src/client/layer/retry/classify.rs +105 -0
  96. data/vendor/wreq/src/client/layer/retry/scope.rs +51 -0
  97. data/vendor/wreq/src/client/layer/retry.rs +151 -0
  98. data/vendor/wreq/src/client/layer/timeout/body.rs +233 -0
  99. data/vendor/wreq/src/client/layer/timeout/future.rs +90 -0
  100. data/vendor/wreq/src/client/layer/timeout.rs +177 -0
  101. data/vendor/wreq/src/client/layer.rs +15 -0
  102. data/vendor/wreq/src/client/multipart.rs +717 -0
  103. data/vendor/wreq/src/client/request.rs +818 -0
  104. data/vendor/wreq/src/client/response.rs +534 -0
  105. data/vendor/wreq/src/client/ws/json.rs +99 -0
  106. data/vendor/wreq/src/client/ws/message.rs +453 -0
  107. data/vendor/wreq/src/client/ws.rs +714 -0
  108. data/vendor/wreq/src/client.rs +27 -0
  109. data/vendor/wreq/src/config.rs +140 -0
  110. data/vendor/wreq/src/cookie.rs +579 -0
  111. data/vendor/wreq/src/dns/gai.rs +249 -0
  112. data/vendor/wreq/src/dns/hickory.rs +78 -0
  113. data/vendor/wreq/src/dns/resolve.rs +180 -0
  114. data/vendor/wreq/src/dns.rs +69 -0
  115. data/vendor/wreq/src/error.rs +502 -0
  116. data/vendor/wreq/src/ext.rs +398 -0
  117. data/vendor/wreq/src/hash.rs +143 -0
  118. data/vendor/wreq/src/header.rs +506 -0
  119. data/vendor/wreq/src/into_uri.rs +187 -0
  120. data/vendor/wreq/src/lib.rs +586 -0
  121. data/vendor/wreq/src/proxy/mac.rs +82 -0
  122. data/vendor/wreq/src/proxy/matcher.rs +806 -0
  123. data/vendor/wreq/src/proxy/uds.rs +66 -0
  124. data/vendor/wreq/src/proxy/win.rs +31 -0
  125. data/vendor/wreq/src/proxy.rs +569 -0
  126. data/vendor/wreq/src/redirect.rs +575 -0
  127. data/vendor/wreq/src/retry.rs +198 -0
  128. data/vendor/wreq/src/sync.rs +129 -0
  129. data/vendor/wreq/src/tls/conn/cache.rs +123 -0
  130. data/vendor/wreq/src/tls/conn/cert_compression.rs +125 -0
  131. data/vendor/wreq/src/tls/conn/ext.rs +82 -0
  132. data/vendor/wreq/src/tls/conn/macros.rs +34 -0
  133. data/vendor/wreq/src/tls/conn/service.rs +138 -0
  134. data/vendor/wreq/src/tls/conn.rs +681 -0
  135. data/vendor/wreq/src/tls/keylog/handle.rs +64 -0
  136. data/vendor/wreq/src/tls/keylog.rs +99 -0
  137. data/vendor/wreq/src/tls/options.rs +464 -0
  138. data/vendor/wreq/src/tls/x509/identity.rs +122 -0
  139. data/vendor/wreq/src/tls/x509/parser.rs +71 -0
  140. data/vendor/wreq/src/tls/x509/store.rs +228 -0
  141. data/vendor/wreq/src/tls/x509.rs +68 -0
  142. data/vendor/wreq/src/tls.rs +154 -0
  143. data/vendor/wreq/src/trace.rs +55 -0
  144. data/vendor/wreq/src/util.rs +122 -0
  145. data/vendor/wreq/tests/badssl.rs +228 -0
  146. data/vendor/wreq/tests/brotli.rs +350 -0
  147. data/vendor/wreq/tests/client.rs +1098 -0
  148. data/vendor/wreq/tests/connector_layers.rs +227 -0
  149. data/vendor/wreq/tests/cookie.rs +306 -0
  150. data/vendor/wreq/tests/deflate.rs +347 -0
  151. data/vendor/wreq/tests/emulation.rs +260 -0
  152. data/vendor/wreq/tests/gzip.rs +347 -0
  153. data/vendor/wreq/tests/layers.rs +261 -0
  154. data/vendor/wreq/tests/multipart.rs +165 -0
  155. data/vendor/wreq/tests/proxy.rs +438 -0
  156. data/vendor/wreq/tests/redirect.rs +629 -0
  157. data/vendor/wreq/tests/retry.rs +135 -0
  158. data/vendor/wreq/tests/support/delay_server.rs +117 -0
  159. data/vendor/wreq/tests/support/error.rs +16 -0
  160. data/vendor/wreq/tests/support/layer.rs +183 -0
  161. data/vendor/wreq/tests/support/mod.rs +9 -0
  162. data/vendor/wreq/tests/support/server.rs +232 -0
  163. data/vendor/wreq/tests/timeouts.rs +281 -0
  164. data/vendor/wreq/tests/unix_socket.rs +135 -0
  165. data/vendor/wreq/tests/upgrade.rs +98 -0
  166. data/vendor/wreq/tests/zstd.rs +559 -0
  167. metadata +225 -0
@@ -0,0 +1,717 @@
1
+ //! multipart/form-data
2
+
3
+ use std::{borrow::Cow, pin::Pin};
4
+
5
+ use bytes::Bytes;
6
+ use futures_util::{Stream, StreamExt, future, stream};
7
+ use http_body_util::BodyExt;
8
+ use mime_guess::Mime;
9
+ use percent_encoding::{self, AsciiSet, NON_ALPHANUMERIC};
10
+ #[cfg(feature = "stream")]
11
+ use {std::io, std::path::Path, tokio::fs::File};
12
+
13
+ use super::Body;
14
+ use crate::header::HeaderMap;
15
+
16
+ /// An async multipart/form-data request.
17
+ #[derive(Debug)]
18
+ pub struct Form {
19
+ inner: FormParts<Part>,
20
+ }
21
+
22
+ /// A field in a multipart form.
23
+ #[derive(Debug)]
24
+ pub struct Part {
25
+ meta: PartMetadata,
26
+ value: Body,
27
+ body_length: Option<u64>,
28
+ }
29
+
30
+ #[derive(Debug)]
31
+ pub(crate) struct FormParts<P> {
32
+ pub(crate) boundary: String,
33
+ pub(crate) computed_headers: Vec<Vec<u8>>,
34
+ pub(crate) fields: Vec<(Cow<'static, str>, P)>,
35
+ pub(crate) percent_encoding: PercentEncoding,
36
+ }
37
+
38
+ #[derive(Debug)]
39
+ pub(crate) struct PartMetadata {
40
+ mime: Option<Mime>,
41
+ file_name: Option<Cow<'static, str>>,
42
+ pub(crate) headers: HeaderMap,
43
+ }
44
+
45
+ pub(crate) trait PartProps {
46
+ fn value_len(&self) -> Option<u64>;
47
+ fn metadata(&self) -> &PartMetadata;
48
+ }
49
+
50
+ // ===== impl Form =====
51
+
52
+ impl Default for Form {
53
+ fn default() -> Self {
54
+ Self::new()
55
+ }
56
+ }
57
+
58
+ impl Form {
59
+ /// Creates a new async Form without any content.
60
+ pub fn new() -> Form {
61
+ Form {
62
+ inner: FormParts::new(),
63
+ }
64
+ }
65
+
66
+ /// Get the boundary that this form will use.
67
+ #[inline]
68
+ pub fn boundary(&self) -> &str {
69
+ self.inner.boundary()
70
+ }
71
+
72
+ /// Add a data field with supplied name and value.
73
+ ///
74
+ /// # Examples
75
+ ///
76
+ /// ```
77
+ /// let form = wreq::multipart::Form::new()
78
+ /// .text("username", "seanmonstar")
79
+ /// .text("password", "secret");
80
+ /// ```
81
+ pub fn text<T, U>(self, name: T, value: U) -> Form
82
+ where
83
+ T: Into<Cow<'static, str>>,
84
+ U: Into<Cow<'static, str>>,
85
+ {
86
+ self.part(name, Part::text(value))
87
+ }
88
+
89
+ /// Adds a file field.
90
+ ///
91
+ /// The path will be used to try to guess the filename and mime.
92
+ ///
93
+ /// # Examples
94
+ ///
95
+ /// ```no_run
96
+ /// # async fn run() -> std::io::Result<()> {
97
+ /// let form = wreq::multipart::Form::new()
98
+ /// .file("key", "/path/to/file")
99
+ /// .await?;
100
+ /// # Ok(())
101
+ /// # }
102
+ /// ```
103
+ ///
104
+ /// # Errors
105
+ ///
106
+ /// Errors when the file cannot be opened.
107
+ #[cfg(feature = "stream")]
108
+ #[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
109
+ pub async fn file<T, U>(self, name: T, path: U) -> io::Result<Form>
110
+ where
111
+ T: Into<Cow<'static, str>>,
112
+ U: AsRef<Path>,
113
+ {
114
+ Ok(self.part(name, Part::file(path).await?))
115
+ }
116
+
117
+ /// Adds a customized Part.
118
+ pub fn part<T>(self, name: T, part: Part) -> Form
119
+ where
120
+ T: Into<Cow<'static, str>>,
121
+ {
122
+ self.with_inner(move |inner| inner.part(name, part))
123
+ }
124
+
125
+ /// Configure this `Form` to percent-encode using the `path-segment` rules.
126
+ pub fn percent_encode_path_segment(self) -> Form {
127
+ self.with_inner(|inner| inner.percent_encode_path_segment())
128
+ }
129
+
130
+ /// Configure this `Form` to percent-encode using the `attr-char` rules.
131
+ pub fn percent_encode_attr_chars(self) -> Form {
132
+ self.with_inner(|inner| inner.percent_encode_attr_chars())
133
+ }
134
+
135
+ /// Configure this `Form` to skip percent-encoding
136
+ pub fn percent_encode_noop(self) -> Form {
137
+ self.with_inner(|inner| inner.percent_encode_noop())
138
+ }
139
+
140
+ /// Consume this instance and transform into an instance of Body for use in a request.
141
+ pub(crate) fn stream(self) -> Body {
142
+ if self.inner.fields.is_empty() {
143
+ return Body::empty();
144
+ }
145
+
146
+ Body::stream(self.into_stream())
147
+ }
148
+
149
+ /// Produce a stream of the bytes in this `Form`, consuming it.
150
+ pub fn into_stream(mut self) -> impl Stream<Item = Result<Bytes, crate::Error>> + Send + Sync {
151
+ if self.inner.fields.is_empty() {
152
+ let empty_stream: Pin<
153
+ Box<dyn Stream<Item = Result<Bytes, crate::Error>> + Send + Sync>,
154
+ > = Box::pin(futures_util::stream::empty());
155
+ return empty_stream;
156
+ }
157
+
158
+ // create initial part to init reduce chain
159
+ let (name, part) = self.inner.fields.remove(0);
160
+ let start = Box::pin(self.part_stream(name, part))
161
+ as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>;
162
+
163
+ let fields = self.inner.take_fields();
164
+ // for each field, chain an additional stream
165
+ let stream = fields.into_iter().fold(start, |memo, (name, part)| {
166
+ let part_stream = self.part_stream(name, part);
167
+ Box::pin(memo.chain(part_stream))
168
+ as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>
169
+ });
170
+ // append special ending boundary
171
+ let last = stream::once(future::ready(Ok(
172
+ format!("--{}--\r\n", self.boundary()).into()
173
+ )));
174
+ Box::pin(stream.chain(last))
175
+ }
176
+
177
+ /// Generate a crate::core::Body stream for a single Part instance of a Form request.
178
+ pub(crate) fn part_stream<T>(
179
+ &mut self,
180
+ name: T,
181
+ part: Part,
182
+ ) -> impl Stream<Item = Result<Bytes, crate::Error>> + use<T>
183
+ where
184
+ T: Into<Cow<'static, str>>,
185
+ {
186
+ // start with boundary
187
+ let boundary = stream::once(future::ready(Ok(
188
+ format!("--{}\r\n", self.boundary()).into()
189
+ )));
190
+ // append headers
191
+ let header = stream::once(future::ready(Ok({
192
+ let mut h = self
193
+ .inner
194
+ .percent_encoding
195
+ .encode_headers(&name.into(), &part.meta);
196
+ h.extend_from_slice(b"\r\n\r\n");
197
+ h.into()
198
+ })));
199
+ // then append form data followed by terminating CRLF
200
+ boundary
201
+ .chain(header)
202
+ .chain(part.value.into_data_stream())
203
+ .chain(stream::once(future::ready(Ok("\r\n".into()))))
204
+ }
205
+
206
+ pub(crate) fn compute_length(&mut self) -> Option<u64> {
207
+ self.inner.compute_length()
208
+ }
209
+
210
+ fn with_inner<F>(self, func: F) -> Self
211
+ where
212
+ F: FnOnce(FormParts<Part>) -> FormParts<Part>,
213
+ {
214
+ Form {
215
+ inner: func(self.inner),
216
+ }
217
+ }
218
+ }
219
+
220
+ // ===== impl Part =====
221
+
222
+ impl Part {
223
+ /// Makes a text parameter.
224
+ pub fn text<T>(value: T) -> Part
225
+ where
226
+ T: Into<Cow<'static, str>>,
227
+ {
228
+ let body = match value.into() {
229
+ Cow::Borrowed(slice) => Body::from(slice),
230
+ Cow::Owned(string) => Body::from(string),
231
+ };
232
+ Part::new(body, None)
233
+ }
234
+
235
+ /// Makes a new parameter from arbitrary bytes.
236
+ pub fn bytes<T>(value: T) -> Part
237
+ where
238
+ T: Into<Cow<'static, [u8]>>,
239
+ {
240
+ let body = match value.into() {
241
+ Cow::Borrowed(slice) => Body::from(slice),
242
+ Cow::Owned(vec) => Body::from(vec),
243
+ };
244
+ Part::new(body, None)
245
+ }
246
+
247
+ /// Makes a new parameter from an arbitrary stream.
248
+ pub fn stream<T: Into<Body>>(value: T) -> Part {
249
+ Part::new(value.into(), None)
250
+ }
251
+
252
+ /// Makes a new parameter from an arbitrary stream with a known length. This is particularly
253
+ /// useful when adding something like file contents as a stream, where you can know the content
254
+ /// length beforehand.
255
+ pub fn stream_with_length<T: Into<Body>>(value: T, length: u64) -> Part {
256
+ Part::new(value.into(), Some(length))
257
+ }
258
+
259
+ /// Makes a file parameter.
260
+ ///
261
+ /// # Errors
262
+ ///
263
+ /// Errors when the file cannot be opened.
264
+ #[cfg(feature = "stream")]
265
+ #[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
266
+ pub async fn file<T: AsRef<Path>>(path: T) -> io::Result<Part> {
267
+ let path = path.as_ref();
268
+ let file_name = path
269
+ .file_name()
270
+ .map(|filename| filename.to_string_lossy().into_owned());
271
+ let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
272
+ let mime = mime_guess::from_ext(ext).first_or_octet_stream();
273
+ let file = File::open(path).await?;
274
+ let len = file.metadata().await.map(|m| m.len()).ok();
275
+ let field = match len {
276
+ Some(len) => Part::stream_with_length(file, len),
277
+ None => Part::stream(file),
278
+ }
279
+ .mime(mime);
280
+
281
+ Ok(if let Some(file_name) = file_name {
282
+ field.file_name(file_name)
283
+ } else {
284
+ field
285
+ })
286
+ }
287
+
288
+ fn new(value: Body, body_length: Option<u64>) -> Part {
289
+ Part {
290
+ meta: PartMetadata::new(),
291
+ value,
292
+ body_length,
293
+ }
294
+ }
295
+
296
+ /// Tries to set the mime of this part.
297
+ pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
298
+ Ok(self.mime(mime.parse().map_err(crate::Error::builder)?))
299
+ }
300
+
301
+ // Re-export when mime 0.4 is available, with split MediaType/MediaRange.
302
+ fn mime(self, mime: Mime) -> Part {
303
+ self.with_inner(move |inner| inner.mime(mime))
304
+ }
305
+
306
+ /// Sets the filename, builder style.
307
+ pub fn file_name<T>(self, filename: T) -> Part
308
+ where
309
+ T: Into<Cow<'static, str>>,
310
+ {
311
+ self.with_inner(move |inner| inner.file_name(filename))
312
+ }
313
+
314
+ /// Sets custom headers for the part.
315
+ pub fn headers(self, headers: HeaderMap) -> Part {
316
+ self.with_inner(move |inner| inner.headers(headers))
317
+ }
318
+
319
+ fn with_inner<F>(self, func: F) -> Self
320
+ where
321
+ F: FnOnce(PartMetadata) -> PartMetadata,
322
+ {
323
+ Part {
324
+ meta: func(self.meta),
325
+ ..self
326
+ }
327
+ }
328
+ }
329
+
330
+ impl PartProps for Part {
331
+ fn value_len(&self) -> Option<u64> {
332
+ if self.body_length.is_some() {
333
+ self.body_length
334
+ } else {
335
+ self.value.content_length()
336
+ }
337
+ }
338
+
339
+ fn metadata(&self) -> &PartMetadata {
340
+ &self.meta
341
+ }
342
+ }
343
+
344
+ // ===== impl FormParts =====
345
+
346
+ impl<P: PartProps> FormParts<P> {
347
+ pub(crate) fn new() -> Self {
348
+ FormParts {
349
+ boundary: gen_boundary(),
350
+ computed_headers: Vec::new(),
351
+ fields: Vec::new(),
352
+ percent_encoding: PercentEncoding::PathSegment,
353
+ }
354
+ }
355
+
356
+ pub(crate) fn boundary(&self) -> &str {
357
+ &self.boundary
358
+ }
359
+
360
+ /// Adds a customized Part.
361
+ pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
362
+ where
363
+ T: Into<Cow<'static, str>>,
364
+ {
365
+ self.fields.push((name.into(), part));
366
+ self
367
+ }
368
+
369
+ /// Configure this `Form` to percent-encode using the `path-segment` rules.
370
+ pub(crate) fn percent_encode_path_segment(mut self) -> Self {
371
+ self.percent_encoding = PercentEncoding::PathSegment;
372
+ self
373
+ }
374
+
375
+ /// Configure this `Form` to percent-encode using the `attr-char` rules.
376
+ pub(crate) fn percent_encode_attr_chars(mut self) -> Self {
377
+ self.percent_encoding = PercentEncoding::AttrChar;
378
+ self
379
+ }
380
+
381
+ /// Configure this `Form` to skip percent-encoding
382
+ pub(crate) fn percent_encode_noop(mut self) -> Self {
383
+ self.percent_encoding = PercentEncoding::NoOp;
384
+ self
385
+ }
386
+
387
+ // If predictable, computes the length the request will have
388
+ // The length should be predictable if only String and file fields have been added,
389
+ // but not if a generic reader has been added;
390
+ pub(crate) fn compute_length(&mut self) -> Option<u64> {
391
+ let mut length = 0u64;
392
+ for (name, field) in self.fields.iter() {
393
+ match field.value_len() {
394
+ Some(value_length) => {
395
+ // We are constructing the header just to get its length. To not have to
396
+ // construct it again when the request is sent we cache these headers.
397
+ let header = self.percent_encoding.encode_headers(name, field.metadata());
398
+ let header_length = header.len();
399
+ self.computed_headers.push(header);
400
+ // The additions mimic the format string out of which the field is constructed
401
+ // in Reader. Not the cleanest solution because if that format string is
402
+ // ever changed then this formula needs to be changed too which is not an
403
+ // obvious dependency in the code.
404
+ length += 2
405
+ + self.boundary().len() as u64
406
+ + 2
407
+ + header_length as u64
408
+ + 4
409
+ + value_length
410
+ + 2
411
+ }
412
+ _ => return None,
413
+ }
414
+ }
415
+ // If there is at least one field there is a special boundary for the very last field.
416
+ if !self.fields.is_empty() {
417
+ length += 2 + self.boundary().len() as u64 + 4
418
+ }
419
+ Some(length)
420
+ }
421
+
422
+ /// Take the fields vector of this instance, replacing with an empty vector.
423
+ fn take_fields(&mut self) -> Vec<(Cow<'static, str>, P)> {
424
+ std::mem::take(&mut self.fields)
425
+ }
426
+ }
427
+
428
+ // ===== impl PartMetadata =====
429
+
430
+ impl PartMetadata {
431
+ pub(crate) fn new() -> Self {
432
+ PartMetadata {
433
+ mime: None,
434
+ file_name: None,
435
+ headers: HeaderMap::default(),
436
+ }
437
+ }
438
+
439
+ pub(crate) fn mime(mut self, mime: Mime) -> Self {
440
+ self.mime = Some(mime);
441
+ self
442
+ }
443
+
444
+ pub(crate) fn file_name<T>(mut self, filename: T) -> Self
445
+ where
446
+ T: Into<Cow<'static, str>>,
447
+ {
448
+ self.file_name = Some(filename.into());
449
+ self
450
+ }
451
+
452
+ pub(crate) fn headers<T>(mut self, headers: T) -> Self
453
+ where
454
+ T: Into<HeaderMap>,
455
+ {
456
+ self.headers = headers.into();
457
+ self
458
+ }
459
+ }
460
+
461
+ // https://url.spec.whatwg.org/#fragment-percent-encode-set
462
+ const FRAGMENT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS
463
+ .add(b' ')
464
+ .add(b'"')
465
+ .add(b'<')
466
+ .add(b'>')
467
+ .add(b'`');
468
+
469
+ // https://url.spec.whatwg.org/#path-percent-encode-set
470
+ const PATH_ENCODE_SET: &AsciiSet = &FRAGMENT_ENCODE_SET.add(b'#').add(b'?').add(b'{').add(b'}');
471
+
472
+ const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &PATH_ENCODE_SET.add(b'/').add(b'%');
473
+
474
+ // https://tools.ietf.org/html/rfc8187#section-3.2.1
475
+ const ATTR_CHAR_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
476
+ .remove(b'!')
477
+ .remove(b'#')
478
+ .remove(b'$')
479
+ .remove(b'&')
480
+ .remove(b'+')
481
+ .remove(b'-')
482
+ .remove(b'.')
483
+ .remove(b'^')
484
+ .remove(b'_')
485
+ .remove(b'`')
486
+ .remove(b'|')
487
+ .remove(b'~');
488
+
489
+ #[derive(Debug)]
490
+ pub(crate) enum PercentEncoding {
491
+ PathSegment,
492
+ AttrChar,
493
+ NoOp,
494
+ }
495
+
496
+ impl PercentEncoding {
497
+ pub(crate) fn encode_headers(&self, name: &str, field: &PartMetadata) -> Vec<u8> {
498
+ let mut buf = Vec::new();
499
+ buf.extend_from_slice(b"Content-Disposition: form-data; ");
500
+
501
+ match self.percent_encode(name) {
502
+ Cow::Borrowed(value) => {
503
+ // nothing has been percent encoded
504
+ buf.extend_from_slice(b"name=\"");
505
+ buf.extend_from_slice(value.as_bytes());
506
+ buf.extend_from_slice(b"\"");
507
+ }
508
+ Cow::Owned(value) => {
509
+ // something has been percent encoded
510
+ buf.extend_from_slice(b"name*=utf-8''");
511
+ buf.extend_from_slice(value.as_bytes());
512
+ }
513
+ }
514
+
515
+ // According to RFC7578 Section 4.2, `filename*=` syntax is invalid.
516
+ // See https://github.com/seanmonstar/reqwest/issues/419.
517
+ if let Some(filename) = &field.file_name {
518
+ buf.extend_from_slice(b"; filename=\"");
519
+ let legal_filename = filename
520
+ .replace('\\', "\\\\")
521
+ .replace('"', "\\\"")
522
+ .replace('\r', "\\\r")
523
+ .replace('\n', "\\\n");
524
+ buf.extend_from_slice(legal_filename.as_bytes());
525
+ buf.extend_from_slice(b"\"");
526
+ }
527
+
528
+ if let Some(mime) = &field.mime {
529
+ buf.extend_from_slice(b"\r\nContent-Type: ");
530
+ buf.extend_from_slice(mime.as_ref().as_bytes());
531
+ }
532
+
533
+ for (k, v) in field.headers.iter() {
534
+ buf.extend_from_slice(b"\r\n");
535
+ buf.extend_from_slice(k.as_str().as_bytes());
536
+ buf.extend_from_slice(b": ");
537
+ buf.extend_from_slice(v.as_bytes());
538
+ }
539
+ buf
540
+ }
541
+
542
+ fn percent_encode<'a>(&self, value: &'a str) -> Cow<'a, str> {
543
+ use percent_encoding::utf8_percent_encode as percent_encode;
544
+
545
+ match self {
546
+ Self::PathSegment => percent_encode(value, PATH_SEGMENT_ENCODE_SET).into(),
547
+ Self::AttrChar => percent_encode(value, ATTR_CHAR_ENCODE_SET).into(),
548
+ Self::NoOp => value.into(),
549
+ }
550
+ }
551
+ }
552
+
553
+ fn gen_boundary() -> String {
554
+ use crate::util::fast_random as random;
555
+
556
+ let a = random();
557
+ let b = random();
558
+ let c = random();
559
+ let d = random();
560
+
561
+ format!("{a:016x}-{b:016x}-{c:016x}-{d:016x}")
562
+ }
563
+
564
+ #[cfg(test)]
565
+ mod tests {
566
+ use std::future;
567
+
568
+ use futures_util::{TryStreamExt, stream};
569
+ use tokio::{self, runtime};
570
+
571
+ use super::*;
572
+
573
+ #[test]
574
+ fn form_empty() {
575
+ let form = Form::new();
576
+
577
+ let rt = runtime::Builder::new_current_thread()
578
+ .enable_all()
579
+ .build()
580
+ .expect("new rt");
581
+ let body = form.stream().into_data_stream();
582
+ let s = body.map_ok(|try_c| try_c.to_vec()).try_concat();
583
+
584
+ let out = rt.block_on(s);
585
+ assert!(out.unwrap().is_empty());
586
+ }
587
+
588
+ #[test]
589
+ fn stream_to_end() {
590
+ let mut form = Form::new()
591
+ .part(
592
+ "reader1",
593
+ Part::stream(Body::stream(stream::once(future::ready::<
594
+ Result<String, crate::Error>,
595
+ >(Ok(
596
+ "part1".to_owned()
597
+ ))))),
598
+ )
599
+ .part("key1", Part::text("value1"))
600
+ .part(
601
+ "key2",
602
+ Part::text("value2").mime(mime_guess::mime::IMAGE_BMP),
603
+ )
604
+ .part(
605
+ "reader2",
606
+ Part::stream(Body::stream(stream::once(future::ready::<
607
+ Result<String, crate::Error>,
608
+ >(Ok(
609
+ "part2".to_owned()
610
+ ))))),
611
+ )
612
+ .part("key3", Part::text("value3").file_name("filename"));
613
+ form.inner.boundary = "boundary".to_string();
614
+ let expected = "--boundary\r\n\
615
+ Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
616
+ part1\r\n\
617
+ --boundary\r\n\
618
+ Content-Disposition: form-data; name=\"key1\"\r\n\r\n\
619
+ value1\r\n\
620
+ --boundary\r\n\
621
+ Content-Disposition: form-data; name=\"key2\"\r\n\
622
+ Content-Type: image/bmp\r\n\r\n\
623
+ value2\r\n\
624
+ --boundary\r\n\
625
+ Content-Disposition: form-data; name=\"reader2\"\r\n\r\n\
626
+ part2\r\n\
627
+ --boundary\r\n\
628
+ Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\
629
+ value3\r\n--boundary--\r\n";
630
+ let rt = runtime::Builder::new_current_thread()
631
+ .enable_all()
632
+ .build()
633
+ .expect("new rt");
634
+ let body = form.stream().into_data_stream();
635
+ let s = body.map(|try_c| try_c.map(|r| r.to_vec())).try_concat();
636
+
637
+ let out = rt.block_on(s).unwrap();
638
+ // These prints are for debug purposes in case the test fails
639
+ println!(
640
+ "START REAL\n{}\nEND REAL",
641
+ std::str::from_utf8(&out).unwrap()
642
+ );
643
+ println!("START EXPECTED\n{expected}\nEND EXPECTED");
644
+ assert_eq!(std::str::from_utf8(&out).unwrap(), expected);
645
+ }
646
+
647
+ #[test]
648
+ fn stream_to_end_with_header() {
649
+ let mut part = Part::text("value2").mime(mime_guess::mime::IMAGE_BMP);
650
+ let mut headers = HeaderMap::new();
651
+ headers.insert("Hdr3", "/a/b/c".parse().unwrap());
652
+ part = part.headers(headers);
653
+ let mut form = Form::new().part("key2", part);
654
+ form.inner.boundary = "boundary".to_string();
655
+ let expected = "--boundary\r\n\
656
+ Content-Disposition: form-data; name=\"key2\"\r\n\
657
+ Content-Type: image/bmp\r\n\
658
+ hdr3: /a/b/c\r\n\
659
+ \r\n\
660
+ value2\r\n\
661
+ --boundary--\r\n";
662
+ let rt = runtime::Builder::new_current_thread()
663
+ .enable_all()
664
+ .build()
665
+ .expect("new rt");
666
+ let body = form.stream().into_data_stream();
667
+ let s = body.map(|try_c| try_c.map(|r| r.to_vec())).try_concat();
668
+
669
+ let out = rt.block_on(s).unwrap();
670
+ // These prints are for debug purposes in case the test fails
671
+ println!(
672
+ "START REAL\n{}\nEND REAL",
673
+ std::str::from_utf8(&out).unwrap()
674
+ );
675
+ println!("START EXPECTED\n{expected}\nEND EXPECTED");
676
+ assert_eq!(std::str::from_utf8(&out).unwrap(), expected);
677
+ }
678
+
679
+ #[test]
680
+ fn correct_content_length() {
681
+ // Setup an arbitrary data stream
682
+ let stream_data = b"just some stream data";
683
+ let stream_len = stream_data.len();
684
+ let stream_data = stream_data
685
+ .chunks(3)
686
+ .map(|c| Ok::<_, std::io::Error>(Bytes::from(c)));
687
+ let the_stream = futures_util::stream::iter(stream_data);
688
+
689
+ let bytes_data = b"some bytes data".to_vec();
690
+ let bytes_len = bytes_data.len();
691
+
692
+ let stream_part = Part::stream_with_length(Body::stream(the_stream), stream_len as u64);
693
+ let body_part = Part::bytes(bytes_data);
694
+
695
+ // A simple check to make sure we get the configured body length
696
+ assert_eq!(stream_part.value_len().unwrap(), stream_len as u64);
697
+
698
+ // Make sure it delegates to the underlying body if length is not specified
699
+ assert_eq!(body_part.value_len().unwrap(), bytes_len as u64);
700
+ }
701
+
702
+ #[test]
703
+ fn header_percent_encoding() {
704
+ let name = "start%'\"\r\nßend";
705
+ let field = Part::text("");
706
+
707
+ assert_eq!(
708
+ PercentEncoding::PathSegment.encode_headers(name, &field.meta),
709
+ &b"Content-Disposition: form-data; name*=utf-8''start%25'%22%0D%0A%C3%9Fend"[..]
710
+ );
711
+
712
+ assert_eq!(
713
+ PercentEncoding::AttrChar.encode_headers(name, &field.meta),
714
+ &b"Content-Disposition: form-data; name*=utf-8''start%25%27%22%0D%0A%C3%9Fend"[..]
715
+ );
716
+ }
717
+ }