1use std::io;
2use std::path::Path;
3use std::pin::Pin;
4use std::task::{Context, Poll};
5
6use bytes::Bytes;
7use futures_util::{Stream, TryFutureExt};
8
9use crate::core::body::{Body, BodyKind};
10
11mod debug;
12
13pub struct Part {
14 pub(crate) name: String,
15 pub(crate) body: Body,
16 pub(crate) filename: Option<String>,
17 pub(crate) mime: Option<String>,
18 pub(crate) extra_headers: Vec<(String, String)>,
19}
20
21impl Part {
22 pub fn text(value: impl Into<String>) -> Self {
23 let s = value.into();
24 Self {
25 name: String::new(),
26 body: Body::bytes(Bytes::from(s.into_bytes())),
27 filename: None,
28 mime: Some("text/plain; charset=utf-8".into()),
29 extra_headers: Vec::new(),
30 }
31 }
32
33 pub fn bytes(bytes: impl Into<Bytes>) -> Self {
34 Self {
35 name: String::new(),
36 body: Body::bytes(bytes.into()),
37 filename: None,
38 mime: None,
39 extra_headers: Vec::new(),
40 }
41 }
42
43 pub fn stream<S>(stream: S) -> Self
44 where
45 S: Stream<Item = io::Result<Bytes>> + Send + 'static,
46 {
47 Self {
48 name: String::new(),
49 body: Body::stream(stream, None),
50 filename: None,
51 mime: None,
52 extra_headers: Vec::new(),
53 }
54 }
55
56 pub fn filename(mut self, name: impl Into<String>) -> Self {
57 self.filename = Some(name.into());
58 self
59 }
60
61 pub fn mime(mut self, mime: impl Into<String>) -> Self {
62 self.mime = Some(mime.into());
63 self
64 }
65
66 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
67 self.extra_headers.push((name.into(), value.into()));
68 self
69 }
70}
71
72pub struct Form {
73 pub(crate) parts: Vec<Part>,
74 pub(crate) boundary: String,
75}
76
77impl Default for Form {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83impl Form {
84 pub fn new() -> Self {
85 Self {
86 parts: Vec::new(),
87 boundary: random_boundary(),
88 }
89 }
90
91 pub fn boundary(&self) -> &str {
92 &self.boundary
93 }
94
95 pub fn text(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
96 let mut part = Part::text(value);
97 part.name = name.into();
98 self.parts.push(part);
99 self
100 }
101
102 pub fn part(mut self, name: impl Into<String>, mut part: Part) -> Self {
103 part.name = name.into();
104 self.parts.push(part);
105 self
106 }
107
108 pub fn file(mut self, name: impl Into<String>, path: impl AsRef<Path>) -> io::Result<Self> {
109 let path = path.as_ref().to_path_buf();
110 let metadata = std::fs::metadata(&path)?;
111 let filename = path
112 .file_name()
113 .and_then(|s| s.to_str())
114 .unwrap_or("file")
115 .to_string();
116
117 let stream = tokio::fs::File::open(path)
118 .map_ok(tokio_util::io::ReaderStream::new)
119 .try_flatten_stream();
120
121 let mut part = Part::stream(stream).filename(filename);
122 part.name = name.into();
123 if let BodyKind::Stream {
124 ref mut length_hint,
125 ..
126 } = part.body.0
127 {
128 *length_hint = Some(metadata.len());
129 }
130 self.parts.push(part);
131 Ok(self)
132 }
133
134 pub(crate) fn len_hint(&self) -> Option<u64> {
135 let mut total: u64 = 0;
136 for part in &self.parts {
137 total += 2 + self.boundary.len() as u64 + 2;
138 total += part_header_len(part) as u64;
139 let body_len = part.body.len_hint()?;
140 total += body_len;
141 total += 2;
142 }
143 total += 2 + self.boundary.len() as u64 + 4;
144 Some(total)
145 }
146
147 pub(crate) fn into_stream_body(self) -> Body {
148 if let Some(bytes) = self.buffered() {
149 return Body::bytes(bytes);
150 }
151 let length_hint = self.len_hint();
152 Body::stream(FormStream::new(self), length_hint)
153 }
154
155 fn buffered(&self) -> Option<Bytes> {
156 let mut out = Vec::with_capacity(usize::try_from(self.len_hint()?).ok()?);
157 for part in &self.parts {
158 let body: &[u8] = match &part.body.0 {
159 BodyKind::Empty => &[],
160 BodyKind::Bytes(b) => b,
161 BodyKind::Stream { .. } => return None,
162 };
163 out.extend_from_slice(&render_part_headers(&self.boundary, part).ok()?);
164 out.extend_from_slice(body);
165 out.extend_from_slice(b"\r\n");
166 }
167 out.extend_from_slice(b"--");
168 out.extend_from_slice(self.boundary.as_bytes());
169 out.extend_from_slice(b"--\r\n");
170 Some(Bytes::from(out))
171 }
172
173 pub(crate) fn content_type(&self) -> String {
174 format!("multipart/form-data; boundary={}", self.boundary)
175 }
176}
177
178impl From<Form> for Body {
179 fn from(form: Form) -> Body {
180 form.into_stream_body()
181 }
182}
183
184fn part_header_len(part: &Part) -> usize {
185 let mut n = "Content-Disposition: form-data; name=\"\"\r\n".len() + quoted_len(&part.name);
186 if let Some(fname) = &part.filename {
187 n += "; filename=\"\"".len() + quoted_len(fname);
188 }
189 if let Some(mime) = &part.mime {
190 n += "Content-Type: \r\n".len() + mime.len();
191 }
192 for (k, v) in &part.extra_headers {
193 n += k.len() + 2 + v.len() + 2;
194 }
195 n += 2;
196 n
197}
198
199struct FormStream {
200 boundary: String,
201 parts: std::collections::VecDeque<Part>,
202 state: FormState,
203}
204
205enum FormState {
206 NextPart,
207 InBody(Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send + 'static>>),
208 PartEnd,
209 Done,
210}
211
212impl FormStream {
213 fn new(form: Form) -> Self {
214 Self {
215 boundary: form.boundary,
216 parts: form.parts.into(),
217 state: FormState::NextPart,
218 }
219 }
220}
221
222fn quoted_len(input: &str) -> usize {
223 input.len() + input.bytes().filter(|b| matches!(b, b'"' | b'\\')).count()
224}
225
226fn escape_quoted(input: &str) -> Option<Vec<u8>> {
227 let mut out = Vec::with_capacity(input.len() + 2);
228 for b in input.bytes() {
229 match b {
230 0x00..=0x1F | 0x7F => return None,
231 b'"' | b'\\' => {
232 out.push(b'\\');
233 out.push(b);
234 }
235 _ => out.push(b),
236 }
237 }
238 Some(out)
239}
240
241fn render_part_headers(boundary: &str, part: &Part) -> io::Result<Bytes> {
242 let mut out = Vec::with_capacity(256);
243 out.extend_from_slice(b"--");
244 out.extend_from_slice(boundary.as_bytes());
245 out.extend_from_slice(b"\r\n");
246
247 out.extend_from_slice(b"Content-Disposition: form-data; name=\"");
248 let escaped_name = escape_quoted(&part.name).ok_or_else(|| {
249 io::Error::new(
250 io::ErrorKind::InvalidInput,
251 "multipart part `name` contains control characters (CRLF/NUL/etc.)",
252 )
253 })?;
254 out.extend_from_slice(&escaped_name);
255 out.push(b'"');
256 if let Some(fname) = &part.filename {
257 let escaped = escape_quoted(fname).ok_or_else(|| {
258 io::Error::new(
259 io::ErrorKind::InvalidInput,
260 "multipart part `filename` contains control characters (CRLF/NUL/etc.)",
261 )
262 })?;
263 out.extend_from_slice(b"; filename=\"");
264 out.extend_from_slice(&escaped);
265 out.push(b'"');
266 }
267 out.extend_from_slice(b"\r\n");
268
269 if let Some(mime) = &part.mime {
270 if mime.bytes().any(|b| matches!(b, 0x00..=0x1F | 0x7F)) {
271 return Err(io::Error::new(
272 io::ErrorKind::InvalidInput,
273 "multipart part `mime` contains control characters",
274 ));
275 }
276 out.extend_from_slice(b"Content-Type: ");
277 out.extend_from_slice(mime.as_bytes());
278 out.extend_from_slice(b"\r\n");
279 }
280
281 for (k, v) in &part.extra_headers {
282 let bad = k.bytes().any(|b| matches!(b, 0x00..=0x1F | 0x7F))
283 || v.bytes().any(|b| matches!(b, 0x00..=0x0A | 0x0D | 0x7F));
284 if bad {
285 return Err(io::Error::new(
286 io::ErrorKind::InvalidInput,
287 "multipart part extra header contains control characters",
288 ));
289 }
290 out.extend_from_slice(k.as_bytes());
291 out.extend_from_slice(b": ");
292 out.extend_from_slice(v.as_bytes());
293 out.extend_from_slice(b"\r\n");
294 }
295
296 out.extend_from_slice(b"\r\n");
297 Ok(Bytes::from(out))
298}
299
300impl Stream for FormStream {
301 type Item = io::Result<Bytes>;
302
303 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
304 loop {
305 let this = &mut *self;
307 match &mut this.state {
308 FormState::NextPart => {
309 let Some(part) = this.parts.pop_front() else {
310 let mut out = Vec::with_capacity(this.boundary.len() + 6);
311 out.extend_from_slice(b"--");
312 out.extend_from_slice(this.boundary.as_bytes());
313 out.extend_from_slice(b"--\r\n");
314 this.state = FormState::Done;
315 return Poll::Ready(Some(Ok(Bytes::from(out))));
316 };
317 let headers = match render_part_headers(&this.boundary, &part) {
318 Ok(h) => h,
319 Err(e) => {
320 this.state = FormState::Done;
321 return Poll::Ready(Some(Err(e)));
322 }
323 };
324 let body = part.body;
325 let body_stream: Pin<
326 Box<dyn Stream<Item = io::Result<Bytes>> + Send + 'static>,
327 > = match body.0 {
328 BodyKind::Empty => Box::pin(futures_util::stream::empty()),
329 BodyKind::Bytes(b) => {
330 Box::pin(futures_util::stream::once(async move { Ok(b) }))
331 }
332 BodyKind::Stream { stream, .. } => stream,
333 };
334 this.state = FormState::InBody(body_stream);
335 return Poll::Ready(Some(Ok(headers)));
336 }
337 FormState::InBody(stream) => match stream.as_mut().poll_next(cx) {
338 Poll::Pending => return Poll::Pending,
339 Poll::Ready(Some(Ok(b))) => return Poll::Ready(Some(Ok(b))),
340 Poll::Ready(Some(Err(e))) => {
341 this.state = FormState::Done;
342 return Poll::Ready(Some(Err(e)));
343 }
344 Poll::Ready(None) => {
345 this.state = FormState::PartEnd;
346 continue;
347 }
348 },
349 FormState::PartEnd => {
350 this.state = FormState::NextPart;
351 return Poll::Ready(Some(Ok(Bytes::from_static(b"\r\n"))));
352 }
353 FormState::Done => return Poll::Ready(None),
354 }
355 }
356 }
357}
358
359fn random_boundary() -> String {
360 format!(
361 "----LeylineFormBoundary{}",
362 crate::util::random_hex_token(16)
363 )
364}
365
366#[cfg(test)]
367mod tests;