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