1#![allow(missing_docs)]
32
33use std::{
34 fmt,
35 pin::Pin,
36 task::{Context, Poll},
37};
38
39use bytes::Bytes;
40#[cfg(feature = "streaming")]
41use bytes::BytesMut;
42use http_body::{Body, Frame};
43use pin_project_lite::pin_project;
44
45use crate::error::StreamingError;
46
47#[cfg(feature = "streaming")]
52const STREAM_BUFFER_SIZE: usize = 64 * 1024;
53
54#[cfg(feature = "streaming")]
56pub struct FileCheck {
57 hasher: blake3::Hasher,
58 expected: [u8; 32],
59 on_corrupt: Box<dyn FnOnce() + Send>,
60}
61
62#[cfg(feature = "streaming")]
63impl FileCheck {
64 fn finish(self) -> Result<(), StreamingError> {
67 if self.hasher.finalize().as_bytes() == &self.expected {
68 return Ok(());
69 }
70 (self.on_corrupt)();
71 Err(StreamingError::new(Box::new(std::io::Error::new(
72 std::io::ErrorKind::InvalidData,
73 "cached body checksum mismatch",
74 ))))
75 }
76
77 fn corrupt(self) {
79 (self.on_corrupt)();
80 }
81}
82
83#[cfg(feature = "streaming")]
84impl fmt::Debug for FileCheck {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.debug_struct("FileCheck")
87 .field("expected", &self.expected)
88 .finish_non_exhaustive()
89 }
90}
91
92#[cfg(feature = "streaming")]
94pin_project! {
95 #[project = StreamingBodyProj]
108 pub enum StreamingBody<B> {
109 Buffered {
110 data: Option<Bytes>,
111 },
112 Streaming {
113 #[pin]
114 inner: B,
115 },
116 File {
117 #[pin]
118 reader: tokio::fs::File,
119 buffer: BytesMut,
120 done: bool,
121 size: u64,
122 check: Option<Box<FileCheck>>,
123 },
124 }
125}
126
127#[cfg(not(feature = "streaming"))]
129pin_project! {
130 #[project = StreamingBodyProj]
141 pub enum StreamingBody<B> {
142 Buffered {
143 data: Option<Bytes>,
144 },
145 Streaming {
146 #[pin]
147 inner: B,
148 },
149 }
150}
151
152impl<B> StreamingBody<B> {
153 #[must_use]
157 pub fn buffered(data: Bytes) -> Self {
158 Self::Buffered { data: Some(data) }
159 }
160
161 #[must_use]
165 pub fn streaming(body: B) -> Self {
166 Self::Streaming { inner: body }
167 }
168
169 #[cfg(feature = "streaming")]
183 #[must_use]
184 pub fn from_file_with_size(file: tokio::fs::File, size: u64) -> Self {
185 Self::File {
186 reader: file,
187 buffer: BytesMut::with_capacity(STREAM_BUFFER_SIZE),
188 done: false,
189 size,
190 check: None,
191 }
192 }
193
194 #[cfg(feature = "streaming")]
199 #[must_use]
200 pub fn from_file_verified(
201 file: tokio::fs::File,
202 size: u64,
203 checksum: [u8; 32],
204 on_corrupt: impl FnOnce() + Send + 'static,
205 ) -> Self {
206 Self::File {
207 reader: file,
208 buffer: BytesMut::with_capacity(STREAM_BUFFER_SIZE),
209 done: false,
210 size,
211 check: Some(Box::new(FileCheck {
212 hasher: blake3::Hasher::new(),
213 expected: checksum,
214 on_corrupt: Box::new(on_corrupt),
215 })),
216 }
217 }
218}
219
220#[cfg(feature = "streaming")]
221impl<B> Body for StreamingBody<B>
222where
223 B: Body + Unpin,
224 B::Error: Into<StreamingError>,
225 B::Data: Into<Bytes>,
226{
227 type Data = Bytes;
228 type Error = StreamingError;
229
230 fn poll_frame(
231 mut self: Pin<&mut Self>,
232 cx: &mut Context<'_>,
233 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
234 match self.as_mut().project() {
235 StreamingBodyProj::Buffered { data } => {
236 if let Some(bytes) = data.take() {
237 if bytes.is_empty() {
238 Poll::Ready(None)
239 } else {
240 Poll::Ready(Some(Ok(Frame::data(bytes))))
241 }
242 } else {
243 Poll::Ready(None)
244 }
245 }
246 StreamingBodyProj::Streaming { inner } => {
247 inner.poll_frame(cx).map(|opt| {
248 opt.map(|res| {
249 res.map(|frame| frame.map_data(Into::into))
250 .map_err(Into::into)
251 })
252 })
253 }
254 StreamingBodyProj::File { reader, buffer, done, size, check } => {
255 if *done {
256 return Poll::Ready(None);
257 }
258 if *size == 0 {
259 *done = true;
260 if let Some(c) = check.take() {
261 if let Err(e) = c.finish() {
262 return Poll::Ready(Some(Err(e)));
263 }
264 }
265 return Poll::Ready(None);
266 }
267
268 use tokio::io::AsyncRead;
269
270 buffer.resize(STREAM_BUFFER_SIZE, 0);
272
273 let mut read_buf = tokio::io::ReadBuf::new(buffer.as_mut());
274
275 match reader.poll_read(cx, &mut read_buf) {
276 Poll::Ready(Ok(())) => {
277 let filled_len = read_buf.filled().len();
278 if filled_len == 0 {
279 *done = true;
280 buffer.clear();
281 if let Some(c) = check.take() {
282 c.corrupt();
283 }
284 Poll::Ready(Some(Err(StreamingError::new(
285 Box::new(std::io::Error::new(
286 std::io::ErrorKind::UnexpectedEof,
287 "cached body file shorter than expected",
288 )),
289 ))))
290 } else {
291 let take = (*size).min(filled_len as u64) as usize;
293 buffer.truncate(take);
294 *size -= take as u64;
295 if let Some(c) = check.as_deref_mut() {
296 c.hasher.update(&buffer[..take]);
297 }
298 if *size == 0 {
299 *done = true;
300 if let Some(c) = check.take() {
301 if let Err(e) = c.finish() {
302 buffer.clear();
303 return Poll::Ready(Some(Err(e)));
304 }
305 }
306 }
307 let bytes = buffer.split().freeze();
308 Poll::Ready(Some(Ok(Frame::data(bytes))))
309 }
310 }
311 Poll::Ready(Err(e)) => {
312 *done = true;
313 buffer.clear();
314 Poll::Ready(Some(Err(StreamingError::new(Box::new(e)))))
315 }
316 Poll::Pending => Poll::Pending,
317 }
318 }
319 }
320 }
321
322 fn is_end_stream(&self) -> bool {
323 match self {
324 StreamingBody::Buffered { data } => data.is_none(),
325 StreamingBody::Streaming { inner } => inner.is_end_stream(),
326 StreamingBody::File { done, .. } => *done,
327 }
328 }
329
330 fn size_hint(&self) -> http_body::SizeHint {
331 match self {
332 StreamingBody::Buffered { data } => {
333 if let Some(bytes) = data {
334 let len = bytes.len() as u64;
335 http_body::SizeHint::with_exact(len)
336 } else {
337 http_body::SizeHint::with_exact(0)
338 }
339 }
340 StreamingBody::Streaming { inner } => inner.size_hint(),
341 StreamingBody::File { size, .. } => {
342 http_body::SizeHint::with_exact(*size)
343 }
344 }
345 }
346}
347
348#[cfg(not(feature = "streaming"))]
349impl<B> Body for StreamingBody<B>
350where
351 B: Body + Unpin,
352 B::Error: Into<StreamingError>,
353 B::Data: Into<Bytes>,
354{
355 type Data = Bytes;
356 type Error = StreamingError;
357
358 fn poll_frame(
359 mut self: Pin<&mut Self>,
360 cx: &mut Context<'_>,
361 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
362 match self.as_mut().project() {
363 StreamingBodyProj::Buffered { data } => {
364 if let Some(bytes) = data.take() {
365 if bytes.is_empty() {
366 Poll::Ready(None)
367 } else {
368 Poll::Ready(Some(Ok(Frame::data(bytes))))
369 }
370 } else {
371 Poll::Ready(None)
372 }
373 }
374 StreamingBodyProj::Streaming { inner } => {
375 inner.poll_frame(cx).map(|opt| {
376 opt.map(|res| {
377 res.map(|frame| frame.map_data(Into::into))
378 .map_err(Into::into)
379 })
380 })
381 }
382 }
383 }
384
385 fn is_end_stream(&self) -> bool {
386 match self {
387 StreamingBody::Buffered { data } => data.is_none(),
388 StreamingBody::Streaming { inner } => inner.is_end_stream(),
389 }
390 }
391
392 fn size_hint(&self) -> http_body::SizeHint {
393 match self {
394 StreamingBody::Buffered { data } => {
395 if let Some(bytes) = data {
396 let len = bytes.len() as u64;
397 http_body::SizeHint::with_exact(len)
398 } else {
399 http_body::SizeHint::with_exact(0)
400 }
401 }
402 StreamingBody::Streaming { inner } => inner.size_hint(),
403 }
404 }
405}
406
407impl<B> From<Bytes> for StreamingBody<B> {
408 fn from(bytes: Bytes) -> Self {
409 Self::buffered(bytes)
410 }
411}
412
413#[cfg(feature = "streaming")]
414impl<B: fmt::Debug> fmt::Debug for StreamingBody<B> {
415 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
416 match self {
417 Self::Buffered { data } => f
418 .debug_struct("StreamingBody::Buffered")
419 .field("has_data", &data.is_some())
420 .field("len", &data.as_ref().map(|b| b.len()))
421 .finish(),
422 Self::Streaming { inner } => f
423 .debug_struct("StreamingBody::Streaming")
424 .field("inner", inner)
425 .finish(),
426 Self::File { done, size, .. } => f
427 .debug_struct("StreamingBody::File")
428 .field("done", done)
429 .field("size", &size)
430 .finish_non_exhaustive(),
431 }
432 }
433}
434
435#[cfg(not(feature = "streaming"))]
436impl<B: fmt::Debug> fmt::Debug for StreamingBody<B> {
437 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438 match self {
439 Self::Buffered { data } => f
440 .debug_struct("StreamingBody::Buffered")
441 .field("has_data", &data.is_some())
442 .field("len", &data.as_ref().map(|b| b.len()))
443 .finish(),
444 Self::Streaming { inner } => f
445 .debug_struct("StreamingBody::Streaming")
446 .field("inner", inner)
447 .finish(),
448 }
449 }
450}
451
452#[cfg(feature = "streaming")]
453impl<B> StreamingBody<B>
454where
455 B: Body + Unpin + Send,
456 B::Error: Into<StreamingError>,
457 B::Data: Into<Bytes>,
458{
459 pub fn into_bytes_stream(
463 self,
464 ) -> impl futures_util::Stream<
465 Item = Result<Bytes, Box<dyn std::error::Error + Send + Sync>>,
466 > + Send {
467 use futures_util::TryStreamExt;
468
469 http_body_util::BodyStream::new(self)
470 .map_ok(|frame| {
471 frame.into_data().unwrap_or_else(|_| Bytes::new())
473 })
474 .map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
475 Box::new(std::io::Error::other(format!("Stream error: {e}")))
476 })
477 }
478}
479
480#[cfg(all(test, feature = "streaming"))]
481mod tests {
482 use super::*;
483 use http_body_util::BodyExt;
484 use tokio::io::AsyncWriteExt;
485
486 async fn file_with(content: &[u8]) -> (tokio::fs::File, tempfile::TempDir) {
487 let dir = tempfile::tempdir().unwrap();
488 let path = dir.path().join("body.bin");
489 let mut f = tokio::fs::File::create(&path).await.unwrap();
490 f.write_all(content).await.unwrap();
491 f.sync_all().await.unwrap();
492 drop(f);
493 (tokio::fs::File::open(&path).await.unwrap(), dir)
494 }
495
496 #[tokio::test]
497 async fn file_body_stops_at_size() {
498 let (f, _dir) = file_with(b"0123456789trailing-garbage").await;
499 let body: StreamingBody<http_body_util::Empty<Bytes>> =
500 StreamingBody::from_file_with_size(f, 10);
501 let collected = body.collect().await.unwrap().to_bytes();
502 assert_eq!(collected.as_ref(), b"0123456789");
503 }
504
505 #[tokio::test]
506 async fn file_body_errors_on_truncated_file() {
507 let (f, _dir) = file_with(b"short").await;
508 let body: StreamingBody<http_body_util::Empty<Bytes>> =
509 StreamingBody::from_file_with_size(f, 100);
510 assert!(body.collect().await.is_err());
511 }
512
513 #[tokio::test]
514 async fn file_body_size_hint_reports_remaining() {
515 use http_body::Body;
516 let payload = vec![7u8; STREAM_BUFFER_SIZE + 100];
517 let (f, _dir) = file_with(&payload).await;
518 let mut body: StreamingBody<http_body_util::Empty<Bytes>> =
519 StreamingBody::from_file_with_size(f, payload.len() as u64);
520 assert_eq!(body.size_hint().exact(), Some(payload.len() as u64));
521 let frame =
522 std::future::poll_fn(|cx| Pin::new(&mut body).poll_frame(cx))
523 .await
524 .unwrap()
525 .unwrap();
526 let n = frame.into_data().unwrap().len() as u64;
527 assert_eq!(body.size_hint().exact(), Some(payload.len() as u64 - n));
528 }
529}