1use std::fmt;
2use std::io;
3use std::path::{Path, PathBuf};
4use std::pin::Pin;
5use std::task::{Context, Poll};
6
7use base64::Engine as _;
8use bytes::Bytes;
9use futures_core::Stream;
10use hyper::body::{Body, Frame, SizeHint};
11use sha2::{Digest, Sha256};
12use tokio::fs::File;
13use tokio::io::{AsyncReadExt, AsyncWriteExt};
14use tokio::sync::Mutex;
15use tokio_util::io::ReaderStream;
16
17use crate::error::S3Error;
18
19type UploadItems = Pin<Box<dyn Stream<Item = Result<Bytes, io::Error>> + Send + 'static>>;
20
21pub struct ByteStream {
27 source: UploadSource,
28}
29
30enum UploadSource {
31 Bytes(Bytes),
32 File(PathBuf),
33 Stream {
34 stream: UploadItems,
35 length: u64,
36 sha256: [u8; 32],
37 },
38}
39
40impl ByteStream {
41 pub fn from_bytes(bytes: impl Into<Bytes>) -> Self {
43 Self {
44 source: UploadSource::Bytes(bytes.into()),
45 }
46 }
47
48 pub fn from_path(path: impl AsRef<Path>) -> Self {
54 Self {
55 source: UploadSource::File(path.as_ref().to_owned()),
56 }
57 }
58
59 pub fn from_stream<S>(stream: S, length: u64, sha256: [u8; 32]) -> Self
64 where
65 S: Stream<Item = Result<Bytes, io::Error>> + Send + 'static,
66 {
67 Self {
68 source: UploadSource::Stream {
69 stream: Box::pin(stream),
70 length,
71 sha256,
72 },
73 }
74 }
75
76 pub(crate) async fn prepare(self) -> Result<PreparedBody, S3Error> {
77 match self.source {
78 UploadSource::Bytes(bytes) => {
79 let sha256 = Sha256::digest(&bytes).into();
80 let length = u64::try_from(bytes.len()).map_err(|_| {
81 S3Error::configuration("in-memory body length does not fit in u64")
82 })?;
83 Ok(PreparedBody {
84 source: PreparedSource::Bytes(bytes),
85 length,
86 sha256,
87 })
88 }
89 UploadSource::File(path) => prepare_file(path).await,
90 UploadSource::Stream {
91 stream,
92 length,
93 sha256,
94 } => Ok(PreparedBody {
95 source: PreparedSource::OneShot(Mutex::new(Some(stream))),
96 length,
97 sha256,
98 }),
99 }
100 }
101}
102
103impl From<Bytes> for ByteStream {
104 fn from(value: Bytes) -> Self {
105 Self::from_bytes(value)
106 }
107}
108
109impl From<Vec<u8>> for ByteStream {
110 fn from(value: Vec<u8>) -> Self {
111 Self::from_bytes(value)
112 }
113}
114
115impl From<&'static [u8]> for ByteStream {
116 fn from(value: &'static [u8]) -> Self {
117 Self::from_bytes(Bytes::from_static(value))
118 }
119}
120
121impl fmt::Debug for ByteStream {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 let (kind, replayable) = match self.source {
124 UploadSource::Bytes(_) => ("bytes", true),
125 UploadSource::File(_) => ("file", true),
126 UploadSource::Stream { .. } => ("stream", false),
127 };
128 formatter
129 .debug_struct("ByteStream")
130 .field("kind", &kind)
131 .field("replayable", &replayable)
132 .finish_non_exhaustive()
133 }
134}
135
136async fn prepare_file(path: PathBuf) -> Result<PreparedBody, S3Error> {
137 let mut file = File::open(&path).await.map_err(S3Error::transport)?;
138 let metadata = file.metadata().await.map_err(S3Error::transport)?;
139 if !metadata.is_file() {
140 return Err(S3Error::configuration(
141 "upload path must identify a regular file",
142 ));
143 }
144
145 let snapshot = tempfile::NamedTempFile::new().map_err(S3Error::transport)?;
149 let (snapshot_file, snapshot_path) = snapshot.into_parts();
150 let mut snapshot_file = File::from_std(snapshot_file);
151 let mut hasher = Sha256::new();
152 let mut length = 0_u64;
153 let mut buffer = vec![0_u8; 64 * 1024];
154 loop {
155 let read = file.read(&mut buffer).await.map_err(S3Error::transport)?;
156 if read == 0 {
157 break;
158 }
159 length =
160 length
161 .checked_add(u64::try_from(read).map_err(|_| {
162 S3Error::integrity("upload file chunk length does not fit in u64")
163 })?)
164 .ok_or_else(|| S3Error::integrity("upload file length overflow"))?;
165 hasher.update(&buffer[..read]);
166 snapshot_file
167 .write_all(&buffer[..read])
168 .await
169 .map_err(S3Error::transport)?;
170 }
171 snapshot_file.flush().await.map_err(S3Error::transport)?;
172 drop(snapshot_file);
173 let mut permissions = std::fs::metadata(&snapshot_path)
174 .map_err(S3Error::transport)?
175 .permissions();
176 permissions.set_readonly(true);
177 std::fs::set_permissions(&snapshot_path, permissions).map_err(S3Error::transport)?;
178
179 Ok(PreparedBody {
180 source: PreparedSource::FileSnapshot(snapshot_path),
181 length,
182 sha256: hasher.finalize().into(),
183 })
184}
185
186pub(crate) struct PreparedBody {
187 source: PreparedSource,
188 length: u64,
189 sha256: [u8; 32],
190}
191
192enum PreparedSource {
193 Bytes(Bytes),
194 FileSnapshot(tempfile::TempPath),
195 OneShot(Mutex<Option<UploadItems>>),
196}
197
198impl PreparedBody {
199 pub(crate) fn length(&self) -> u64 {
200 self.length
201 }
202
203 pub(crate) fn sha256_hex(&self) -> String {
204 encode_hex(&self.sha256)
205 }
206
207 pub(crate) fn sha256_base64(&self) -> String {
208 base64::engine::general_purpose::STANDARD.encode(self.sha256)
209 }
210
211 pub(crate) fn is_replayable(&self) -> bool {
212 !matches!(self.source, PreparedSource::OneShot(_))
213 }
214
215 pub(crate) async fn request_body(&self) -> Result<TransportBody, S3Error> {
216 match &self.source {
217 PreparedSource::Bytes(bytes) => Ok(TransportBody::new(
218 Box::pin(futures_util::stream::once(std::future::ready(Ok(
219 bytes.clone()
220 )))),
221 self.length,
222 self.sha256,
223 )),
224 PreparedSource::FileSnapshot(path) => {
225 let file = File::open(path).await.map_err(S3Error::transport)?;
226 let stream = ReaderStream::with_capacity(file.take(self.length), 64 * 1024);
227 Ok(TransportBody::new(
228 Box::pin(stream),
229 self.length,
230 self.sha256,
231 ))
232 }
233 PreparedSource::OneShot(stream) => {
234 let stream = stream.lock().await.take().ok_or_else(|| {
235 S3Error::unsupported("a non-replayable upload body cannot be sent twice")
236 })?;
237 Ok(TransportBody::new(stream, self.length, self.sha256))
238 }
239 }
240 }
241}
242
243pub(crate) struct TransportBody {
245 stream: UploadItems,
246 remaining: u64,
247 expected_sha256: [u8; 32],
248 hasher: Sha256,
249 final_chunk: Option<Bytes>,
250 finished: bool,
251}
252
253impl TransportBody {
254 fn new(stream: UploadItems, length: u64, expected_sha256: [u8; 32]) -> Self {
255 Self {
256 stream,
257 remaining: length,
258 expected_sha256,
259 hasher: Sha256::new(),
260 final_chunk: None,
261 finished: false,
262 }
263 }
264
265 pub(crate) fn empty() -> Self {
266 let digest = Sha256::digest([]).into();
267 Self::new(Box::pin(futures_util::stream::empty()), 0, digest)
268 }
269}
270
271impl Body for TransportBody {
272 type Data = Bytes;
273 type Error = S3Error;
274
275 fn poll_frame(
276 mut self: Pin<&mut Self>,
277 context: &mut Context<'_>,
278 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
279 if self.finished {
280 return Poll::Ready(None);
281 }
282
283 loop {
284 if self.final_chunk.is_some() {
285 match self.stream.as_mut().poll_next(context) {
286 Poll::Ready(Some(Ok(chunk))) if chunk.is_empty() => continue,
287 Poll::Ready(Some(Ok(_))) => {
288 self.finished = true;
289 self.remaining = 0;
290 self.final_chunk = None;
291 return Poll::Ready(Some(Err(S3Error::integrity(
292 "upload exceeded its declared content length",
293 ))));
294 }
295 Poll::Ready(Some(Err(error))) => {
296 self.finished = true;
297 self.remaining = 0;
298 self.final_chunk = None;
299 return Poll::Ready(Some(Err(S3Error::transport(error))));
300 }
301 Poll::Ready(None) => {
302 let digest: [u8; 32] = self.hasher.clone().finalize().into();
303 if digest != self.expected_sha256 {
304 self.finished = true;
305 self.remaining = 0;
306 self.final_chunk = None;
307 return Poll::Ready(Some(Err(S3Error::integrity(
308 "upload bytes did not match the signed payload digest",
309 ))));
310 }
311 self.finished = true;
312 self.remaining = 0;
313 let chunk = self
314 .final_chunk
315 .take()
316 .expect("final upload chunk was established");
317 return Poll::Ready(Some(Ok(Frame::data(chunk))));
318 }
319 Poll::Pending => return Poll::Pending,
320 }
321 }
322
323 match self.stream.as_mut().poll_next(context) {
324 Poll::Ready(Some(Ok(chunk))) => {
325 if chunk.is_empty() {
326 continue;
327 }
328 let Ok(chunk_length) = u64::try_from(chunk.len()) else {
329 self.finished = true;
330 self.remaining = 0;
331 return Poll::Ready(Some(Err(S3Error::integrity(
332 "upload chunk length does not fit in u64",
333 ))));
334 };
335 if chunk_length > self.remaining {
336 self.finished = true;
337 self.remaining = 0;
338 return Poll::Ready(Some(Err(S3Error::integrity(
339 "upload exceeded its declared content length",
340 ))));
341 }
342 self.hasher.update(&chunk);
343 if chunk_length == self.remaining {
344 self.final_chunk = Some(chunk);
345 continue;
346 }
347 self.remaining -= chunk_length;
348 return Poll::Ready(Some(Ok(Frame::data(chunk))));
349 }
350 Poll::Ready(Some(Err(error))) => {
351 self.finished = true;
352 self.remaining = 0;
353 return Poll::Ready(Some(Err(S3Error::transport(error))));
354 }
355 Poll::Ready(None) => {
356 self.finished = true;
357 if self.remaining != 0 {
358 self.remaining = 0;
359 return Poll::Ready(Some(Err(S3Error::integrity(
360 "upload ended before its declared content length",
361 ))));
362 }
363 let digest: [u8; 32] = self.hasher.clone().finalize().into();
364 return if digest == self.expected_sha256 {
365 Poll::Ready(None)
366 } else {
367 Poll::Ready(Some(Err(S3Error::integrity(
368 "upload bytes did not match the signed payload digest",
369 ))))
370 };
371 }
372 Poll::Pending => return Poll::Pending,
373 }
374 }
375 }
376
377 fn is_end_stream(&self) -> bool {
378 self.finished
379 }
380
381 fn size_hint(&self) -> SizeHint {
382 SizeHint::with_exact(self.remaining)
383 }
384}
385
386fn encode_hex(bytes: &[u8]) -> String {
387 const HEX: &[u8; 16] = b"0123456789abcdef";
388 let mut output = String::with_capacity(bytes.len() * 2);
389 for byte in bytes {
390 output.push(char::from(HEX[usize::from(byte >> 4)]));
391 output.push(char::from(HEX[usize::from(byte & 0x0f)]));
392 }
393 output
394}
395
396#[cfg(test)]
397mod tests {
398 use std::io;
399
400 use bytes::Bytes;
401 use futures_util::stream;
402 use http_body_util::BodyExt;
403 use sha2::{Digest, Sha256};
404 use tokio::io::AsyncWriteExt;
405
406 use super::ByteStream;
407
408 #[tokio::test]
409 async fn bytes_are_replayable_and_hashed() {
410 let prepared = ByteStream::from_bytes(Bytes::from_static(b"hello"))
411 .prepare()
412 .await
413 .expect("body prepares");
414 assert!(prepared.is_replayable());
415 assert_eq!(prepared.length(), 5);
416 assert_eq!(
417 prepared.sha256_hex(),
418 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
419 );
420 let first = prepared
421 .request_body()
422 .await
423 .expect("first body")
424 .collect()
425 .await
426 .expect("first body is valid")
427 .to_bytes();
428 let second = prepared
429 .request_body()
430 .await
431 .expect("second body")
432 .collect()
433 .await
434 .expect("second body is valid")
435 .to_bytes();
436 assert_eq!(first, Bytes::from_static(b"hello"));
437 assert_eq!(second, first);
438 }
439
440 #[tokio::test]
441 async fn one_shot_stream_rejects_second_attempt() {
442 let sha256 = Sha256::digest(b"x").into();
443 let source = stream::iter([Ok::<_, io::Error>(Bytes::from_static(b"x"))]);
444 let prepared = ByteStream::from_stream(source, 1, sha256)
445 .prepare()
446 .await
447 .expect("body prepares");
448 assert!(!prepared.is_replayable());
449 prepared
450 .request_body()
451 .await
452 .expect("first body")
453 .collect()
454 .await
455 .expect("first body is valid");
456 assert!(prepared.request_body().await.is_err());
457 }
458
459 #[tokio::test]
460 async fn file_retries_use_the_immutable_hashed_snapshot() {
461 let mut original = tempfile::NamedTempFile::new().unwrap();
462 std::io::Write::write_all(&mut original, b"signed bytes").unwrap();
463 std::io::Write::flush(&mut original).unwrap();
464 let prepared = ByteStream::from_path(original.path())
465 .prepare()
466 .await
467 .expect("file body prepares");
468
469 let mut changed = tokio::fs::File::create(original.path()).await.unwrap();
470 changed.write_all(b"different bytes").await.unwrap();
471 changed.flush().await.unwrap();
472
473 for _ in 0..2 {
474 let body = prepared
475 .request_body()
476 .await
477 .expect("snapshot opens")
478 .collect()
479 .await
480 .expect("snapshot body is valid")
481 .to_bytes();
482 assert_eq!(body, Bytes::from_static(b"signed bytes"));
483 }
484 }
485
486 #[tokio::test]
487 async fn one_shot_stream_enforces_declared_length_and_digest() {
488 let too_long = stream::iter([Ok::<_, io::Error>(Bytes::from_static(b"ab"))]);
489 let prepared = ByteStream::from_stream(too_long, 1, Sha256::digest(b"a").into())
490 .prepare()
491 .await
492 .unwrap();
493 assert!(
494 prepared
495 .request_body()
496 .await
497 .unwrap()
498 .collect()
499 .await
500 .is_err()
501 );
502
503 let wrong_digest = stream::iter([Ok::<_, io::Error>(Bytes::from_static(b"x"))]);
504 let prepared = ByteStream::from_stream(wrong_digest, 1, [0_u8; 32])
505 .prepare()
506 .await
507 .unwrap();
508 assert!(
509 prepared
510 .request_body()
511 .await
512 .unwrap()
513 .collect()
514 .await
515 .is_err()
516 );
517
518 let trailing_chunk = stream::iter([
519 Ok::<_, io::Error>(Bytes::from_static(b"a")),
520 Ok::<_, io::Error>(Bytes::from_static(b"b")),
521 ]);
522 let prepared = ByteStream::from_stream(trailing_chunk, 1, Sha256::digest(b"a").into())
523 .prepare()
524 .await
525 .unwrap();
526 let error = prepared
527 .request_body()
528 .await
529 .unwrap()
530 .collect()
531 .await
532 .unwrap_err();
533 assert_eq!(error.category(), crate::error::ErrorCategory::Integrity);
534 }
535}