Skip to main content

s3/
bucket.rs

1//! # Rust S3 Bucket Operations
2//!
3//! This module provides functionality for interacting with S3 buckets and objects,
4//! including creating, listing, uploading, downloading, and deleting objects. It supports
5//! various features such as asynchronous and blocking operations, multipart uploads,
6//! presigned URLs, and tagging objects.
7//!
8//! ## Features
9//!
10//! The module supports the following features:
11//!
12//! - **blocking**: Enables blocking (synchronous) operations using the `block_on` macro.
13//! - **tags**: Adds support for managing S3 object tags.
14//! - **with-tokio**: Enables asynchronous operations using the Tokio runtime.
15//! - **with-async-std**: Enables asynchronous operations using the async-std runtime.
16//! - **sync**: Enables synchronous (blocking) operations using standard Rust synchronization primitives.
17//!
18//! ## Constants
19//!
20//! - `CHUNK_SIZE`: Defines the chunk size for multipart uploads (8 MiB).
21//! - `DEFAULT_REQUEST_TIMEOUT`: The default request timeout (60 seconds).
22//!
23//! ## Types
24//!
25//! - `Query`: A type alias for `HashMap<String, String>`, representing query parameters for requests.
26//!
27//! ## Structs
28//!
29//! - `Bucket`: Represents an S3 bucket, providing methods to interact with the bucket and its contents.
30//! - `Tag`: Represents a key-value pair used for tagging S3 objects.
31//!
32//! ## Errors
33//!
34//! - `S3Error`: Represents various errors that can occur during S3 operations.
35
36#[cfg(feature = "blocking")]
37use block_on_proc::block_on;
38#[cfg(feature = "tags")]
39use minidom::Element;
40use std::collections::{HashMap, HashSet};
41use std::time::Duration;
42
43use crate::bucket_ops::{BucketConfiguration, CreateBucketResponse};
44use crate::command::{Command, Multipart};
45use crate::creds::Credentials;
46use crate::region::Region;
47#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
48use crate::request::ResponseDataStream;
49#[cfg(feature = "with-tokio")]
50use crate::request::tokio_backend::ClientOptions;
51#[cfg(feature = "with-tokio")]
52use crate::request::tokio_backend::client;
53use crate::request::{Request as _, ResponseData};
54use std::str::FromStr;
55use std::sync::Arc;
56
57#[cfg(feature = "with-tokio")]
58use tokio::sync::{Mutex as AsyncMutex, RwLock};
59
60#[cfg(feature = "with-async-std")]
61use async_std::sync::{Mutex as AsyncMutex, RwLock};
62
63#[cfg(feature = "sync")]
64use std::sync::RwLock;
65
66pub type Query = HashMap<String, String>;
67
68#[cfg(feature = "with-async-std")]
69use crate::request::async_std_backend::SurfRequest as RequestImpl;
70#[cfg(feature = "with-tokio")]
71use crate::request::tokio_backend::ReqwestRequest as RequestImpl;
72
73#[cfg(feature = "with-async-std")]
74use async_std::io::Write as AsyncWrite;
75#[cfg(feature = "with-tokio")]
76use tokio::io::AsyncWrite;
77
78#[cfg(feature = "sync")]
79use crate::request::blocking::AttoRequest as RequestImpl;
80use std::io::Read;
81
82#[cfg(feature = "with-tokio")]
83use tokio::io::AsyncRead;
84
85#[cfg(feature = "with-async-std")]
86use async_std::io::Read as AsyncRead;
87
88use crate::PostPolicy;
89use crate::error::S3Error;
90use crate::post_policy::PresignedPost;
91use crate::serde_types::{
92    BucketLifecycleConfiguration, BucketLocationResult, CompleteMultipartUploadData,
93    CorsConfiguration, DeleteObjectsRequest, DeleteObjectsResult, GetObjectAttributesOutput,
94    HeadObjectResult, InitiateMultipartUploadResponse, ListBucketResult,
95    ListMultipartUploadsResult, ObjectIdentifier, Part,
96};
97#[allow(unused_imports)]
98use crate::utils::{PutStreamResponse, error_from_response_data};
99
100use http::HeaderMap;
101use http::header::HeaderName;
102#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
103use sysinfo::{MemoryRefreshKind, System};
104
105fn validate_success_xml_response(
106    response_data: ResponseData,
107    expected_root: &str,
108) -> Result<ResponseData, S3Error> {
109    if !(200..300).contains(&response_data.status_code()) {
110        return Ok(response_data);
111    }
112
113    let body = response_data.as_slice();
114    let body_text = std::str::from_utf8(body)?;
115    let mut reader = quick_xml::Reader::from_str(body_text);
116    reader.config_mut().check_end_names = true;
117    reader.config_mut().check_comments = true;
118    let mut depth = 0usize;
119    let mut root = None;
120    let mut valid = true;
121    let mut declaration_seen = false;
122    loop {
123        use quick_xml::events::Event;
124        match reader.read_event() {
125            Ok(Event::Start(element)) => {
126                for attr in element.attributes() {
127                    match attr {
128                        Ok(attr)
129                            if attr
130                                .decoded_and_normalized_value(
131                                    quick_xml::XmlVersion::Implicit1_0,
132                                    reader.decoder(),
133                                )
134                                .is_ok() => {}
135                        _ => {
136                            valid = false;
137                            break;
138                        }
139                    }
140                }
141                if depth == 0 {
142                    if root.is_some() {
143                        valid = false;
144                        break;
145                    }
146                    root =
147                        Some(String::from_utf8_lossy(element.local_name().as_ref()).into_owned());
148                }
149                depth += 1;
150            }
151            Ok(Event::Empty(element)) => {
152                for attr in element.attributes() {
153                    match attr {
154                        Ok(attr)
155                            if attr
156                                .decoded_and_normalized_value(
157                                    quick_xml::XmlVersion::Implicit1_0,
158                                    reader.decoder(),
159                                )
160                                .is_ok() => {}
161                        _ => {
162                            valid = false;
163                            break;
164                        }
165                    }
166                }
167                if depth == 0 {
168                    if root.is_some() {
169                        valid = false;
170                        break;
171                    }
172                    root =
173                        Some(String::from_utf8_lossy(element.local_name().as_ref()).into_owned());
174                }
175            }
176            Ok(Event::End(_)) => {
177                if depth == 0 {
178                    valid = false;
179                    break;
180                }
181                depth -= 1;
182            }
183            Ok(Event::Text(text)) => {
184                // quick-xml 0.38's xml_content() used XML 1.1 normalization.
185                // Keep that behavior explicit across the 0.41 API change.
186                if text.xml11_content().is_err()
187                    || (depth == 0 && !text.as_ref().iter().all(u8::is_ascii_whitespace))
188                {
189                    valid = false;
190                    break;
191                }
192            }
193            Ok(Event::GeneralRef(reference)) => {
194                let name = reference.as_ref();
195                let predefined = matches!(name, b"amp" | b"lt" | b"gt" | b"apos" | b"quot");
196                if depth == 0
197                    || (!predefined && reference.resolve_char_ref().ok().flatten().is_none())
198                {
199                    valid = false;
200                    break;
201                }
202            }
203            Ok(Event::Eof) => break,
204            Ok(Event::DocType(_)) => {
205                valid = false;
206                break;
207            }
208            Ok(Event::Decl(declaration)) if root.is_none() && !declaration_seen => {
209                declaration_seen = true;
210                if !matches!(declaration.version().as_deref(), Ok(b"1.0") | Ok(b"1.1")) {
211                    valid = false;
212                    break;
213                }
214            }
215            Ok(Event::Decl(_)) => {
216                valid = false;
217                break;
218            }
219            Ok(Event::Comment(_) | Event::PI(_)) => {}
220            Ok(Event::CData(_)) if depth > 0 => {}
221            Ok(_) => {
222                valid = false;
223                break;
224            }
225            Err(_) => {
226                valid = false;
227                break;
228            }
229        }
230    }
231
232    if !valid || depth != 0 || root.is_none() {
233        return Err(S3Error::SerdeXml(quick_xml::de::DeError::Custom(
234            "invalid or incomplete XML response".to_owned(),
235        )));
236    }
237    match root.as_deref() {
238        Some("Error") => Err(error_from_response_data(response_data)?),
239        Some(root) if root == expected_root => Ok(response_data),
240        _ => Err(S3Error::HttpFailWithBody(
241            response_data.status_code(),
242            String::from_utf8_lossy(body).into_owned(),
243        )),
244    }
245}
246
247fn response_error_from_data(response_data: ResponseData) -> S3Error {
248    match error_from_response_data(response_data) {
249        Ok(error) | Err(error) => error,
250    }
251}
252
253#[cfg(all(
254    not(feature = "sync"),
255    any(feature = "with-tokio", feature = "with-async-std")
256))]
257#[derive(Default)]
258struct MultipartHeaderPlan {
259    initiate: HeaderMap,
260    part: HeaderMap,
261    complete: HeaderMap,
262    abort: HeaderMap,
263}
264
265#[cfg(all(
266    not(feature = "sync"),
267    any(feature = "with-tokio", feature = "with-async-std")
268))]
269impl MultipartHeaderPlan {
270    fn from_headers(headers: Option<&HeaderMap>) -> Result<Self, S3Error> {
271        let mut plan = Self::default();
272        let Some(headers) = headers else {
273            return Ok(plan);
274        };
275
276        for (name, value) in headers {
277            let name_text = name.as_str();
278            if name_text == "content-md5"
279                || name_text == "content-length"
280                || name_text == "transfer-encoding"
281                || name_text == "x-amz-content-sha256"
282                || name_text.starts_with("x-amz-checksum-")
283                || name_text.starts_with("x-amz-sdk-checksum-")
284            {
285                return Err(S3Error::UnsupportedMultipartHeader(name.clone()));
286            }
287
288            // The request's explicit content_type argument generates this
289            // header after custom headers are merged, so it remains authoritative.
290            if name_text == "content-type" {
291                continue;
292            }
293
294            if matches!(
295                name_text,
296                "x-amz-expected-bucket-owner" | "x-amz-request-payer"
297            ) {
298                plan.initiate.insert(name.clone(), value.clone());
299                plan.part.insert(name.clone(), value.clone());
300                plan.complete.insert(name.clone(), value.clone());
301                plan.abort.insert(name.clone(), value.clone());
302            } else if matches!(
303                name_text,
304                "x-amz-server-side-encryption-customer-algorithm"
305                    | "x-amz-server-side-encryption-customer-key"
306                    | "x-amz-server-side-encryption-customer-key-md5"
307            ) {
308                plan.initiate.insert(name.clone(), value.clone());
309                plan.part.insert(name.clone(), value.clone());
310                plan.complete.insert(name.clone(), value.clone());
311            } else if matches!(name_text, "if-match" | "if-none-match") {
312                plan.complete.insert(name.clone(), value.clone());
313            } else {
314                // PutObject properties and provider-specific custom headers are
315                // applied at multipart initiation, where S3 stores object metadata.
316                plan.initiate.insert(name.clone(), value.clone());
317            }
318        }
319
320        Ok(plan)
321    }
322}
323
324#[cfg(feature = "sync")]
325fn part_from_response(response_data: ResponseData, part_number: u32) -> Result<Part, S3Error> {
326    if !(200..300).contains(&response_data.status_code()) {
327        return Err(response_error_from_data(response_data));
328    }
329    let etag = response_data.as_str()?.to_owned();
330    Ok(Part { etag, part_number })
331}
332
333pub const CHUNK_SIZE: usize = 8_388_608; // 8 Mebibytes, min is 5 (5_242_880);
334
335const DEFAULT_REQUEST_TIMEOUT: Option<Duration> = Some(Duration::from_secs(60));
336
337#[derive(Debug, PartialEq, Eq)]
338pub struct Tag {
339    key: String,
340    value: String,
341}
342
343impl Tag {
344    pub fn key(&self) -> String {
345        self.key.to_owned()
346    }
347
348    pub fn value(&self) -> String {
349        self.value.to_owned()
350    }
351}
352
353/// Instantiate an existing Bucket
354///
355/// # Example
356///
357/// ```no_run
358/// use s3::bucket::Bucket;
359/// use s3::creds::Credentials;
360///
361/// let bucket_name = "rust-s3-test";
362/// let region = "us-east-1".parse().unwrap();
363/// let credentials = Credentials::default().unwrap();
364///
365/// let bucket = Bucket::new(bucket_name, region, credentials);
366/// ```
367#[derive(Clone, Debug)]
368pub struct Bucket {
369    pub name: String,
370    pub region: Region,
371    credentials: Arc<RwLock<Credentials>>,
372    #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
373    credentials_refresh_gate: Arc<AsyncMutex<()>>,
374    pub extra_headers: HeaderMap,
375    pub extra_query: Query,
376    pub request_timeout: Option<Duration>,
377    path_style: bool,
378    listobjects_v2: bool,
379    #[cfg(feature = "with-tokio")]
380    http_client: reqwest::Client,
381    #[cfg(feature = "with-tokio")]
382    client_options: crate::request::tokio_backend::ClientOptions,
383}
384
385impl Bucket {
386    #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
387    async fn credentials_refresh_with<F>(&self, refresh: F) -> Result<(), S3Error>
388    where
389        F: FnOnce(Credentials) -> Result<Credentials, crate::creds::error::CredentialsError>
390            + Send
391            + 'static,
392    {
393        let expired = {
394            let credentials = self.credentials.read().await;
395            credentials
396                .expiration
397                .is_some_and(|expiration| *expiration <= time::OffsetDateTime::now_utc())
398        };
399        if !expired {
400            return Ok(());
401        }
402
403        let _refresh_guard = self.credentials_refresh_gate.lock().await;
404        let credentials = { self.credentials.read().await.clone() };
405        if !credentials
406            .expiration
407            .is_some_and(|expiration| *expiration <= time::OffsetDateTime::now_utc())
408        {
409            return Ok(());
410        }
411
412        #[cfg(feature = "with-tokio")]
413        let refreshed = tokio::task::spawn_blocking(move || refresh(credentials))
414            .await
415            .map_err(|_| S3Error::Io(std::io::Error::other("credential refresh task failed")))??;
416
417        #[cfg(all(not(feature = "with-tokio"), feature = "with-async-std"))]
418        let refreshed = async_std::task::spawn_blocking(move || refresh(credentials)).await?;
419
420        *self.credentials.write().await = refreshed;
421        Ok(())
422    }
423
424    #[maybe_async::async_impl]
425    /// Credential refreshing is done automatically, but can be manually triggered.
426    pub async fn credentials_refresh(&self) -> Result<(), S3Error> {
427        self.credentials_refresh_with(|mut credentials| {
428            credentials.refresh()?;
429            Ok(credentials)
430        })
431        .await
432    }
433
434    #[maybe_async::sync_impl]
435    /// Credential refreshing is done automatically, but can be manually triggered.
436    pub fn credentials_refresh(&self) -> Result<(), S3Error> {
437        match self.credentials.write() {
438            Ok(mut credentials) => Ok(credentials.refresh()?),
439            Err(_) => Err(S3Error::CredentialsWriteLock),
440        }
441    }
442
443    #[cfg(feature = "with-tokio")]
444    pub fn http_client(&self) -> reqwest::Client {
445        self.http_client.clone()
446    }
447}
448
449fn validate_expiry(expiry_secs: u32) -> Result<(), S3Error> {
450    if 604800 < expiry_secs {
451        return Err(S3Error::MaxExpiry(expiry_secs));
452    }
453    Ok(())
454}
455
456#[cfg_attr(all(feature = "with-tokio", feature = "blocking"), block_on("tokio"))]
457#[cfg_attr(
458    all(feature = "with-async-std", feature = "blocking"),
459    block_on("async-std")
460)]
461impl Bucket {
462    /// Get a presigned url for getting object on a given path
463    ///
464    /// # Example:
465    ///
466    /// ```no_run
467    /// use std::collections::HashMap;
468    /// use s3::bucket::Bucket;
469    /// use s3::creds::Credentials;
470    ///
471    /// #[tokio::main]
472    /// async fn main() {
473    /// let bucket_name = "rust-s3-test";
474    /// let region = "us-east-1".parse().unwrap();
475    /// let credentials = Credentials::default().unwrap();
476    /// let bucket = Bucket::new(bucket_name, region, credentials).unwrap();
477    ///
478    /// // Add optional custom queries
479    /// let mut custom_queries = HashMap::new();
480    /// custom_queries.insert(
481    ///    "response-content-disposition".into(),
482    ///    "attachment; filename=\"test.png\"".into(),
483    /// );
484    ///
485    /// #[cfg(not(feature = "sync"))]
486    /// let url = bucket.presign_get("/test.file", 86400, Some(custom_queries)).await.unwrap();
487    /// #[cfg(feature = "sync")]
488    /// let url = bucket.presign_get("/test.file", 86400, Some(custom_queries)).unwrap();
489    /// println!("Presigned url: {}", url);
490    /// }
491    /// ```
492    #[maybe_async::maybe_async]
493    pub async fn presign_get<S: AsRef<str>>(
494        &self,
495        path: S,
496        expiry_secs: u32,
497        custom_queries: Option<HashMap<String, String>>,
498    ) -> Result<String, S3Error> {
499        validate_expiry(expiry_secs)?;
500        let request = RequestImpl::new(
501            self,
502            path.as_ref(),
503            Command::PresignGet {
504                expiry_secs,
505                custom_queries,
506            },
507        )
508        .await?;
509        request.presigned().await
510    }
511
512    /// Get a presigned url for posting an object to a given path
513    ///
514    /// # Example:
515    ///
516    /// ```no_run
517    /// use s3::bucket::Bucket;
518    /// use s3::creds::Credentials;
519    /// use s3::post_policy::*;
520    /// use std::borrow::Cow;
521    ///
522    /// #[tokio::main]
523    /// async fn main() {
524    /// let bucket_name = "rust-s3-test";
525    /// let region = "us-east-1".parse().unwrap();
526    /// let credentials = Credentials::default().unwrap();
527    /// let bucket = Bucket::new(bucket_name, region, credentials).unwrap();
528    ///
529    /// let post_policy = PostPolicy::new(86400).condition(
530    ///     PostPolicyField::Key,
531    ///     PostPolicyValue::StartsWith(Cow::from("user/user1/"))
532    /// ).unwrap();
533    ///
534    /// #[cfg(not(feature = "sync"))]
535    /// let presigned_post = bucket.presign_post(post_policy).await.unwrap();
536    /// #[cfg(feature = "sync")]
537    /// let presigned_post = bucket.presign_post(post_policy).unwrap();
538    /// println!("Presigned url: {}, fields: {:?}", presigned_post.url, presigned_post.fields);
539    /// }
540    /// ```
541    #[maybe_async::maybe_async]
542    #[allow(clippy::needless_lifetimes)]
543    pub async fn presign_post<'a>(
544        &self,
545        post_policy: PostPolicy<'a>,
546    ) -> Result<PresignedPost, S3Error> {
547        post_policy.sign(Box::new(self.clone())).await
548    }
549
550    /// Get a presigned url for putting object to a given path
551    ///
552    /// # Example:
553    ///
554    /// ```no_run
555    /// use s3::bucket::Bucket;
556    /// use s3::creds::Credentials;
557    /// use http::HeaderMap;
558    /// use http::header::HeaderName;
559    /// #[tokio::main]
560    /// async fn main() {
561    /// let bucket_name = "rust-s3-test";
562    /// let region = "us-east-1".parse().unwrap();
563    /// let credentials = Credentials::default().unwrap();
564    /// let bucket = Bucket::new(bucket_name, region, credentials).unwrap();
565    ///
566    /// // Add optional custom headers
567    /// let mut custom_headers = HeaderMap::new();
568    /// custom_headers.insert(
569    ///    HeaderName::from_static("custom_header"),
570    ///    "custom_value".parse().unwrap(),
571    /// );
572    ///
573    /// #[cfg(not(feature = "sync"))]
574    /// let url = bucket.presign_put("/test.file", 86400, Some(custom_headers), None).await.unwrap();
575    /// #[cfg(feature = "sync")]
576    /// let url = bucket.presign_put("/test.file", 86400, Some(custom_headers), None).unwrap();
577    /// println!("Presigned url: {}", url);
578    /// }
579    /// ```
580    #[maybe_async::maybe_async]
581    pub async fn presign_put<S: AsRef<str>>(
582        &self,
583        path: S,
584        expiry_secs: u32,
585        custom_headers: Option<HeaderMap>,
586        custom_queries: Option<HashMap<String, String>>,
587    ) -> Result<String, S3Error> {
588        validate_expiry(expiry_secs)?;
589        let request = RequestImpl::new(
590            self,
591            path.as_ref(),
592            Command::PresignPut {
593                expiry_secs,
594                custom_headers,
595                custom_queries,
596            },
597        )
598        .await?;
599        request.presigned().await
600    }
601
602    /// Get a presigned url for deleting object on a given path
603    ///
604    /// # Example:
605    ///
606    /// ```no_run
607    /// use s3::bucket::Bucket;
608    /// use s3::creds::Credentials;
609    ///
610    ///
611    /// #[tokio::main]
612    /// async fn main() {
613    /// let bucket_name = "rust-s3-test";
614    /// let region = "us-east-1".parse().unwrap();
615    /// let credentials = Credentials::default().unwrap();
616    /// let bucket = Bucket::new(bucket_name, region, credentials).unwrap();
617    ///
618    /// #[cfg(not(feature = "sync"))]
619    /// let url = bucket.presign_delete("/test.file", 86400).await.unwrap();
620    /// #[cfg(feature = "sync")]
621    /// let url = bucket.presign_delete("/test.file", 86400).unwrap();
622    /// println!("Presigned url: {}", url);
623    /// }
624    /// ```
625    #[maybe_async::maybe_async]
626    pub async fn presign_delete<S: AsRef<str>>(
627        &self,
628        path: S,
629        expiry_secs: u32,
630    ) -> Result<String, S3Error> {
631        validate_expiry(expiry_secs)?;
632        let request =
633            RequestImpl::new(self, path.as_ref(), Command::PresignDelete { expiry_secs }).await?;
634        request.presigned().await
635    }
636
637    /// Create a new `Bucket` and instantiate it
638    ///
639    /// ```no_run
640    /// use s3::{Bucket, BucketConfiguration};
641    /// use s3::creds::Credentials;
642    /// # use s3::region::Region;
643    /// use anyhow::Result;
644    ///
645    /// # #[tokio::main]
646    /// # async fn main() -> Result<()> {
647    /// let bucket_name = "rust-s3-test";
648    /// let region = "us-east-1".parse()?;
649    /// let credentials = Credentials::default()?;
650    /// let config = BucketConfiguration::default();
651    ///
652    /// // Async variant with `tokio` or `async-std` features
653    /// #[cfg(not(feature = "sync"))]
654    /// let create_bucket_response = Bucket::create(bucket_name, region, credentials, config).await?;
655    ///
656    /// // `sync` fature will produce an identical method
657    /// #[cfg(feature = "sync")]
658    /// let create_bucket_response = Bucket::create(bucket_name, region, credentials, config)?;
659    ///
660    /// # let region: Region = "us-east-1".parse()?;
661    /// # let credentials = Credentials::default()?;
662    /// # let config = BucketConfiguration::default();
663    /// // Blocking variant, generated with `blocking` feature in combination
664    /// // with `tokio` or `async-std` features.
665    /// #[cfg(feature = "blocking")]
666    /// let create_bucket_response = Bucket::create_blocking(bucket_name, region, credentials, config)?;
667    /// # Ok(())
668    /// # }
669    /// ```
670    #[maybe_async::maybe_async]
671    pub async fn create(
672        name: &str,
673        region: Region,
674        credentials: Credentials,
675        config: BucketConfiguration,
676    ) -> Result<CreateBucketResponse, S3Error> {
677        let mut config = config;
678
679        // Check if we should skip location constraint for LocalStack/Minio compatibility
680        // This env var allows users to create buckets on S3-compatible services that
681        // don't support or require location constraints in the request body
682        let skip_constraint = std::env::var("RUST_S3_SKIP_LOCATION_CONSTRAINT")
683            .unwrap_or_default()
684            .to_lowercase();
685
686        if skip_constraint != "true" && skip_constraint != "1" {
687            config.set_region(region.clone());
688        }
689
690        let command = Command::CreateBucket { config };
691        let bucket = Bucket::new(name, region, credentials)?;
692        let request = RequestImpl::new(&bucket, "", command).await?;
693        let response_data = request.response_data(false).await?;
694        let response_text = response_data.as_str()?;
695        Ok(CreateBucketResponse {
696            bucket,
697            response_text: response_text.to_string(),
698            response_code: response_data.status_code(),
699        })
700    }
701
702    /// Get a list of all existing buckets in the region
703    /// that are accessible by the given credentials.
704    /// ```no_run
705    /// use s3::{Bucket, BucketConfiguration};
706    /// use s3::creds::Credentials;
707    /// use s3::region::Region;
708    /// use anyhow::Result;
709    ///
710    /// # #[tokio::main]
711    /// # async fn main() -> Result<()> {
712    /// let region = Region::Custom {
713    ///   region: "eu-central-1".to_owned(),
714    ///   endpoint: "http://localhost:9000".to_owned()
715    /// };
716    /// let credentials = Credentials::default()?;
717    ///
718    /// // Async variant with `tokio` or `async-std` features
719    /// #[cfg(not(feature = "sync"))]
720    /// let response = Bucket::list_buckets(region.clone(), credentials.clone()).await?;
721    ///
722    /// // `sync` feature will produce an identical method
723    /// #[cfg(feature = "sync")]
724    /// let response = Bucket::list_buckets(region, credentials)?;
725    ///
726    /// // Blocking variant, generated with `blocking` feature in combination
727    /// // with `tokio` or `async-std` features.
728    /// #[cfg(feature = "blocking")]
729    /// let response = Bucket::list_buckets_blocking(region, credentials)?;
730    ///
731    /// let found_buckets = response.bucket_names().collect::<Vec<String>>();
732    /// println!("found buckets: {:#?}", found_buckets);
733    /// # Ok(())
734    /// # }
735    /// ```
736    #[maybe_async::maybe_async]
737    pub async fn list_buckets(
738        region: Region,
739        credentials: Credentials,
740    ) -> Result<crate::bucket_ops::ListBucketsResponse, S3Error> {
741        let dummy_bucket = Bucket::new("", region, credentials)?.with_path_style();
742        dummy_bucket._list_buckets().await
743    }
744
745    /// Internal helper method that performs the actual bucket listing operation.
746    /// Used by the public `list_buckets` method to retrieve the list of buckets for the configured client.
747    #[maybe_async::maybe_async]
748    async fn _list_buckets(&self) -> Result<crate::bucket_ops::ListBucketsResponse, S3Error> {
749        let request = RequestImpl::new(self, "", Command::ListBuckets).await?;
750        let response = request.response_data(false).await?;
751
752        Ok(quick_xml::de::from_str::<
753            crate::bucket_ops::ListBucketsResponse,
754        >(response.as_str()?)?)
755    }
756
757    /// Determine whether the instantiated bucket exists.
758    /// ```no_run
759    /// use s3::{Bucket, BucketConfiguration};
760    /// use s3::creds::Credentials;
761    /// use s3::region::Region;
762    /// use anyhow::Result;
763    ///
764    /// # #[tokio::main]
765    /// # async fn main() -> Result<()> {
766    /// let bucket_name = "some-bucket-that-is-known-to-exist";
767    /// let region = "us-east-1".parse()?;
768    /// let credentials = Credentials::default()?;
769    ///
770    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
771    ///
772    /// // Async variant with `tokio` or `async-std` features
773    /// #[cfg(not(feature = "sync"))]
774    /// let exists = bucket.exists().await?;
775    ///
776    /// // `sync` feature will produce an identical method
777    /// #[cfg(feature = "sync")]
778    /// let exists = bucket.exists()?;
779    ///
780    /// // Blocking variant, generated with `blocking` feature in combination
781    /// // with `tokio` or `async-std` features.
782    /// #[cfg(feature = "blocking")]
783    /// let exists = bucket.exists_blocking()?;
784    ///
785    /// assert_eq!(exists, true);
786    /// # Ok(())
787    /// # }
788    /// ```
789    #[maybe_async::maybe_async]
790    pub async fn exists(&self) -> Result<bool, S3Error> {
791        let mut dummy_bucket = self.clone();
792        dummy_bucket.name = "".into();
793
794        let response = dummy_bucket._list_buckets().await?;
795
796        Ok(response
797            .bucket_names()
798            .collect::<std::collections::HashSet<String>>()
799            .contains(&self.name))
800    }
801
802    /// Create a new `Bucket` with path style and instantiate it
803    ///
804    /// ```no_run
805    /// use s3::{Bucket, BucketConfiguration};
806    /// use s3::creds::Credentials;
807    /// # use s3::region::Region;
808    /// use anyhow::Result;
809    ///
810    /// # #[tokio::main]
811    /// # async fn main() -> Result<()> {
812    /// let bucket_name = "rust-s3-test";
813    /// let region = "us-east-1".parse()?;
814    /// let credentials = Credentials::default()?;
815    /// let config = BucketConfiguration::default();
816    ///
817    /// // Async variant with `tokio` or `async-std` features
818    /// #[cfg(not(feature = "sync"))]
819    /// let create_bucket_response = Bucket::create_with_path_style(bucket_name, region, credentials, config).await?;
820    ///
821    /// // `sync` fature will produce an identical method
822    /// #[cfg(feature = "sync")]
823    /// let create_bucket_response = Bucket::create_with_path_style(bucket_name, region, credentials, config)?;
824    ///
825    /// # let region: Region = "us-east-1".parse()?;
826    /// # let credentials = Credentials::default()?;
827    /// # let config = BucketConfiguration::default();
828    /// // Blocking variant, generated with `blocking` feature in combination
829    /// // with `tokio` or `async-std` features.
830    /// #[cfg(feature = "blocking")]
831    /// let create_bucket_response = Bucket::create_with_path_style_blocking(bucket_name, region, credentials, config)?;
832    /// # Ok(())
833    /// # }
834    /// ```
835    #[maybe_async::maybe_async]
836    pub async fn create_with_path_style(
837        name: &str,
838        region: Region,
839        credentials: Credentials,
840        config: BucketConfiguration,
841    ) -> Result<CreateBucketResponse, S3Error> {
842        let mut config = config;
843
844        // Check if we should skip location constraint for LocalStack/Minio compatibility
845        // This env var allows users to create buckets on S3-compatible services that
846        // don't support or require location constraints in the request body
847        let skip_constraint = std::env::var("RUST_S3_SKIP_LOCATION_CONSTRAINT")
848            .unwrap_or_default()
849            .to_lowercase();
850
851        if skip_constraint != "true" && skip_constraint != "1" {
852            config.set_region(region.clone());
853        }
854
855        let command = Command::CreateBucket { config };
856        let bucket = Bucket::new(name, region, credentials)?.with_path_style();
857        let request = RequestImpl::new(&bucket, "", command).await?;
858        let response_data = request.response_data(false).await?;
859        let response_text = response_data.to_string()?;
860
861        Ok(CreateBucketResponse {
862            bucket,
863            response_text,
864            response_code: response_data.status_code(),
865        })
866    }
867
868    /// Delete existing `Bucket`
869    ///
870    /// # Example
871    /// ```rust,no_run
872    /// use s3::Bucket;
873    /// use s3::creds::Credentials;
874    /// use anyhow::Result;
875    ///
876    /// # #[tokio::main]
877    /// # async fn main() -> Result<()> {
878    /// let bucket_name = "rust-s3-test";
879    /// let region = "us-east-1".parse().unwrap();
880    /// let credentials = Credentials::default().unwrap();
881    /// let bucket = Bucket::new(bucket_name, region, credentials).unwrap();
882    ///
883    /// // Async variant with `tokio` or `async-std` features
884    /// #[cfg(not(feature = "sync"))]
885    /// bucket.delete().await.unwrap();
886    /// // `sync` fature will produce an identical method
887    ///
888    /// #[cfg(feature = "sync")]
889    /// bucket.delete().unwrap();
890    /// // Blocking variant, generated with `blocking` feature in combination
891    /// // with `tokio` or `async-std` features.
892    ///
893    /// #[cfg(feature = "blocking")]
894    /// bucket.delete_blocking().unwrap();
895    ///
896    /// # Ok(())
897    /// # }
898    /// ```
899    #[maybe_async::maybe_async]
900    pub async fn delete(&self) -> Result<u16, S3Error> {
901        let command = Command::DeleteBucket;
902        let request = RequestImpl::new(self, "", command).await?;
903        let response_data = request.response_data(false).await?;
904        Ok(response_data.status_code())
905    }
906
907    /// Instantiate an existing `Bucket`.
908    ///
909    /// # Example
910    /// ```no_run
911    /// use s3::bucket::Bucket;
912    /// use s3::creds::Credentials;
913    ///
914    /// // Fake  credentials so we don't access user's real credentials in tests
915    /// let bucket_name = "rust-s3-test";
916    /// let region = "us-east-1".parse().unwrap();
917    /// let credentials = Credentials::default().unwrap();
918    ///
919    /// let bucket = Bucket::new(bucket_name, region, credentials).unwrap();
920    /// ```
921    pub fn new(
922        name: &str,
923        region: Region,
924        credentials: Credentials,
925    ) -> Result<Box<Bucket>, S3Error> {
926        #[cfg(feature = "with-tokio")]
927        let options = ClientOptions::default();
928
929        Ok(Box::new(Bucket {
930            name: name.into(),
931            region,
932            credentials: Arc::new(RwLock::new(credentials)),
933            #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
934            credentials_refresh_gate: Arc::new(AsyncMutex::new(())),
935            extra_headers: HeaderMap::new(),
936            extra_query: HashMap::new(),
937            request_timeout: DEFAULT_REQUEST_TIMEOUT,
938            path_style: false,
939            listobjects_v2: true,
940            #[cfg(feature = "with-tokio")]
941            http_client: client(&options)?,
942            #[cfg(feature = "with-tokio")]
943            client_options: options,
944        }))
945    }
946
947    /// Instantiate a public existing `Bucket`.
948    ///
949    /// # Example
950    /// ```no_run
951    /// use s3::bucket::Bucket;
952    ///
953    /// let bucket_name = "rust-s3-test";
954    /// let region = "us-east-1".parse().unwrap();
955    ///
956    /// let bucket = Bucket::new_public(bucket_name, region).unwrap();
957    /// ```
958    pub fn new_public(name: &str, region: Region) -> Result<Bucket, S3Error> {
959        #[cfg(feature = "with-tokio")]
960        let options = ClientOptions::default();
961
962        Ok(Bucket {
963            name: name.into(),
964            region,
965            credentials: Arc::new(RwLock::new(Credentials::anonymous()?)),
966            #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
967            credentials_refresh_gate: Arc::new(AsyncMutex::new(())),
968            extra_headers: HeaderMap::new(),
969            extra_query: HashMap::new(),
970            request_timeout: DEFAULT_REQUEST_TIMEOUT,
971            path_style: false,
972            listobjects_v2: true,
973            #[cfg(feature = "with-tokio")]
974            http_client: client(&options)?,
975            #[cfg(feature = "with-tokio")]
976            client_options: options,
977        })
978    }
979
980    pub fn with_path_style(&self) -> Box<Bucket> {
981        Box::new(Bucket {
982            name: self.name.clone(),
983            region: self.region.clone(),
984            credentials: self.credentials.clone(),
985            #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
986            credentials_refresh_gate: self.credentials_refresh_gate.clone(),
987            extra_headers: self.extra_headers.clone(),
988            extra_query: self.extra_query.clone(),
989            request_timeout: self.request_timeout,
990            path_style: true,
991            listobjects_v2: self.listobjects_v2,
992            #[cfg(feature = "with-tokio")]
993            http_client: self.http_client(),
994            #[cfg(feature = "with-tokio")]
995            client_options: self.client_options.clone(),
996        })
997    }
998
999    pub fn with_extra_headers(&self, extra_headers: HeaderMap) -> Result<Bucket, S3Error> {
1000        Ok(Bucket {
1001            name: self.name.clone(),
1002            region: self.region.clone(),
1003            credentials: self.credentials.clone(),
1004            #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1005            credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1006            extra_headers,
1007            extra_query: self.extra_query.clone(),
1008            request_timeout: self.request_timeout,
1009            path_style: self.path_style,
1010            listobjects_v2: self.listobjects_v2,
1011            #[cfg(feature = "with-tokio")]
1012            http_client: self.http_client(),
1013            #[cfg(feature = "with-tokio")]
1014            client_options: self.client_options.clone(),
1015        })
1016    }
1017
1018    pub fn with_extra_query(
1019        &self,
1020        extra_query: HashMap<String, String>,
1021    ) -> Result<Bucket, S3Error> {
1022        Ok(Bucket {
1023            name: self.name.clone(),
1024            region: self.region.clone(),
1025            credentials: self.credentials.clone(),
1026            #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1027            credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1028            extra_headers: self.extra_headers.clone(),
1029            extra_query,
1030            request_timeout: self.request_timeout,
1031            path_style: self.path_style,
1032            listobjects_v2: self.listobjects_v2,
1033            #[cfg(feature = "with-tokio")]
1034            http_client: self.http_client(),
1035            #[cfg(feature = "with-tokio")]
1036            client_options: self.client_options.clone(),
1037        })
1038    }
1039
1040    #[cfg(not(feature = "with-tokio"))]
1041    pub fn with_request_timeout(&self, request_timeout: Duration) -> Result<Box<Bucket>, S3Error> {
1042        Ok(Box::new(Bucket {
1043            name: self.name.clone(),
1044            region: self.region.clone(),
1045            credentials: self.credentials.clone(),
1046            #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1047            credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1048            extra_headers: self.extra_headers.clone(),
1049            extra_query: self.extra_query.clone(),
1050            request_timeout: Some(request_timeout),
1051            path_style: self.path_style,
1052            listobjects_v2: self.listobjects_v2,
1053        }))
1054    }
1055
1056    #[cfg(feature = "with-tokio")]
1057    pub fn with_request_timeout(&self, request_timeout: Duration) -> Result<Box<Bucket>, S3Error> {
1058        Ok(Box::new(Bucket {
1059            name: self.name.clone(),
1060            region: self.region.clone(),
1061            credentials: self.credentials.clone(),
1062            #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1063            credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1064            extra_headers: self.extra_headers.clone(),
1065            extra_query: self.extra_query.clone(),
1066            request_timeout: Some(request_timeout),
1067            path_style: self.path_style,
1068            listobjects_v2: self.listobjects_v2,
1069            #[cfg(feature = "with-tokio")]
1070            http_client: self.http_client(),
1071            #[cfg(feature = "with-tokio")]
1072            client_options: self.client_options.clone(),
1073        }))
1074    }
1075
1076    pub fn with_listobjects_v1(&self) -> Bucket {
1077        Bucket {
1078            name: self.name.clone(),
1079            region: self.region.clone(),
1080            credentials: self.credentials.clone(),
1081            #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1082            credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1083            extra_headers: self.extra_headers.clone(),
1084            extra_query: self.extra_query.clone(),
1085            request_timeout: self.request_timeout,
1086            path_style: self.path_style,
1087            listobjects_v2: false,
1088            #[cfg(feature = "with-tokio")]
1089            http_client: self.http_client(),
1090            #[cfg(feature = "with-tokio")]
1091            client_options: self.client_options.clone(),
1092        }
1093    }
1094
1095    /// Configures a bucket to accept invalid SSL certificates and hostnames.
1096    ///
1097    /// This method is available only when one of the `tokio-native-tls`, `tokio-rustls-tls`, or `tokio-rustls-tls-ring` features is enabled.
1098    ///
1099    /// # Parameters
1100    ///
1101    /// - `accept_invalid_certs`: A boolean flag that determines whether the client should accept invalid SSL certificates.
1102    /// - `accept_invalid_hostnames`: A boolean flag that determines whether the client should accept invalid hostnames.
1103    ///
1104    /// # Returns
1105    ///
1106    /// Returns a `Result` containing the newly configured `Bucket` instance if successful, or an `S3Error` if an error occurs during client configuration.
1107    ///
1108    /// # Errors
1109    ///
1110    /// This function returns an `S3Error` if the HTTP client configuration fails.
1111    ///
1112    /// # Example
1113    ///
1114    /// ```rust
1115    /// # use s3::bucket::Bucket;
1116    /// # use s3::error::S3Error;
1117    /// # use s3::creds::Credentials;
1118    /// # use s3::Region;
1119    /// # use std::str::FromStr;
1120    ///
1121    /// # fn example() -> Result<(), S3Error> {
1122    /// let bucket = Bucket::new("my-bucket", Region::from_str("us-east-1")?, Credentials::default()?)?
1123    ///     .set_dangerous_config(true, true)?;
1124    /// # Ok(())
1125    /// # }
1126    /// ```
1127    #[cfg(any(
1128        feature = "tokio-native-tls",
1129        feature = "tokio-rustls-tls",
1130        feature = "tokio-rustls-tls-ring"
1131    ))]
1132    pub fn set_dangerous_config(
1133        &self,
1134        accept_invalid_certs: bool,
1135        accept_invalid_hostnames: bool,
1136    ) -> Result<Bucket, S3Error> {
1137        let mut options = self.client_options.clone();
1138        options.accept_invalid_certs = accept_invalid_certs;
1139        options.accept_invalid_hostnames = accept_invalid_hostnames;
1140
1141        Ok(Bucket {
1142            name: self.name.clone(),
1143            region: self.region.clone(),
1144            credentials: self.credentials.clone(),
1145            #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1146            credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1147            extra_headers: self.extra_headers.clone(),
1148            extra_query: self.extra_query.clone(),
1149            request_timeout: self.request_timeout,
1150            path_style: self.path_style,
1151            listobjects_v2: self.listobjects_v2,
1152            http_client: client(&options)?,
1153            client_options: options,
1154        })
1155    }
1156
1157    /// Deprecated alias for [`Bucket::set_dangerous_config`].
1158    #[deprecated(
1159        since = "0.37.3",
1160        note = "use `set_dangerous_config`; this misspelled method remains for compatibility"
1161    )]
1162    #[cfg(any(
1163        feature = "tokio-native-tls",
1164        feature = "tokio-rustls-tls",
1165        feature = "tokio-rustls-tls-ring"
1166    ))]
1167    pub fn set_dangereous_config(
1168        &self,
1169        accept_invalid_certs: bool,
1170        accept_invalid_hostnames: bool,
1171    ) -> Result<Bucket, S3Error> {
1172        self.set_dangerous_config(accept_invalid_certs, accept_invalid_hostnames)
1173    }
1174
1175    #[cfg(feature = "with-tokio")]
1176    pub fn set_proxy(&self, proxy: reqwest::Proxy) -> Result<Bucket, S3Error> {
1177        let mut options = self.client_options.clone();
1178        options.proxy = Some(proxy);
1179
1180        Ok(Bucket {
1181            name: self.name.clone(),
1182            region: self.region.clone(),
1183            credentials: self.credentials.clone(),
1184            #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1185            credentials_refresh_gate: self.credentials_refresh_gate.clone(),
1186            extra_headers: self.extra_headers.clone(),
1187            extra_query: self.extra_query.clone(),
1188            request_timeout: self.request_timeout,
1189            path_style: self.path_style,
1190            listobjects_v2: self.listobjects_v2,
1191            http_client: client(&options)?,
1192            client_options: options,
1193        })
1194    }
1195
1196    /// Copy file from an S3 path, internally within the same bucket.
1197    ///
1198    /// Returns an error if S3 returns an XML error document with HTTP status 200.
1199    ///
1200    /// # Example:
1201    ///
1202    /// ```rust,no_run
1203    /// use s3::bucket::Bucket;
1204    /// use s3::creds::Credentials;
1205    /// use anyhow::Result;
1206    ///
1207    /// # #[tokio::main]
1208    /// # async fn main() -> Result<()> {
1209    ///
1210    /// let bucket_name = "rust-s3-test";
1211    /// let region = "us-east-1".parse()?;
1212    /// let credentials = Credentials::default()?;
1213    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1214    ///
1215    /// // Async variant with `tokio` or `async-std` features
1216    /// #[cfg(not(feature = "sync"))]
1217    /// let code = bucket.copy_object_internal("/from.file", "/to.file").await?;
1218    ///
1219    /// // `sync` feature will produce an identical method
1220    /// #[cfg(feature = "sync")]
1221    /// let code = bucket.copy_object_internal("/from.file", "/to.file")?;
1222    ///
1223    /// # Ok(())
1224    /// # }
1225    /// ```
1226    #[maybe_async::maybe_async]
1227    pub async fn copy_object_internal<F: AsRef<str>, T: AsRef<str>>(
1228        &self,
1229        from: F,
1230        to: T,
1231    ) -> Result<u16, S3Error> {
1232        let fq_from = {
1233            let from = from.as_ref();
1234            let from = from.strip_prefix('/').unwrap_or(from);
1235            format!("{bucket}/{path}", bucket = self.name(), path = from)
1236        };
1237        self.copy_object(fq_from, to).await
1238    }
1239
1240    #[maybe_async::maybe_async]
1241    async fn copy_object<F: AsRef<str>, T: AsRef<str>>(
1242        &self,
1243        from: F,
1244        to: T,
1245    ) -> Result<u16, S3Error> {
1246        let command = Command::CopyObject {
1247            from: from.as_ref(),
1248        };
1249        let request = RequestImpl::new(self, to.as_ref(), command).await?;
1250        let response_data = request.response_data(false).await?;
1251        let response_data = validate_success_xml_response(response_data, "CopyObjectResult")?;
1252        Ok(response_data.status_code())
1253    }
1254
1255    /// Gets file from an S3 path.
1256    ///
1257    /// # Example:
1258    ///
1259    /// ```rust,no_run
1260    /// use s3::bucket::Bucket;
1261    /// use s3::creds::Credentials;
1262    /// use anyhow::Result;
1263    ///
1264    /// # #[tokio::main]
1265    /// # async fn main() -> Result<()> {
1266    ///
1267    /// let bucket_name = "rust-s3-test";
1268    /// let region = "us-east-1".parse()?;
1269    /// let credentials = Credentials::default()?;
1270    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1271    ///
1272    /// // Async variant with `tokio` or `async-std` features
1273    /// #[cfg(not(feature = "sync"))]
1274    /// let response_data = bucket.get_object("/test.file").await?;
1275    ///
1276    /// // `sync` feature will produce an identical method
1277    /// #[cfg(feature = "sync")]
1278    /// let response_data = bucket.get_object("/test.file")?;
1279    ///
1280    /// // Blocking variant, generated with `blocking` feature in combination
1281    /// // with `tokio` or `async-std` features.
1282    /// #[cfg(feature = "blocking")]
1283    /// let response_data = bucket.get_object_blocking("/test.file")?;
1284    /// # Ok(())
1285    /// # }
1286    /// ```
1287    #[maybe_async::maybe_async]
1288    pub async fn get_object<S: AsRef<str>>(&self, path: S) -> Result<ResponseData, S3Error> {
1289        let command = Command::GetObject;
1290        let request = RequestImpl::new(self, path.as_ref(), command).await?;
1291        request.response_data(false).await
1292    }
1293
1294    #[maybe_async::maybe_async]
1295    pub async fn get_object_attributes<S: AsRef<str>>(
1296        &self,
1297        path: S,
1298        expected_bucket_owner: &str,
1299        version_id: Option<String>,
1300    ) -> Result<GetObjectAttributesOutput, S3Error> {
1301        let command = Command::GetObjectAttributes {
1302            expected_bucket_owner: expected_bucket_owner.to_string(),
1303            version_id,
1304        };
1305        let request = RequestImpl::new(self, path.as_ref(), command).await?;
1306
1307        let response = request.response_data(false).await?;
1308
1309        Ok(quick_xml::de::from_str::<GetObjectAttributesOutput>(
1310            response.as_str()?,
1311        )?)
1312    }
1313
1314    /// Checks if an object exists at the specified S3 path.
1315    ///
1316    /// # Example:
1317    ///
1318    /// ```rust,no_run
1319    /// use s3::bucket::Bucket;
1320    /// use s3::creds::Credentials;
1321    /// use anyhow::Result;
1322    ///
1323    /// # #[tokio::main]
1324    /// # async fn main() -> Result<()> {
1325    ///
1326    /// let bucket_name = "rust-s3-test";
1327    /// let region = "us-east-1".parse()?;
1328    /// let credentials = Credentials::default()?;
1329    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1330    ///
1331    /// // Async variant with `tokio` or `async-std` features
1332    /// #[cfg(not(feature = "sync"))]
1333    /// let exists = bucket.object_exists("/test.file").await?;
1334    ///
1335    /// // `sync` feature will produce an identical method
1336    /// #[cfg(feature = "sync")]
1337    /// let exists = bucket.object_exists("/test.file")?;
1338    ///
1339    /// // Blocking variant, generated with `blocking` feature in combination
1340    /// // with `tokio` or `async-std` features.
1341    /// #[cfg(feature = "blocking")]
1342    /// let exists = bucket.object_exists_blocking("/test.file")?;
1343    ///
1344    /// if exists {
1345    ///     println!("Object exists.");
1346    /// } else {
1347    ///     println!("Object does not exist.");
1348    /// }
1349    /// # Ok(())
1350    /// # }
1351    /// ```
1352    ///
1353    /// # Errors
1354    ///
1355    /// This function will return an `Err` if the request to the S3 service fails or if there is an unexpected error.
1356    /// It will return `Ok(false)` if the object does not exist (i.e., the server returns a 404 status code).
1357    #[maybe_async::maybe_async]
1358    pub async fn object_exists<S: AsRef<str>>(&self, path: S) -> Result<bool, S3Error> {
1359        let command = Command::HeadObject;
1360        let request = RequestImpl::new(self, path.as_ref(), command).await?;
1361        let status_code = request.response_status().await?;
1362        Ok(status_code != 404)
1363    }
1364
1365    #[maybe_async::maybe_async]
1366    pub async fn put_bucket_cors(
1367        &self,
1368        expected_bucket_owner: &str,
1369        cors_config: &CorsConfiguration,
1370    ) -> Result<ResponseData, S3Error> {
1371        let command = Command::PutBucketCors {
1372            expected_bucket_owner: expected_bucket_owner.to_string(),
1373            configuration: cors_config.clone(),
1374        };
1375        let request = RequestImpl::new(self, "", command).await?;
1376        request.response_data(false).await
1377    }
1378
1379    #[maybe_async::maybe_async]
1380    pub async fn get_bucket_cors(
1381        &self,
1382        expected_bucket_owner: &str,
1383    ) -> Result<CorsConfiguration, S3Error> {
1384        let command = Command::GetBucketCors {
1385            expected_bucket_owner: expected_bucket_owner.to_string(),
1386        };
1387        let request = RequestImpl::new(self, "", command).await?;
1388        let response = request.response_data(false).await?;
1389        Ok(quick_xml::de::from_str::<CorsConfiguration>(
1390            response.as_str()?,
1391        )?)
1392    }
1393
1394    #[maybe_async::maybe_async]
1395    pub async fn delete_bucket_cors(
1396        &self,
1397        expected_bucket_owner: &str,
1398    ) -> Result<ResponseData, S3Error> {
1399        let command = Command::DeleteBucketCors {
1400            expected_bucket_owner: expected_bucket_owner.to_string(),
1401        };
1402        let request = RequestImpl::new(self, "", command).await?;
1403        request.response_data(false).await
1404    }
1405
1406    #[maybe_async::maybe_async]
1407    pub async fn get_bucket_lifecycle(&self) -> Result<BucketLifecycleConfiguration, S3Error> {
1408        let request = RequestImpl::new(self, "", Command::GetBucketLifecycle).await?;
1409        let response = request.response_data(false).await?;
1410        Ok(quick_xml::de::from_str::<BucketLifecycleConfiguration>(
1411            response.as_str()?,
1412        )?)
1413    }
1414
1415    #[maybe_async::maybe_async]
1416    pub async fn put_bucket_lifecycle(
1417        &self,
1418        lifecycle_config: BucketLifecycleConfiguration,
1419    ) -> Result<ResponseData, S3Error> {
1420        let command = Command::PutBucketLifecycle {
1421            configuration: lifecycle_config,
1422        };
1423        let request = RequestImpl::new(self, "", command).await?;
1424        request.response_data(false).await
1425    }
1426
1427    #[maybe_async::maybe_async]
1428    pub async fn delete_bucket_lifecycle(&self) -> Result<ResponseData, S3Error> {
1429        let request = RequestImpl::new(self, "", Command::DeleteBucketLifecycle).await?;
1430        request.response_data(false).await
1431    }
1432
1433    /// Retrieves the bucket policy response, including its JSON body and HTTP status.
1434    ///
1435    /// # Example:
1436    ///
1437    /// ```rust,no_run
1438    /// use s3::bucket::Bucket;
1439    /// use s3::creds::Credentials;
1440    /// use anyhow::Result;
1441    ///
1442    /// # #[cfg_attr(feature = "with-tokio", tokio::main)]
1443    /// # #[cfg_attr(all(not(feature = "with-tokio"), feature = "with-async-std"), async_std::main)]
1444    /// # #[cfg_attr(all(feature = "sync", not(feature = "with-tokio"), not(feature = "with-async-std")), tokio::main)]
1445    /// # async fn main() -> Result<()> {
1446    ///
1447    /// let bucket_name = "rust-s3-test";
1448    /// let region = "us-east-1".parse()?;
1449    /// let credentials = Credentials::default()?;
1450    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1451    ///
1452    /// // Async variant with `tokio` or `async-std` features
1453    /// #[cfg(not(feature = "sync"))]
1454    /// let response = bucket.get_bucket_policy().await?;
1455    ///
1456    /// // `sync` feature will produce an identical method
1457    /// #[cfg(feature = "sync")]
1458    /// let response = bucket.get_bucket_policy()?;
1459    ///
1460    /// // Blocking variant, generated with `blocking` feature in combination
1461    /// // with `tokio` or `async-std` features.
1462    /// #[cfg(all(feature = "blocking", not(feature = "sync")))]
1463    /// let response = bucket.get_bucket_policy_blocking()?;
1464    /// # Ok(())
1465    /// # }
1466    /// ```
1467    #[maybe_async::maybe_async]
1468    pub async fn get_bucket_policy(&self) -> Result<ResponseData, S3Error> {
1469        let request = RequestImpl::new(self, "", Command::GetBucketPolicy).await?;
1470        request.response_data(false).await
1471    }
1472
1473    /// Sets the bucket policy from a JSON string.
1474    ///
1475    /// # Example:
1476    ///
1477    /// ```rust,no_run
1478    /// use s3::bucket::Bucket;
1479    /// use s3::creds::Credentials;
1480    /// use anyhow::Result;
1481    ///
1482    /// # #[cfg_attr(feature = "with-tokio", tokio::main)]
1483    /// # #[cfg_attr(all(not(feature = "with-tokio"), feature = "with-async-std"), async_std::main)]
1484    /// # #[cfg_attr(all(feature = "sync", not(feature = "with-tokio"), not(feature = "with-async-std")), tokio::main)]
1485    /// # async fn main() -> Result<()> {
1486    ///
1487    /// let bucket_name = "rust-s3-test";
1488    /// let region = "us-east-1".parse()?;
1489    /// let credentials = Credentials::default()?;
1490    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1491    ///
1492    /// let policy = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::rust-s3-test/policy-probe"}]}"#;
1493    ///
1494    /// // Async variant with `tokio` or `async-std` features
1495    /// #[cfg(not(feature = "sync"))]
1496    /// let response = bucket.put_bucket_policy(policy).await?;
1497    ///
1498    /// // `sync` feature will produce an identical method
1499    /// #[cfg(feature = "sync")]
1500    /// let response = bucket.put_bucket_policy(policy)?;
1501    ///
1502    /// // Blocking variant, generated with `blocking` feature in combination
1503    /// // with `tokio` or `async-std` features.
1504    /// #[cfg(all(feature = "blocking", not(feature = "sync")))]
1505    /// let response = bucket.put_bucket_policy_blocking(policy)?;
1506    /// # Ok(())
1507    /// # }
1508    /// ```
1509    #[maybe_async::maybe_async]
1510    pub async fn put_bucket_policy(&self, policy: &str) -> Result<ResponseData, S3Error> {
1511        let command = Command::PutBucketPolicy {
1512            policy: policy.to_string(),
1513        };
1514        let request = RequestImpl::new(self, "", command).await?;
1515        request.response_data(false).await
1516    }
1517
1518    /// Deletes the bucket policy.
1519    ///
1520    /// # Example:
1521    ///
1522    /// ```rust,no_run
1523    /// use s3::bucket::Bucket;
1524    /// use s3::creds::Credentials;
1525    /// use anyhow::Result;
1526    ///
1527    /// # #[cfg_attr(feature = "with-tokio", tokio::main)]
1528    /// # #[cfg_attr(all(not(feature = "with-tokio"), feature = "with-async-std"), async_std::main)]
1529    /// # #[cfg_attr(all(feature = "sync", not(feature = "with-tokio"), not(feature = "with-async-std")), tokio::main)]
1530    /// # async fn main() -> Result<()> {
1531    ///
1532    /// let bucket_name = "rust-s3-test";
1533    /// let region = "us-east-1".parse()?;
1534    /// let credentials = Credentials::default()?;
1535    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1536    ///
1537    /// // Async variant with `tokio` or `async-std` features
1538    /// #[cfg(not(feature = "sync"))]
1539    /// let response = bucket.delete_bucket_policy().await?;
1540    ///
1541    /// // `sync` feature will produce an identical method
1542    /// #[cfg(feature = "sync")]
1543    /// let response = bucket.delete_bucket_policy()?;
1544    ///
1545    /// // Blocking variant, generated with `blocking` feature in combination
1546    /// // with `tokio` or `async-std` features.
1547    /// #[cfg(all(feature = "blocking", not(feature = "sync")))]
1548    /// let response = bucket.delete_bucket_policy_blocking()?;
1549    /// # Ok(())
1550    /// # }
1551    /// ```
1552    #[maybe_async::maybe_async]
1553    pub async fn delete_bucket_policy(&self) -> Result<ResponseData, S3Error> {
1554        let request = RequestImpl::new(self, "", Command::DeleteBucketPolicy).await?;
1555        request.response_data(false).await
1556    }
1557
1558    /// Gets torrent from an S3 path.
1559    ///
1560    /// # Example:
1561    ///
1562    /// ```rust,no_run
1563    /// use s3::bucket::Bucket;
1564    /// use s3::creds::Credentials;
1565    /// use anyhow::Result;
1566    ///
1567    /// # #[tokio::main]
1568    /// # async fn main() -> Result<()> {
1569    ///
1570    /// let bucket_name = "rust-s3-test";
1571    /// let region = "us-east-1".parse()?;
1572    /// let credentials = Credentials::default()?;
1573    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1574    ///
1575    /// // Async variant with `tokio` or `async-std` features
1576    /// #[cfg(not(feature = "sync"))]
1577    /// let response_data = bucket.get_object_torrent("/test.file").await?;
1578    ///
1579    /// // `sync` feature will produce an identical method
1580    /// #[cfg(feature = "sync")]
1581    /// let response_data = bucket.get_object_torrent("/test.file")?;
1582    ///
1583    /// // Blocking variant, generated with `blocking` feature in combination
1584    /// // with `tokio` or `async-std` features.
1585    /// #[cfg(feature = "blocking")]
1586    /// let response_data = bucket.get_object_torrent_blocking("/test.file")?;
1587    /// # Ok(())
1588    /// # }
1589    /// ```
1590    #[maybe_async::maybe_async]
1591    pub async fn get_object_torrent<S: AsRef<str>>(
1592        &self,
1593        path: S,
1594    ) -> Result<ResponseData, S3Error> {
1595        let command = Command::GetObjectTorrent;
1596        let request = RequestImpl::new(self, path.as_ref(), command).await?;
1597        request.response_data(false).await
1598    }
1599
1600    /// Gets specified inclusive byte range of file from an S3 path.
1601    ///
1602    /// # Example:
1603    ///
1604    /// ```rust,no_run
1605    /// use s3::bucket::Bucket;
1606    /// use s3::creds::Credentials;
1607    /// use anyhow::Result;
1608    ///
1609    /// # #[tokio::main]
1610    /// # async fn main() -> Result<()> {
1611    ///
1612    /// let bucket_name = "rust-s3-test";
1613    /// let region = "us-east-1".parse()?;
1614    /// let credentials = Credentials::default()?;
1615    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1616    ///
1617    /// // Async variant with `tokio` or `async-std` features
1618    /// #[cfg(not(feature = "sync"))]
1619    /// let response_data = bucket.get_object_range("/test.file", 0, Some(31)).await?;
1620    ///
1621    /// // `sync` feature will produce an identical method
1622    /// #[cfg(feature = "sync")]
1623    /// let response_data = bucket.get_object_range("/test.file", 0, Some(31))?;
1624    ///
1625    /// // Blocking variant, generated with `blocking` feature in combination
1626    /// // with `tokio` or `async-std` features.
1627    /// #[cfg(feature = "blocking")]
1628    /// let response_data = bucket.get_object_range_blocking("/test.file", 0, Some(31))?;
1629    /// #
1630    /// # Ok(())
1631    /// # }
1632    /// ```
1633    #[maybe_async::maybe_async]
1634    pub async fn get_object_range<S: AsRef<str>>(
1635        &self,
1636        path: S,
1637        start: u64,
1638        end: Option<u64>,
1639    ) -> Result<ResponseData, S3Error> {
1640        if let Some(end) = end {
1641            assert!(start <= end);
1642        }
1643
1644        let command = Command::GetObjectRange { start, end };
1645        let request = RequestImpl::new(self, path.as_ref(), command).await?;
1646        request.response_data(false).await
1647    }
1648
1649    /// Stream range of bytes from S3 path to a local file, generic over T: Write.
1650    ///
1651    /// # Example:
1652    ///
1653    /// ```rust,no_run
1654    /// use s3::bucket::Bucket;
1655    /// use s3::creds::Credentials;
1656    /// use anyhow::Result;
1657    /// use std::fs::File;
1658    ///
1659    /// # #[tokio::main]
1660    /// # async fn main() -> Result<()> {
1661    ///
1662    /// let bucket_name = "rust-s3-test";
1663    /// let region = "us-east-1".parse()?;
1664    /// let credentials = Credentials::default()?;
1665    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1666    /// let mut output_file = File::create("output_file").expect("Unable to create file");
1667    /// let mut async_output_file = tokio::fs::File::create("async_output_file").await.expect("Unable to create file");
1668    /// #[cfg(feature = "with-async-std")]
1669    /// let mut async_output_file = async_std::fs::File::create("async_output_file").await.expect("Unable to create file");
1670    ///
1671    /// let start = 0;
1672    /// let end = Some(1024);
1673    ///
1674    /// // Async variant with `tokio` or `async-std` features
1675    /// let status_code = bucket.get_object_range_to_writer("/test.file", start, end, &mut async_output_file).await?;
1676    ///
1677    /// // `sync` feature will produce an identical method
1678    /// #[cfg(feature = "sync")]
1679    /// let status_code = bucket.get_object_range_to_writer("/test.file", start, end, &mut output_file)?;
1680    ///
1681    /// // Blocking variant, generated with `blocking` feature in combination
1682    /// // with `tokio` or `async-std` features. Based of the async branch
1683    /// #[cfg(feature = "blocking")]
1684    /// let status_code = bucket.get_object_range_to_writer_blocking("/test.file", start, end, &mut async_output_file)?;
1685    /// #
1686    /// # Ok(())
1687    /// # }
1688    /// ```
1689    #[maybe_async::async_impl]
1690    pub async fn get_object_range_to_writer<T, S>(
1691        &self,
1692        path: S,
1693        start: u64,
1694        end: Option<u64>,
1695        writer: &mut T,
1696    ) -> Result<u16, S3Error>
1697    where
1698        T: AsyncWrite + Send + Unpin + ?Sized,
1699        S: AsRef<str>,
1700    {
1701        if let Some(end) = end {
1702            assert!(start <= end);
1703        }
1704
1705        let command = Command::GetObjectRange { start, end };
1706        let request = RequestImpl::new(self, path.as_ref(), command).await?;
1707        request.response_data_to_writer(writer).await
1708    }
1709
1710    #[maybe_async::sync_impl]
1711    pub fn get_object_range_to_writer<T: std::io::Write + Send + ?Sized, S: AsRef<str>>(
1712        &self,
1713        path: S,
1714        start: u64,
1715        end: Option<u64>,
1716        writer: &mut T,
1717    ) -> Result<u16, S3Error> {
1718        if let Some(end) = end {
1719            assert!(start <= end);
1720        }
1721
1722        let command = Command::GetObjectRange { start, end };
1723        let request = RequestImpl::new(self, path.as_ref(), command)?;
1724        request.response_data_to_writer(writer)
1725    }
1726
1727    /// Stream file from S3 path to a local file, generic over T: Write.
1728    ///
1729    /// # Example:
1730    ///
1731    /// ```rust,no_run
1732    /// use s3::bucket::Bucket;
1733    /// use s3::creds::Credentials;
1734    /// use anyhow::Result;
1735    /// use std::fs::File;
1736    ///
1737    /// # #[tokio::main]
1738    /// # async fn main() -> Result<()> {
1739    ///
1740    /// let bucket_name = "rust-s3-test";
1741    /// let region = "us-east-1".parse()?;
1742    /// let credentials = Credentials::default()?;
1743    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1744    /// let mut output_file = File::create("output_file").expect("Unable to create file");
1745    /// let mut async_output_file = tokio::fs::File::create("async_output_file").await.expect("Unable to create file");
1746    /// #[cfg(feature = "with-async-std")]
1747    /// let mut async_output_file = async_std::fs::File::create("async_output_file").await.expect("Unable to create file");
1748    ///
1749    /// // Async variant with `tokio` or `async-std` features
1750    /// let status_code = bucket.get_object_to_writer("/test.file", &mut async_output_file).await?;
1751    ///
1752    /// // `sync` feature will produce an identical method
1753    /// #[cfg(feature = "sync")]
1754    /// let status_code = bucket.get_object_to_writer("/test.file", &mut output_file)?;
1755    ///
1756    /// // Blocking variant, generated with `blocking` feature in combination
1757    /// // with `tokio` or `async-std` features. Based of the async branch
1758    /// #[cfg(feature = "blocking")]
1759    /// let status_code = bucket.get_object_to_writer_blocking("/test.file", &mut async_output_file)?;
1760    /// #
1761    /// # Ok(())
1762    /// # }
1763    /// ```
1764    #[maybe_async::async_impl]
1765    pub async fn get_object_to_writer<T: AsyncWrite + Send + Unpin + ?Sized, S: AsRef<str>>(
1766        &self,
1767        path: S,
1768        writer: &mut T,
1769    ) -> Result<u16, S3Error> {
1770        let command = Command::GetObject;
1771        let request = RequestImpl::new(self, path.as_ref(), command).await?;
1772        request.response_data_to_writer(writer).await
1773    }
1774
1775    #[maybe_async::sync_impl]
1776    pub fn get_object_to_writer<T: std::io::Write + Send + ?Sized, S: AsRef<str>>(
1777        &self,
1778        path: S,
1779        writer: &mut T,
1780    ) -> Result<u16, S3Error> {
1781        let command = Command::GetObject;
1782        let request = RequestImpl::new(self, path.as_ref(), command)?;
1783        request.response_data_to_writer(writer)
1784    }
1785
1786    /// Stream file from S3 path to a local file using an async stream.
1787    ///
1788    /// # Example
1789    ///
1790    /// ```rust,no_run
1791    /// use s3::bucket::Bucket;
1792    /// use s3::creds::Credentials;
1793    /// use anyhow::Result;
1794    /// #[cfg(feature = "with-tokio")]
1795    /// use tokio_stream::StreamExt;
1796    /// #[cfg(feature = "with-tokio")]
1797    /// use tokio::io::AsyncWriteExt;
1798    /// #[cfg(feature = "with-async-std")]
1799    /// use async_std::stream::StreamExt;
1800    /// #[cfg(feature = "with-async-std")]
1801    /// use async_std::io::WriteExt;
1802    ///
1803    /// # #[tokio::main]
1804    /// # async fn main() -> Result<()> {
1805    ///
1806    /// let bucket_name = "rust-s3-test";
1807    /// let region = "us-east-1".parse()?;
1808    /// let credentials = Credentials::default()?;
1809    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1810    /// let path = "path";
1811    ///
1812    /// let mut response_data_stream = bucket.get_object_stream(path).await?;
1813    ///
1814    /// #[cfg(feature = "with-tokio")]
1815    /// let mut async_output_file = tokio::fs::File::create("async_output_file").await.expect("Unable to create file");
1816    /// #[cfg(feature = "with-async-std")]
1817    /// let mut async_output_file = async_std::fs::File::create("async_output_file").await.expect("Unable to create file");
1818    ///
1819    /// while let Some(chunk) = response_data_stream.bytes().next().await {
1820    ///     async_output_file.write_all(&chunk.unwrap()).await?;
1821    /// }
1822    ///
1823    /// #
1824    /// # Ok(())
1825    /// # }
1826    /// ```
1827    #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1828    pub async fn get_object_stream<S: AsRef<str>>(
1829        &self,
1830        path: S,
1831    ) -> Result<ResponseDataStream, S3Error> {
1832        let command = Command::GetObject;
1833        let request = RequestImpl::new(self, path.as_ref(), command).await?;
1834        request.response_data_to_stream().await
1835    }
1836
1837    /// Stream file from local path to s3, generic over T: Write.
1838    ///
1839    /// # Example:
1840    ///
1841    /// ```rust,no_run
1842    /// use s3::bucket::Bucket;
1843    /// use s3::creds::Credentials;
1844    /// use anyhow::Result;
1845    /// use std::fs::File;
1846    /// use std::io::Write;
1847    ///
1848    /// # #[tokio::main]
1849    /// # async fn main() -> Result<()> {
1850    ///
1851    /// let bucket_name = "rust-s3-test";
1852    /// let region = "us-east-1".parse()?;
1853    /// let credentials = Credentials::default()?;
1854    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1855    /// let path = "path";
1856    /// let test: Vec<u8> = (0..1000).map(|_| 42).collect();
1857    /// let mut file = File::create(path)?;
1858    /// file.write_all(&test)?;
1859    ///
1860    /// #[cfg(feature = "with-tokio")]
1861    /// let mut async_reader = tokio::fs::File::open(path).await?;
1862    /// #[cfg(feature = "with-tokio")]
1863    /// let status_code = bucket.put_object_stream(&mut async_reader, "/path").await?;
1864    /// #[cfg(feature = "with-async-std")]
1865    /// let mut async_reader = async_std::fs::File::open(path).await?;
1866    /// #[cfg(feature = "with-async-std")]
1867    /// let status_code = bucket.put_object_stream(&mut async_reader, "/path").await?;
1868    ///
1869    /// // `sync` feature will produce an identical method
1870    /// #[cfg(feature = "sync")]
1871    /// let mut sync_reader = File::open(path)?;
1872    /// #[cfg(feature = "sync")]
1873    /// let status_code = bucket.put_object_stream(&mut sync_reader, "/path")?;
1874    ///
1875    /// // Blocking variant, generated with `blocking` feature in combination
1876    /// // with `tokio` or `async-std` features.
1877    /// #[cfg(feature = "blocking")]
1878    /// let status_code = bucket.put_object_stream_blocking(&mut async_reader, "/path")?;
1879    /// #
1880    /// # Ok(())
1881    /// # }
1882    /// ```
1883    #[maybe_async::async_impl]
1884    pub async fn put_object_stream<R: AsyncRead + Unpin + ?Sized>(
1885        &self,
1886        reader: &mut R,
1887        s3_path: impl AsRef<str>,
1888    ) -> Result<PutStreamResponse, S3Error> {
1889        self._put_object_stream_with_content_type(
1890            reader,
1891            s3_path.as_ref(),
1892            "application/octet-stream",
1893        )
1894        .await
1895    }
1896
1897    /// Create a builder for streaming PUT operations with custom options
1898    ///
1899    /// # Example:
1900    ///
1901    /// ```no_run
1902    /// use s3::bucket::Bucket;
1903    /// use s3::creds::Credentials;
1904    /// use anyhow::Result;
1905    ///
1906    /// # #[cfg(feature = "with-tokio")]
1907    /// # #[tokio::main]
1908    /// # async fn main() -> Result<()> {
1909    /// # use tokio::fs::File;
1910    ///
1911    /// let bucket = Bucket::new("my-bucket", "us-east-1".parse()?, Credentials::default()?)?;
1912    ///
1913    /// # #[cfg(feature = "with-tokio")]
1914    /// let mut file = File::open("large-file.zip").await?;
1915    ///
1916    /// // Stream upload with custom headers using builder pattern
1917    /// let response = bucket.put_object_stream_builder("/large-file.zip")
1918    ///     .with_content_type("application/zip")
1919    ///     .with_cache_control("public, max-age=3600")?
1920    ///     .with_metadata("uploaded-by", "stream-builder")?
1921    ///     .with_max_concurrent_chunks(2)
1922    ///     .execute_stream(&mut file)
1923    ///     .await?;
1924    /// #
1925    /// # Ok(())
1926    /// # }
1927    /// # #[cfg(not(feature = "with-tokio"))]
1928    /// # fn main() {}
1929    /// ```
1930    #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
1931    pub fn put_object_stream_builder<S: AsRef<str>>(
1932        &self,
1933        path: S,
1934    ) -> crate::put_object_request::PutObjectStreamRequest<'_> {
1935        crate::put_object_request::PutObjectStreamRequest::new(self, path)
1936    }
1937
1938    #[maybe_async::sync_impl]
1939    pub fn put_object_stream<R: Read>(
1940        &self,
1941        reader: &mut R,
1942        s3_path: impl AsRef<str>,
1943    ) -> Result<u16, S3Error> {
1944        self._put_object_stream_with_content_type(
1945            reader,
1946            s3_path.as_ref(),
1947            "application/octet-stream",
1948        )
1949    }
1950
1951    /// Stream file from local path to s3, generic over T: Write with explicit content type.
1952    ///
1953    /// # Example:
1954    ///
1955    /// ```rust,no_run
1956    /// use s3::bucket::Bucket;
1957    /// use s3::creds::Credentials;
1958    /// use anyhow::Result;
1959    /// use std::fs::File;
1960    /// use std::io::Write;
1961    ///
1962    /// # #[tokio::main]
1963    /// # async fn main() -> Result<()> {
1964    ///
1965    /// let bucket_name = "rust-s3-test";
1966    /// let region = "us-east-1".parse()?;
1967    /// let credentials = Credentials::default()?;
1968    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
1969    /// let path = "path";
1970    /// let test: Vec<u8> = (0..1000).map(|_| 42).collect();
1971    /// let mut file = File::create(path)?;
1972    /// file.write_all(&test)?;
1973    ///
1974    /// #[cfg(feature = "with-tokio")]
1975    /// let mut async_reader = tokio::fs::File::open(path).await?;
1976    ///
1977    /// #[cfg(feature = "with-async-std")]
1978    /// let mut async_reader = async_std::fs::File::open(path).await?;
1979    ///
1980    /// // Async variant with `tokio` or `async-std` features
1981    /// // Generic over std::io::Read
1982    /// let status_code = bucket
1983    ///     .put_object_stream_with_content_type(&mut async_reader, "/path", "application/octet-stream")
1984    ///     .await?;
1985    ///
1986    /// // `sync` feature will produce an identical method
1987    /// #[cfg(feature = "sync")]
1988    /// // Generic over std::io::Read
1989    /// let mut sync_reader = File::open(path)?;
1990    /// #[cfg(feature = "sync")]
1991    /// let status_code = bucket
1992    ///     .put_object_stream_with_content_type(&mut sync_reader, "/path", "application/octet-stream")?;
1993    ///
1994    /// // Blocking variant, generated with `blocking` feature in combination
1995    /// // with `tokio` or `async-std` features.
1996    /// #[cfg(feature = "blocking")]
1997    /// let status_code = bucket
1998    ///     .put_object_stream_with_content_type_blocking(&mut async_reader, "/path", "application/octet-stream")?;
1999    ///
2000    /// #
2001    /// # Ok(())
2002    /// # }
2003    /// ```
2004    /// After multipart initiation, returned errors trigger a best-effort abort.
2005    /// Cancelling the operation or losing the completion response can leave the
2006    /// remote upload outcome uncertain.
2007    #[maybe_async::async_impl]
2008    pub async fn put_object_stream_with_content_type<R: AsyncRead + Unpin>(
2009        &self,
2010        reader: &mut R,
2011        s3_path: impl AsRef<str>,
2012        content_type: impl AsRef<str>,
2013    ) -> Result<PutStreamResponse, S3Error> {
2014        self._put_object_stream_with_content_type(reader, s3_path.as_ref(), content_type.as_ref())
2015            .await
2016    }
2017
2018    #[maybe_async::sync_impl]
2019    pub fn put_object_stream_with_content_type<R: Read>(
2020        &self,
2021        reader: &mut R,
2022        s3_path: impl AsRef<str>,
2023        content_type: impl AsRef<str>,
2024    ) -> Result<u16, S3Error> {
2025        self._put_object_stream_with_content_type(reader, s3_path.as_ref(), content_type.as_ref())
2026    }
2027
2028    #[maybe_async::maybe_async]
2029    async fn make_multipart_request(
2030        &self,
2031        path: &str,
2032        chunk: Vec<u8>,
2033        part_number: u32,
2034        upload_id: &str,
2035        content_type: &str,
2036        custom_headers: &HeaderMap,
2037    ) -> Result<ResponseData, S3Error> {
2038        let command = Command::PutObject {
2039            content: &chunk,
2040            multipart: Some(Multipart::new(part_number, upload_id)), // upload_id: &msg.upload_id,
2041            custom_headers: Some(custom_headers.clone()),
2042            content_type,
2043        };
2044        let request = RequestImpl::new(self, path, command).await?;
2045        request.response_data(true).await
2046    }
2047
2048    #[maybe_async::maybe_async]
2049    async fn abort_upload_after_failure<T>(
2050        &self,
2051        path: &str,
2052        upload_id: &str,
2053        abort_headers: &HeaderMap,
2054        error: S3Error,
2055    ) -> Result<T, S3Error> {
2056        // Cleanup is best effort. Keep the operation's original error even if
2057        // the abort fails (for example, if completion already reached S3).
2058        let bucket = self.with_overlaid_extra_headers(abort_headers);
2059        let _ = bucket.abort_upload(path, upload_id).await;
2060        Err(error)
2061    }
2062
2063    fn with_overlaid_extra_headers(&self, overlay: &HeaderMap) -> Bucket {
2064        let mut bucket = self.clone();
2065        bucket.extra_headers.extend(overlay.clone());
2066        bucket
2067    }
2068
2069    #[maybe_async::async_impl]
2070    async fn _put_object_stream_with_content_type<R: AsyncRead + Unpin + ?Sized>(
2071        &self,
2072        reader: &mut R,
2073        s3_path: &str,
2074        content_type: &str,
2075    ) -> Result<PutStreamResponse, S3Error> {
2076        self._put_object_stream_with_content_type_and_headers(
2077            reader,
2078            s3_path,
2079            content_type,
2080            None,
2081            None,
2082        )
2083        .await
2084    }
2085
2086    /// Calculate the maximum number of concurrent chunks based on available memory.
2087    /// Returns a per-upload count between 2 and 10, defaulting to 3 if detection fails.
2088    /// This sizing input is not a process-wide memory or RSS limit.
2089    #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
2090    fn max_concurrent_chunks_for_memory(available_memory: u64) -> usize {
2091        const DEFAULT_CONCURRENT_CHUNKS: usize = 3;
2092        const MAX_CONCURRENT_CHUNKS: usize = 10;
2093
2094        if available_memory == 0 {
2095            return DEFAULT_CONCURRENT_CHUNKS;
2096        }
2097
2098        let memory_per_chunk = CHUNK_SIZE as u64 * 3;
2099        (available_memory / memory_per_chunk).clamp(2, MAX_CONCURRENT_CHUNKS as u64) as usize
2100    }
2101
2102    #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
2103    fn calculate_max_concurrent_chunks() -> usize {
2104        // Create a new System instance and refresh memory info
2105        let mut system = System::new();
2106        system.refresh_memory_specifics(MemoryRefreshKind::everything());
2107
2108        // Get available memory in bytes
2109        let available_memory = system.available_memory();
2110
2111        Self::max_concurrent_chunks_for_memory(available_memory)
2112    }
2113
2114    #[maybe_async::async_impl]
2115    pub(crate) async fn _put_object_stream_with_content_type_and_headers<
2116        R: AsyncRead + Unpin + ?Sized,
2117    >(
2118        &self,
2119        reader: &mut R,
2120        s3_path: &str,
2121        content_type: &str,
2122        custom_headers: Option<http::HeaderMap>,
2123        max_concurrent_chunks: Option<std::num::NonZeroUsize>,
2124    ) -> Result<PutStreamResponse, S3Error> {
2125        // If the file is smaller CHUNK_SIZE, just do a regular upload.
2126        // Otherwise perform a multi-part upload.
2127        let first_chunk = crate::utils::read_chunk_async(reader).await?;
2128        // println!("First chunk size: {}", first_chunk.len());
2129        if first_chunk.len() < CHUNK_SIZE {
2130            let total_size = first_chunk.len();
2131            // Use the builder pattern for small files
2132            let mut builder = self
2133                .put_object_builder(s3_path, first_chunk.as_slice())
2134                .with_content_type(content_type);
2135
2136            // Add custom headers if provided
2137            if let Some(headers) = custom_headers {
2138                builder = builder.with_headers(headers);
2139            }
2140
2141            let response_data = builder.execute().await?;
2142            if response_data.status_code() >= 300 {
2143                return Err(error_from_response_data(response_data)?);
2144            }
2145            return Ok(PutStreamResponse::new(
2146                response_data.status_code(),
2147                total_size,
2148            ));
2149        }
2150
2151        let header_plan = MultipartHeaderPlan::from_headers(custom_headers.as_ref())?;
2152        let initiate_bucket = self.with_overlaid_extra_headers(&header_plan.initiate);
2153        let msg = initiate_bucket
2154            .initiate_multipart_upload(s3_path, content_type)
2155            .await?;
2156        let path = msg.key;
2157        let upload_id = &msg.upload_id;
2158
2159        // Determine max concurrent chunks based on available memory
2160        let max_concurrent_chunks = max_concurrent_chunks.map_or_else(
2161            Self::calculate_max_concurrent_chunks,
2162            std::num::NonZeroUsize::get,
2163        );
2164
2165        // Use FuturesUnordered for bounded parallelism
2166        use futures_util::FutureExt;
2167        use futures_util::stream::{FuturesUnordered, StreamExt};
2168
2169        // Keep all part futures inside this scope. If reading or a part fails,
2170        // leaving the scope drops the outstanding futures before abort starts.
2171        let upload_result: Result<(Vec<(u32, String)>, usize), S3Error> = async {
2172            let mut part_number: u32 = 0;
2173            let mut total_size = 0;
2174            let mut etags = Vec::new();
2175            let mut active_uploads: FuturesUnordered<
2176                futures_util::future::BoxFuture<'_, (u32, Result<ResponseData, S3Error>)>,
2177            > = FuturesUnordered::new();
2178            let mut reading_done = false;
2179
2180            part_number += 1;
2181            total_size += first_chunk.len();
2182            if first_chunk.len() < CHUNK_SIZE {
2183                reading_done = true;
2184            }
2185
2186            let path_clone = path.clone();
2187            let upload_id_clone = upload_id.clone();
2188            let content_type_clone = content_type.to_string();
2189            let part_headers = header_plan.part.clone();
2190            let bucket_clone = self.clone();
2191
2192            active_uploads.push(
2193                async move {
2194                    let result = bucket_clone
2195                        .make_multipart_request(
2196                            &path_clone,
2197                            first_chunk,
2198                            1,
2199                            &upload_id_clone,
2200                            &content_type_clone,
2201                            &part_headers,
2202                        )
2203                        .await;
2204                    (1, result)
2205                }
2206                .boxed(),
2207            );
2208
2209            while !active_uploads.is_empty() || !reading_done {
2210                while active_uploads.len() < max_concurrent_chunks && !reading_done {
2211                    let chunk = crate::utils::read_chunk_async(reader).await?;
2212                    let chunk_len = chunk.len();
2213
2214                    if chunk_len == 0 {
2215                        reading_done = true;
2216                        break;
2217                    }
2218
2219                    total_size += chunk_len;
2220                    part_number += 1;
2221
2222                    if chunk_len < CHUNK_SIZE {
2223                        reading_done = true;
2224                    }
2225
2226                    let current_part = part_number;
2227                    let path_clone = path.clone();
2228                    let upload_id_clone = upload_id.clone();
2229                    let content_type_clone = content_type.to_string();
2230                    let part_headers = header_plan.part.clone();
2231                    let bucket_clone = self.clone();
2232
2233                    active_uploads.push(
2234                        async move {
2235                            let result = bucket_clone
2236                                .make_multipart_request(
2237                                    &path_clone,
2238                                    chunk,
2239                                    current_part,
2240                                    &upload_id_clone,
2241                                    &content_type_clone,
2242                                    &part_headers,
2243                                )
2244                                .await;
2245                            (current_part, result)
2246                        }
2247                        .boxed(),
2248                    );
2249                }
2250
2251                if let Some((part_num, result)) = active_uploads.next().await {
2252                    let response_data = result?;
2253                    if !(200..300).contains(&response_data.status_code()) {
2254                        return Err(response_error_from_data(response_data));
2255                    }
2256
2257                    let etag = response_data.as_str()?;
2258                    etags.push((part_num, etag.to_string()));
2259                }
2260            }
2261
2262            Ok((etags, total_size))
2263        }
2264        .await;
2265
2266        let (mut etags, total_size) = match upload_result {
2267            Ok(result) => result,
2268            Err(error) => {
2269                return self
2270                    .abort_upload_after_failure(&path, upload_id, &header_plan.abort, error)
2271                    .await;
2272            }
2273        };
2274
2275        // Sort etags by part number to ensure correct order
2276        etags.sort_by_key(|k| k.0);
2277        let etags: Vec<String> = etags.into_iter().map(|(_, etag)| etag).collect();
2278
2279        // Finish the upload
2280        let inner_data = etags
2281            .into_iter()
2282            .enumerate()
2283            .map(|(i, x)| Part {
2284                etag: x,
2285                part_number: i as u32 + 1,
2286            })
2287            .collect::<Vec<Part>>();
2288        let complete_bucket = self.with_overlaid_extra_headers(&header_plan.complete);
2289        let response_data = match complete_bucket
2290            .complete_multipart_upload(&path, &msg.upload_id, inner_data)
2291            .await
2292        {
2293            Ok(response_data) if (200..300).contains(&response_data.status_code()) => response_data,
2294            Ok(response_data) => {
2295                let error = response_error_from_data(response_data);
2296                return self
2297                    .abort_upload_after_failure(&path, upload_id, &header_plan.abort, error)
2298                    .await;
2299            }
2300            Err(error) => {
2301                return self
2302                    .abort_upload_after_failure(&path, upload_id, &header_plan.abort, error)
2303                    .await;
2304            }
2305        };
2306
2307        Ok(PutStreamResponse::new(
2308            response_data.status_code(),
2309            total_size,
2310        ))
2311    }
2312
2313    #[maybe_async::sync_impl]
2314    fn _put_object_stream_with_content_type<R: Read + ?Sized>(
2315        &self,
2316        reader: &mut R,
2317        s3_path: &str,
2318        content_type: &str,
2319    ) -> Result<u16, S3Error> {
2320        let msg = self.initiate_multipart_upload(s3_path, content_type)?;
2321        let path = msg.key;
2322        let upload_id = &msg.upload_id;
2323
2324        let mut needs_abort = true;
2325        let upload_result = (|| {
2326            let mut part_number: u32 = 0;
2327            let mut etags = Vec::new();
2328            loop {
2329                let chunk = crate::utils::read_chunk(reader)?;
2330
2331                if chunk.len() < CHUNK_SIZE {
2332                    if part_number == 0 {
2333                        // Files is not big enough for multipart upload, going with regular put_object.
2334                        // The upload is already aborted, so a fallback PUT error must not abort again.
2335                        needs_abort = false;
2336                        self.abort_upload(&path, upload_id)?;
2337                        return Ok(self.put_object(s3_path, chunk.as_slice())?.status_code());
2338                    }
2339
2340                    part_number += 1;
2341                    let response_data = self.make_multipart_request(
2342                        &path,
2343                        chunk,
2344                        part_number,
2345                        upload_id,
2346                        content_type,
2347                        &HeaderMap::new(),
2348                    )?;
2349                    let part = part_from_response(response_data, part_number)?;
2350                    etags.push(part.etag);
2351                    let inner_data = etags
2352                        .into_iter()
2353                        .enumerate()
2354                        .map(|(i, x)| Part {
2355                            etag: x,
2356                            part_number: i as u32 + 1,
2357                        })
2358                        .collect::<Vec<Part>>();
2359                    let response_data =
2360                        self.complete_multipart_upload(&path, upload_id, inner_data)?;
2361                    if !(200..300).contains(&response_data.status_code()) {
2362                        return Err(response_error_from_data(response_data));
2363                    }
2364                    return Ok(response_data.status_code());
2365                }
2366
2367                part_number += 1;
2368                let response_data = self.make_multipart_request(
2369                    &path,
2370                    chunk,
2371                    part_number,
2372                    upload_id,
2373                    content_type,
2374                    &HeaderMap::new(),
2375                )?;
2376                let part = part_from_response(response_data, part_number)?;
2377                etags.push(part.etag);
2378            }
2379        })();
2380
2381        match upload_result {
2382            Ok(status_code) => Ok(status_code),
2383            Err(error) if needs_abort => {
2384                self.abort_upload_after_failure(&path, upload_id, &HeaderMap::new(), error)
2385            }
2386            Err(error) => Err(error),
2387        }
2388    }
2389
2390    /// Initiate multipart upload to s3.
2391    #[maybe_async::async_impl]
2392    pub async fn initiate_multipart_upload(
2393        &self,
2394        s3_path: &str,
2395        content_type: &str,
2396    ) -> Result<InitiateMultipartUploadResponse, S3Error> {
2397        let command = Command::InitiateMultipartUpload { content_type };
2398        let request = RequestImpl::new(self, s3_path, command).await?;
2399        let response_data = request.response_data(false).await?;
2400        if response_data.status_code() >= 300 {
2401            return Err(error_from_response_data(response_data)?);
2402        }
2403
2404        let msg: InitiateMultipartUploadResponse =
2405            quick_xml::de::from_str(response_data.as_str()?)?;
2406        Ok(msg)
2407    }
2408
2409    #[maybe_async::sync_impl]
2410    pub fn initiate_multipart_upload(
2411        &self,
2412        s3_path: &str,
2413        content_type: &str,
2414    ) -> Result<InitiateMultipartUploadResponse, S3Error> {
2415        let command = Command::InitiateMultipartUpload { content_type };
2416        let request = RequestImpl::new(self, s3_path, command)?;
2417        let response_data = request.response_data(false)?;
2418        if response_data.status_code() >= 300 {
2419            return Err(error_from_response_data(response_data)?);
2420        }
2421
2422        let msg: InitiateMultipartUploadResponse =
2423            quick_xml::de::from_str(response_data.as_str()?)?;
2424        Ok(msg)
2425    }
2426
2427    /// Upload a streamed multipart chunk to s3 using a previously initiated multipart upload
2428    #[maybe_async::async_impl]
2429    pub async fn put_multipart_stream<R: Read + Unpin>(
2430        &self,
2431        reader: &mut R,
2432        path: &str,
2433        part_number: u32,
2434        upload_id: &str,
2435        content_type: &str,
2436    ) -> Result<Part, S3Error> {
2437        let chunk = crate::utils::read_chunk(reader)?;
2438        self.put_multipart_chunk(chunk, path, part_number, upload_id, content_type)
2439            .await
2440    }
2441
2442    #[maybe_async::sync_impl]
2443    pub async fn put_multipart_stream<R: Read + Unpin>(
2444        &self,
2445        reader: &mut R,
2446        path: &str,
2447        part_number: u32,
2448        upload_id: &str,
2449        content_type: &str,
2450    ) -> Result<Part, S3Error> {
2451        let chunk = crate::utils::read_chunk(reader)?;
2452        self.put_multipart_chunk(&chunk, path, part_number, upload_id, content_type)
2453    }
2454
2455    /// Upload a buffered multipart chunk to s3 using a previously initiated multipart upload
2456    #[maybe_async::async_impl]
2457    pub async fn put_multipart_chunk(
2458        &self,
2459        chunk: Vec<u8>,
2460        path: &str,
2461        part_number: u32,
2462        upload_id: &str,
2463        content_type: &str,
2464    ) -> Result<Part, S3Error> {
2465        let command = Command::PutObject {
2466            // part_number,
2467            content: &chunk,
2468            multipart: Some(Multipart::new(part_number, upload_id)), // upload_id: &msg.upload_id,
2469            custom_headers: None,
2470            content_type,
2471        };
2472        let request = RequestImpl::new(self, path, command).await?;
2473        let response_data = request.response_data(true).await?;
2474        if !(200..300).contains(&response_data.status_code()) {
2475            // if chunk upload failed - abort the upload
2476            match self.abort_upload(path, upload_id).await {
2477                Ok(_) => {
2478                    return Err(error_from_response_data(response_data)?);
2479                }
2480                Err(error) => {
2481                    return Err(error);
2482                }
2483            }
2484        }
2485        let etag = response_data.as_str()?;
2486        Ok(Part {
2487            etag: etag.to_string(),
2488            part_number,
2489        })
2490    }
2491
2492    #[maybe_async::sync_impl]
2493    pub fn put_multipart_chunk(
2494        &self,
2495        chunk: &[u8],
2496        path: &str,
2497        part_number: u32,
2498        upload_id: &str,
2499        content_type: &str,
2500    ) -> Result<Part, S3Error> {
2501        let command = Command::PutObject {
2502            // part_number,
2503            content: chunk,
2504            multipart: Some(Multipart::new(part_number, upload_id)), // upload_id: &msg.upload_id,
2505            custom_headers: None,
2506            content_type,
2507        };
2508        let request = RequestImpl::new(self, path, command)?;
2509        let response_data = request.response_data(true)?;
2510        if !(200..300).contains(&response_data.status_code()) {
2511            // if chunk upload failed - abort the upload
2512            match self.abort_upload(path, upload_id) {
2513                Ok(_) => {
2514                    return Err(error_from_response_data(response_data)?);
2515                }
2516                Err(error) => {
2517                    return Err(error);
2518                }
2519            }
2520        }
2521        let etag = response_data.as_str()?;
2522        Ok(Part {
2523            etag: etag.to_string(),
2524            part_number,
2525        })
2526    }
2527
2528    /// Completes a previously initiated multipart upload, with optional final data chunks.
2529    ///
2530    /// Returns an error if S3 returns an XML error document with HTTP status 200.
2531    #[maybe_async::async_impl]
2532    pub async fn complete_multipart_upload(
2533        &self,
2534        path: &str,
2535        upload_id: &str,
2536        parts: Vec<Part>,
2537    ) -> Result<ResponseData, S3Error> {
2538        let data = CompleteMultipartUploadData { parts };
2539        let complete = Command::CompleteMultipartUpload { upload_id, data };
2540        let complete_request = RequestImpl::new(self, path, complete).await?;
2541        let response_data = complete_request.response_data(false).await?;
2542        validate_success_xml_response(response_data, "CompleteMultipartUploadResult")
2543    }
2544
2545    #[maybe_async::sync_impl]
2546    pub fn complete_multipart_upload(
2547        &self,
2548        path: &str,
2549        upload_id: &str,
2550        parts: Vec<Part>,
2551    ) -> Result<ResponseData, S3Error> {
2552        let data = CompleteMultipartUploadData { parts };
2553        let complete = Command::CompleteMultipartUpload { upload_id, data };
2554        let complete_request = RequestImpl::new(self, path, complete)?;
2555        let response_data = complete_request.response_data(false)?;
2556        validate_success_xml_response(response_data, "CompleteMultipartUploadResult")
2557    }
2558
2559    /// Get Bucket location.
2560    ///
2561    /// # Example:
2562    ///
2563    /// ```no_run
2564    /// use s3::bucket::Bucket;
2565    /// use s3::creds::Credentials;
2566    /// use anyhow::Result;
2567    ///
2568    /// # #[tokio::main]
2569    /// # async fn main() -> Result<()> {
2570    ///
2571    /// let bucket_name = "rust-s3-test";
2572    /// let region = "us-east-1".parse()?;
2573    /// let credentials = Credentials::default()?;
2574    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
2575    ///
2576    /// // Async variant with `tokio` or `async-std` features
2577    /// #[cfg(not(feature = "sync"))]
2578    /// let (region, status_code) = bucket.location().await?;
2579    ///
2580    /// // `sync` feature will produce an identical method
2581    /// #[cfg(feature = "sync")]
2582    /// let (region, status_code) = bucket.location()?;
2583    ///
2584    /// // Blocking variant, generated with `blocking` feature in combination
2585    /// // with `tokio` or `async-std` features.
2586    /// #[cfg(feature = "blocking")]
2587    /// let (region, status_code) = bucket.location_blocking()?;
2588    /// #
2589    /// # Ok(())
2590    /// # }
2591    /// ```
2592    #[maybe_async::maybe_async]
2593    pub async fn location(&self) -> Result<(Region, u16), S3Error> {
2594        let request = RequestImpl::new(self, "?location", Command::GetBucketLocation).await?;
2595        let response_data = request.response_data(false).await?;
2596        let region_string = String::from_utf8_lossy(response_data.as_slice());
2597        let region = match quick_xml::de::from_reader(region_string.as_bytes()) {
2598            Ok(r) => {
2599                let location_result: BucketLocationResult = r;
2600                location_result.region.parse()?
2601            }
2602            Err(e) => {
2603                if response_data.status_code() == 200 {
2604                    Region::Custom {
2605                        region: "Custom".to_string(),
2606                        endpoint: "".to_string(),
2607                    }
2608                } else {
2609                    Region::Custom {
2610                        region: format!("Error encountered : {}", e),
2611                        endpoint: "".to_string(),
2612                    }
2613                }
2614            }
2615        };
2616        Ok((region, response_data.status_code()))
2617    }
2618
2619    /// Delete file from an S3 path.
2620    ///
2621    /// # Example:
2622    ///
2623    /// ```no_run
2624    /// use s3::bucket::Bucket;
2625    /// use s3::creds::Credentials;
2626    /// use anyhow::Result;
2627    ///
2628    /// # #[tokio::main]
2629    /// # async fn main() -> Result<()> {
2630    ///
2631    /// let bucket_name = "rust-s3-test";
2632    /// let region = "us-east-1".parse()?;
2633    /// let credentials = Credentials::default()?;
2634    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
2635    ///
2636    /// // Async variant with `tokio` or `async-std` features
2637    /// #[cfg(not(feature = "sync"))]
2638    /// let response_data = bucket.delete_object("/test.file").await?;
2639    ///
2640    /// // `sync` feature will produce an identical method
2641    /// #[cfg(feature = "sync")]
2642    /// let response_data = bucket.delete_object("/test.file")?;
2643    ///
2644    /// // Blocking variant, generated with `blocking` feature in combination
2645    /// // with `tokio` or `async-std` features.
2646    /// #[cfg(feature = "blocking")]
2647    /// let response_data = bucket.delete_object_blocking("/test.file")?;
2648    /// #
2649    /// # Ok(())
2650    /// # }
2651    /// ```
2652    #[maybe_async::maybe_async]
2653    pub async fn delete_object<S: AsRef<str>>(&self, path: S) -> Result<ResponseData, S3Error> {
2654        let command = Command::DeleteObject;
2655        let request = RequestImpl::new(self, path.as_ref(), command).await?;
2656        request.response_data(false).await
2657    }
2658
2659    /// Delete multiple objects from S3 using the Multi-Object Delete API.
2660    ///
2661    /// If more than 1000 objects are provided, they are automatically batched
2662    /// into multiple requests (S3 allows at most 1000 keys per request).
2663    /// Results from all batches are combined into a single response.
2664    ///
2665    /// # Example:
2666    ///
2667    /// ```no_run
2668    /// use s3::bucket::Bucket;
2669    /// use s3::creds::Credentials;
2670    /// use s3::serde_types::ObjectIdentifier;
2671    /// use anyhow::Result;
2672    ///
2673    /// # #[tokio::main]
2674    /// # async fn main() -> Result<()> {
2675    ///
2676    /// let bucket_name = "rust-s3-test";
2677    /// let region = "us-east-1".parse()?;
2678    /// let credentials = Credentials::default()?;
2679    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
2680    ///
2681    /// let objects = vec![
2682    ///     ObjectIdentifier::new("file1.txt"),
2683    ///     ObjectIdentifier::new("file2.txt"),
2684    ///     ObjectIdentifier::new("file3.txt"),
2685    /// ];
2686    ///
2687    /// // Async variant with `tokio` or `async-std` features
2688    /// #[cfg(not(feature = "sync"))]
2689    /// let response = bucket.delete_objects(objects.clone()).await?;
2690    ///
2691    /// // `sync` feature will produce an identical method
2692    /// #[cfg(feature = "sync")]
2693    /// let response = bucket.delete_objects(objects)?;
2694    ///
2695    /// // Blocking variant, generated with `blocking` feature in combination
2696    /// // with `tokio` or `async-std` features.
2697    /// #[cfg(feature = "blocking")]
2698    /// let response = bucket.delete_objects_blocking(objects)?;
2699    /// #
2700    /// # Ok(())
2701    /// # }
2702    /// ```
2703    #[maybe_async::maybe_async]
2704    pub async fn delete_objects<I: Into<Vec<ObjectIdentifier>>>(
2705        &self,
2706        objects: I,
2707    ) -> Result<DeleteObjectsResult, S3Error> {
2708        let objects = objects.into();
2709        let mut result = DeleteObjectsResult {
2710            deleted: Vec::new(),
2711            errors: Vec::new(),
2712        };
2713
2714        // Strip leading '/' from keys to match library convention.
2715        // Other methods (put_object, delete_object, etc.) strip the leading
2716        // slash when building the URL; we do the same for the XML body.
2717        let objects: Vec<ObjectIdentifier> = objects
2718            .into_iter()
2719            .map(|mut obj| {
2720                if let Some(stripped) = obj.key.strip_prefix('/') {
2721                    obj.key = stripped.to_string();
2722                }
2723                obj
2724            })
2725            .collect();
2726
2727        for chunk in objects.chunks(1000) {
2728            let data = DeleteObjectsRequest {
2729                objects: chunk.to_vec(),
2730                quiet: false,
2731            };
2732            let command = Command::DeleteObjects { data };
2733            let request = RequestImpl::new(self, "/", command).await?;
2734            let response_data = request.response_data(false).await?;
2735            if response_data.status_code() >= 300 {
2736                return Err(error_from_response_data(response_data)?);
2737            }
2738            let msg: DeleteObjectsResult = quick_xml::de::from_str(response_data.as_str()?)?;
2739            result.deleted.extend(msg.deleted);
2740            result.errors.extend(msg.errors);
2741        }
2742
2743        Ok(result)
2744    }
2745
2746    /// Head object from S3.
2747    ///
2748    /// # Example:
2749    ///
2750    /// ```no_run
2751    /// use s3::bucket::Bucket;
2752    /// use s3::creds::Credentials;
2753    /// use anyhow::Result;
2754    ///
2755    /// # #[tokio::main]
2756    /// # async fn main() -> Result<()> {
2757    ///
2758    /// let bucket_name = "rust-s3-test";
2759    /// let region = "us-east-1".parse()?;
2760    /// let credentials = Credentials::default()?;
2761    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
2762    ///
2763    /// // Async variant with `tokio` or `async-std` features
2764    /// #[cfg(not(feature = "sync"))]
2765    /// let (head_object_result, code) = bucket.head_object("/test.png").await?;
2766    ///
2767    /// // `sync` feature will produce an identical method
2768    /// #[cfg(feature = "sync")]
2769    /// let (head_object_result, code) = bucket.head_object("/test.png")?;
2770    ///
2771    /// // Blocking variant, generated with `blocking` feature in combination
2772    /// // with `tokio` or `async-std` features.
2773    /// #[cfg(feature = "blocking")]
2774    /// let (head_object_result, code) = bucket.head_object_blocking("/test.png")?;
2775    /// #
2776    /// # Ok(())
2777    /// # }
2778    /// ```
2779    #[maybe_async::maybe_async]
2780    pub async fn head_object<S: AsRef<str>>(
2781        &self,
2782        path: S,
2783    ) -> Result<(HeadObjectResult, u16), S3Error> {
2784        let command = Command::HeadObject;
2785        let request = RequestImpl::new(self, path.as_ref(), command).await?;
2786        let (headers, status) = request.response_header().await?;
2787        let header_object = HeadObjectResult::from(&headers);
2788        Ok((header_object, status))
2789    }
2790
2791    /// Put into an S3 bucket, with explicit content-type.
2792    ///
2793    /// # Example:
2794    ///
2795    /// ```no_run
2796    /// use s3::bucket::Bucket;
2797    /// use s3::creds::Credentials;
2798    /// use anyhow::Result;
2799    ///
2800    /// # #[tokio::main]
2801    /// # async fn main() -> Result<()> {
2802    ///
2803    /// let bucket_name = "rust-s3-test";
2804    /// let region = "us-east-1".parse()?;
2805    /// let credentials = Credentials::default()?;
2806    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
2807    /// let content = "I want to go to S3".as_bytes();
2808    ///
2809    /// // Async variant with `tokio` or `async-std` features
2810    /// #[cfg(not(feature = "sync"))]
2811    /// let response_data = bucket.put_object_with_content_type("/test.file", content, "text/plain").await?;
2812    ///
2813    /// // `sync` feature will produce an identical method
2814    /// #[cfg(feature = "sync")]
2815    /// let response_data = bucket.put_object_with_content_type("/test.file", content, "text/plain")?;
2816    ///
2817    /// // Blocking variant, generated with `blocking` feature in combination
2818    /// // with `tokio` or `async-std` features.
2819    /// #[cfg(feature = "blocking")]
2820    /// let response_data = bucket.put_object_with_content_type_blocking("/test.file", content, "text/plain")?;
2821    /// #
2822    /// # Ok(())
2823    /// # }
2824    /// ```
2825    #[maybe_async::maybe_async]
2826    pub async fn put_object_with_content_type<S: AsRef<str>>(
2827        &self,
2828        path: S,
2829        content: &[u8],
2830        content_type: &str,
2831    ) -> Result<ResponseData, S3Error> {
2832        let command = Command::PutObject {
2833            content,
2834            content_type,
2835            custom_headers: None,
2836            multipart: None,
2837        };
2838        let request = RequestImpl::new(self, path.as_ref(), command).await?;
2839        request.response_data(true).await
2840    }
2841
2842    /// Put into an S3 bucket, with explicit content-type and custom headers for the request.
2843    ///
2844    /// # Example:
2845    ///
2846    /// ```no_run
2847    /// use s3::bucket::Bucket;
2848    /// use s3::creds::Credentials;
2849    /// use anyhow::Result;
2850    ///
2851    /// # #[tokio::main]
2852    /// # async fn main() -> Result<()> {
2853    ///
2854    /// let bucket_name = "rust-s3-test";
2855    /// let region = "us-east-1".parse()?;
2856    /// let credentials = Credentials::default()?;
2857    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
2858    /// let content = "I want to go to S3".as_bytes();
2859    ///
2860    /// let mut headers = http::HeaderMap::new();
2861    /// headers.insert(
2862    ///     http::HeaderName::from_static("cache-control"),
2863    ///     "public, max-age=300".parse().unwrap(),
2864    /// );
2865    ///
2866    /// // Async variant with `tokio` or `async-std` features
2867    /// #[cfg(not(feature = "sync"))]
2868    /// let response_data = bucket
2869    ///     .put_object_with_content_type_and_headers("/test.file", content, "text/plain", Some(headers.clone())).await?;
2870    ///
2871    /// // `sync` feature will produce an identical method
2872    /// #[cfg(feature = "sync")]
2873    /// let response_data = bucket
2874    ///     .put_object_with_content_type_and_headers("/test.file", content, "text/plain", Some(headers))?;
2875    ///
2876    /// // Blocking variant, generated with `blocking` feature in combination
2877    /// // with `tokio` or `async-std` features.
2878    /// #[cfg(feature = "blocking")]
2879    /// let response_data = bucket
2880    ///     .put_object_with_content_type_and_headers_blocking("/test.file", content, "text/plain", Some(headers))?;
2881    /// #
2882    /// # Ok(())
2883    /// # }
2884    /// ```
2885    #[maybe_async::maybe_async]
2886    pub async fn put_object_with_content_type_and_headers<S: AsRef<str>>(
2887        &self,
2888        path: S,
2889        content: &[u8],
2890        content_type: &str,
2891        custom_headers: Option<HeaderMap>,
2892    ) -> Result<ResponseData, S3Error> {
2893        let command = Command::PutObject {
2894            content,
2895            content_type,
2896            custom_headers,
2897            multipart: None,
2898        };
2899        let request = RequestImpl::new(self, path.as_ref(), command).await?;
2900        request.response_data(true).await
2901    }
2902
2903    /// Put into an S3 bucket, with custom headers for the request.
2904    ///
2905    /// # Example:
2906    ///
2907    /// ```no_run
2908    /// use s3::bucket::Bucket;
2909    /// use s3::creds::Credentials;
2910    /// use anyhow::Result;
2911    ///
2912    /// # #[tokio::main]
2913    /// # async fn main() -> Result<()> {
2914    ///
2915    /// let bucket_name = "rust-s3-test";
2916    /// let region = "us-east-1".parse()?;
2917    /// let credentials = Credentials::default()?;
2918    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
2919    /// let content = "I want to go to S3".as_bytes();
2920    ///
2921    /// let mut headers = http::HeaderMap::new();
2922    /// headers.insert(
2923    ///     http::HeaderName::from_static("cache-control"),
2924    ///     "public, max-age=300".parse().unwrap(),
2925    /// );
2926    ///
2927    /// // Async variant with `tokio` or `async-std` features
2928    /// #[cfg(not(feature = "sync"))]
2929    /// let response_data = bucket.put_object_with_headers("/test.file", content, Some(headers.clone())).await?;
2930    ///
2931    /// // `sync` feature will produce an identical method
2932    /// #[cfg(feature = "sync")]
2933    /// let response_data = bucket.put_object_with_headers("/test.file", content, Some(headers))?;
2934    ///
2935    /// // Blocking variant, generated with `blocking` feature in combination
2936    /// // with `tokio` or `async-std` features.
2937    /// #[cfg(feature = "blocking")]
2938    /// let response_data = bucket.put_object_with_headers_blocking("/test.file", content, Some(headers))?;
2939    /// #
2940    /// # Ok(())
2941    /// # }
2942    /// ```
2943    #[maybe_async::maybe_async]
2944    pub async fn put_object_with_headers<S: AsRef<str>>(
2945        &self,
2946        path: S,
2947        content: &[u8],
2948        custom_headers: Option<HeaderMap>,
2949    ) -> Result<ResponseData, S3Error> {
2950        self.put_object_with_content_type_and_headers(
2951            path,
2952            content,
2953            "application/octet-stream",
2954            custom_headers,
2955        )
2956        .await
2957    }
2958
2959    /// Put into an S3 bucket.
2960    ///
2961    /// # Example:
2962    ///
2963    /// ```no_run
2964    /// use s3::bucket::Bucket;
2965    /// use s3::creds::Credentials;
2966    /// use anyhow::Result;
2967    ///
2968    /// # #[tokio::main]
2969    /// # async fn main() -> Result<()> {
2970    ///
2971    /// let bucket_name = "rust-s3-test";
2972    /// let region = "us-east-1".parse()?;
2973    /// let credentials = Credentials::default()?;
2974    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
2975    /// let content = "I want to go to S3".as_bytes();
2976    ///
2977    /// // Async variant with `tokio` or `async-std` features
2978    /// #[cfg(not(feature = "sync"))]
2979    /// let response_data = bucket.put_object("/test.file", content).await?;
2980    ///
2981    /// // `sync` feature will produce an identical method
2982    /// #[cfg(feature = "sync")]
2983    /// let response_data = bucket.put_object("/test.file", content)?;
2984    ///
2985    /// // Blocking variant, generated with `blocking` feature in combination
2986    /// // with `tokio` or `async-std` features.
2987    /// #[cfg(feature = "blocking")]
2988    /// let response_data = bucket.put_object_blocking("/test.file", content)?;
2989    /// #
2990    /// # Ok(())
2991    /// # }
2992    /// ```
2993    #[maybe_async::maybe_async]
2994    pub async fn put_object<S: AsRef<str>>(
2995        &self,
2996        path: S,
2997        content: &[u8],
2998    ) -> Result<ResponseData, S3Error> {
2999        self.put_object_with_content_type(path, content, "application/octet-stream")
3000            .await
3001    }
3002
3003    /// Create a builder for PUT object operations with custom options
3004    ///
3005    /// This method returns a builder that allows configuring various options
3006    /// for the PUT operation including headers, content type, and metadata.
3007    ///
3008    /// # Example:
3009    ///
3010    /// ```no_run
3011    /// use s3::bucket::Bucket;
3012    /// use s3::creds::Credentials;
3013    /// use anyhow::Result;
3014    ///
3015    /// # #[tokio::main]
3016    /// # async fn main() -> Result<()> {
3017    ///
3018    /// let bucket = Bucket::new("my-bucket", "us-east-1".parse()?, Credentials::default()?)?;
3019    ///
3020    /// // Upload with custom headers using builder pattern
3021    /// #[cfg(not(feature = "sync"))]
3022    /// let response = bucket.put_object_builder("/my-file.txt", b"Hello, World!")
3023    ///     .with_content_type("text/plain")
3024    ///     .with_cache_control("public, max-age=3600")?
3025    ///     .with_metadata("author", "john-doe")?
3026    ///     .execute()
3027    ///     .await?;
3028    /// #[cfg(feature = "sync")]
3029    /// let response = bucket.put_object_builder("/my-file.txt", b"Hello, World!")
3030    ///     .with_content_type("text/plain")
3031    ///     .with_cache_control("public, max-age=3600")?
3032    ///     .with_metadata("author", "john-doe")?
3033    ///     .execute()
3034    ///     ?;
3035    /// #
3036    /// # Ok(())
3037    /// # }
3038    /// ```
3039    pub fn put_object_builder<S: AsRef<str>>(
3040        &self,
3041        path: S,
3042        content: &[u8],
3043    ) -> crate::put_object_request::PutObjectRequest<'_> {
3044        crate::put_object_request::PutObjectRequest::new(self, path, content)
3045    }
3046
3047    fn _tags_xml<S: AsRef<str>>(&self, tags: &[(S, S)]) -> String {
3048        let mut s = String::new();
3049        let content = tags
3050            .iter()
3051            .map(|(name, value)| {
3052                format!(
3053                    "<Tag><Key>{}</Key><Value>{}</Value></Tag>",
3054                    name.as_ref(),
3055                    value.as_ref()
3056                )
3057            })
3058            .fold(String::new(), |mut a, b| {
3059                a.push_str(b.as_str());
3060                a
3061            });
3062        s.push_str("<Tagging><TagSet>");
3063        s.push_str(&content);
3064        s.push_str("</TagSet></Tagging>");
3065        s
3066    }
3067
3068    /// Tag an S3 object.
3069    ///
3070    /// # Example:
3071    ///
3072    /// ```no_run
3073    /// use s3::bucket::Bucket;
3074    /// use s3::creds::Credentials;
3075    /// use anyhow::Result;
3076    ///
3077    /// # #[tokio::main]
3078    /// # async fn main() -> Result<()> {
3079    ///
3080    /// let bucket_name = "rust-s3-test";
3081    /// let region = "us-east-1".parse()?;
3082    /// let credentials = Credentials::default()?;
3083    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
3084    ///
3085    /// // Async variant with `tokio` or `async-std` features
3086    /// #[cfg(not(feature = "sync"))]
3087    /// let response_data = bucket.put_object_tagging("/test.file", &[("Tag1", "Value1"), ("Tag2", "Value2")]).await?;
3088    ///
3089    /// // `sync` feature will produce an identical method
3090    /// #[cfg(feature = "sync")]
3091    /// let response_data = bucket.put_object_tagging("/test.file", &[("Tag1", "Value1"), ("Tag2", "Value2")])?;
3092    ///
3093    /// // Blocking variant, generated with `blocking` feature in combination
3094    /// // with `tokio` or `async-std` features.
3095    /// #[cfg(feature = "blocking")]
3096    /// let response_data = bucket.put_object_tagging_blocking("/test.file", &[("Tag1", "Value1"), ("Tag2", "Value2")])?;
3097    /// #
3098    /// # Ok(())
3099    /// # }
3100    /// ```
3101    #[maybe_async::maybe_async]
3102    pub async fn put_object_tagging<S: AsRef<str>>(
3103        &self,
3104        path: &str,
3105        tags: &[(S, S)],
3106    ) -> Result<ResponseData, S3Error> {
3107        let content = self._tags_xml(tags);
3108        let command = Command::PutObjectTagging { tags: &content };
3109        let request = RequestImpl::new(self, path, command).await?;
3110        request.response_data(false).await
3111    }
3112
3113    /// Delete tags from an S3 object.
3114    ///
3115    /// # Example:
3116    ///
3117    /// ```no_run
3118    /// use s3::bucket::Bucket;
3119    /// use s3::creds::Credentials;
3120    /// use anyhow::Result;
3121    ///
3122    /// # #[tokio::main]
3123    /// # async fn main() -> Result<()> {
3124    ///
3125    /// let bucket_name = "rust-s3-test";
3126    /// let region = "us-east-1".parse()?;
3127    /// let credentials = Credentials::default()?;
3128    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
3129    ///
3130    /// // Async variant with `tokio` or `async-std` features
3131    /// #[cfg(not(feature = "sync"))]
3132    /// let response_data = bucket.delete_object_tagging("/test.file").await?;
3133    ///
3134    /// // `sync` feature will produce an identical method
3135    /// #[cfg(feature = "sync")]
3136    /// let response_data = bucket.delete_object_tagging("/test.file")?;
3137    ///
3138    /// // Blocking variant, generated with `blocking` feature in combination
3139    /// // with `tokio` or `async-std` features.
3140    /// #[cfg(feature = "blocking")]
3141    /// let response_data = bucket.delete_object_tagging_blocking("/test.file")?;
3142    /// #
3143    /// # Ok(())
3144    /// # }
3145    /// ```
3146    #[maybe_async::maybe_async]
3147    pub async fn delete_object_tagging<S: AsRef<str>>(
3148        &self,
3149        path: S,
3150    ) -> Result<ResponseData, S3Error> {
3151        let command = Command::DeleteObjectTagging;
3152        let request = RequestImpl::new(self, path.as_ref(), command).await?;
3153        request.response_data(false).await
3154    }
3155
3156    /// Retrieve an S3 object list of tags.
3157    ///
3158    /// # Example:
3159    ///
3160    /// ```no_run
3161    /// use s3::bucket::Bucket;
3162    /// use s3::creds::Credentials;
3163    /// use anyhow::Result;
3164    ///
3165    /// # #[tokio::main]
3166    /// # async fn main() -> Result<()> {
3167    ///
3168    /// let bucket_name = "rust-s3-test";
3169    /// let region = "us-east-1".parse()?;
3170    /// let credentials = Credentials::default()?;
3171    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
3172    ///
3173    /// // Async variant with `tokio` or `async-std` features
3174    /// #[cfg(not(feature = "sync"))]
3175    /// let response_data = bucket.get_object_tagging("/test.file").await?;
3176    ///
3177    /// // `sync` feature will produce an identical method
3178    /// #[cfg(feature = "sync")]
3179    /// let response_data = bucket.get_object_tagging("/test.file")?;
3180    ///
3181    /// // Blocking variant, generated with `blocking` feature in combination
3182    /// // with `tokio` or `async-std` features.
3183    /// #[cfg(feature = "blocking")]
3184    /// let response_data = bucket.get_object_tagging_blocking("/test.file")?;
3185    /// #
3186    /// # Ok(())
3187    /// # }
3188    /// ```
3189    #[cfg(feature = "tags")]
3190    #[maybe_async::maybe_async]
3191    pub async fn get_object_tagging<S: AsRef<str>>(
3192        &self,
3193        path: S,
3194    ) -> Result<(Vec<Tag>, u16), S3Error> {
3195        let command = Command::GetObjectTagging {};
3196        let request = RequestImpl::new(self, path.as_ref(), command).await?;
3197        let result = request.response_data(false).await?;
3198
3199        let mut tags = Vec::new();
3200
3201        if result.status_code() == 200 {
3202            let result_string = String::from_utf8_lossy(result.as_slice());
3203
3204            // Add namespace if it doesn't exist
3205            let ns = "http://s3.amazonaws.com/doc/2006-03-01/";
3206            let result_string =
3207                if let Err(minidom::Error::MissingNamespace) = result_string.parse::<Element>() {
3208                    result_string
3209                        .replace("<Tagging>", &format!("<Tagging xmlns=\"{}\">", ns))
3210                        .into()
3211                } else {
3212                    result_string
3213                };
3214
3215            if let Ok(tagging) = result_string.parse::<Element>() {
3216                for tag_set in tagging.children() {
3217                    if tag_set.is("TagSet", ns) {
3218                        for tag in tag_set.children() {
3219                            if tag.is("Tag", ns) {
3220                                let key = if let Some(element) = tag.get_child("Key", ns) {
3221                                    element.text()
3222                                } else {
3223                                    "Could not parse Key from Tag".to_string()
3224                                };
3225                                let value = if let Some(element) = tag.get_child("Value", ns) {
3226                                    element.text()
3227                                } else {
3228                                    "Could not parse Values from Tag".to_string()
3229                                };
3230                                tags.push(Tag { key, value });
3231                            }
3232                        }
3233                    }
3234                }
3235            }
3236        }
3237
3238        Ok((tags, result.status_code()))
3239    }
3240
3241    #[maybe_async::maybe_async]
3242    pub async fn list_page(
3243        &self,
3244        prefix: String,
3245        delimiter: Option<String>,
3246        continuation_token: Option<String>,
3247        start_after: Option<String>,
3248        max_keys: Option<usize>,
3249    ) -> Result<(ListBucketResult, u16), S3Error> {
3250        let command = if self.listobjects_v2 {
3251            Command::ListObjectsV2 {
3252                prefix,
3253                delimiter,
3254                continuation_token,
3255                start_after,
3256                max_keys,
3257            }
3258        } else {
3259            // In the v1 ListObjects request, there is only one "marker"
3260            // field that serves as both the initial starting position,
3261            // and as the continuation token.
3262            Command::ListObjects {
3263                prefix,
3264                delimiter,
3265                marker: std::cmp::max(continuation_token, start_after),
3266                max_keys,
3267            }
3268        };
3269        let request = RequestImpl::new(self, "/", command).await?;
3270        let response_data = request.response_data(false).await?;
3271        let list_bucket_result = quick_xml::de::from_reader(response_data.as_slice())?;
3272
3273        Ok((list_bucket_result, response_data.status_code()))
3274    }
3275
3276    /// List the contents of an S3 bucket.
3277    ///
3278    /// # Example:
3279    ///
3280    /// ```no_run
3281    /// use s3::bucket::Bucket;
3282    /// use s3::creds::Credentials;
3283    /// use anyhow::Result;
3284    ///
3285    /// # #[tokio::main]
3286    /// # async fn main() -> Result<()> {
3287    ///
3288    /// let bucket_name = "rust-s3-test";
3289    /// let region = "us-east-1".parse()?;
3290    /// let credentials = Credentials::default()?;
3291    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
3292    ///
3293    /// // Async variant with `tokio` or `async-std` features
3294    /// #[cfg(not(feature = "sync"))]
3295    /// let results = bucket.list("/".to_string(), Some("/".to_string())).await?;
3296    ///
3297    /// // `sync` feature will produce an identical method
3298    /// #[cfg(feature = "sync")]
3299    /// let results = bucket.list("/".to_string(), Some("/".to_string()))?;
3300    ///
3301    /// // Blocking variant, generated with `blocking` feature in combination
3302    /// // with `tokio` or `async-std` features.
3303    /// #[cfg(feature = "blocking")]
3304    /// let results = bucket.list_blocking("/".to_string(), Some("/".to_string()))?;
3305    /// #
3306    /// # Ok(())
3307    /// # }
3308    /// ```
3309    #[maybe_async::maybe_async]
3310    #[allow(clippy::assigning_clones)]
3311    pub async fn list(
3312        &self,
3313        prefix: String,
3314        delimiter: Option<String>,
3315    ) -> Result<Vec<ListBucketResult>, S3Error> {
3316        let the_bucket = self.to_owned();
3317        let mut results = Vec::new();
3318        let mut continuation_token = None;
3319
3320        loop {
3321            let (list_bucket_result, _) = the_bucket
3322                .list_page(
3323                    prefix.clone(),
3324                    delimiter.clone(),
3325                    continuation_token,
3326                    None,
3327                    None,
3328                )
3329                .await?;
3330            continuation_token = list_bucket_result.next_continuation_token.clone();
3331            results.push(list_bucket_result);
3332            if continuation_token.is_none() {
3333                break;
3334            }
3335        }
3336
3337        Ok(results)
3338    }
3339
3340    /// List one page of in-progress multipart uploads. Pass both returned cursor markers to
3341    /// continue a truncated listing; directory buckets and some compatible services may omit
3342    /// the upload ID marker.
3343    #[maybe_async::maybe_async]
3344    pub async fn list_multiparts_uploads_page(
3345        &self,
3346        prefix: Option<&str>,
3347        delimiter: Option<&str>,
3348        key_marker: Option<String>,
3349        upload_id_marker: Option<String>,
3350        max_uploads: Option<usize>,
3351    ) -> Result<(ListMultipartUploadsResult, u16), S3Error> {
3352        let command = Command::ListMultipartUploads {
3353            prefix,
3354            delimiter,
3355            key_marker,
3356            upload_id_marker,
3357            max_uploads,
3358        };
3359        let request = RequestImpl::new(self, "/", command).await?;
3360        let response_data = request.response_data(false).await?;
3361        let list_bucket_result = quick_xml::de::from_reader(response_data.as_slice())?;
3362
3363        Ok((list_bucket_result, response_data.status_code()))
3364    }
3365
3366    /// List the ongoing multipart uploads of an S3 bucket. This may be useful to cleanup failed
3367    /// uploads, together with [`crate::bucket::Bucket::abort_upload`]. The method follows both
3368    /// pagination markers when supplied by the service and returns an error if a truncated
3369    /// response has no next key marker or repeats a cursor.
3370    ///
3371    /// # Example:
3372    ///
3373    /// ```no_run
3374    /// use s3::bucket::Bucket;
3375    /// use s3::creds::Credentials;
3376    /// use anyhow::Result;
3377    ///
3378    /// # #[tokio::main]
3379    /// # async fn main() -> Result<()> {
3380    ///
3381    /// let bucket_name = "rust-s3-test";
3382    /// let region = "us-east-1".parse()?;
3383    /// let credentials = Credentials::default()?;
3384    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
3385    ///
3386    /// // Async variant with `tokio` or `async-std` features
3387    /// #[cfg(not(feature = "sync"))]
3388    /// let results = bucket.list_multiparts_uploads(Some("/"), Some("/")).await?;
3389    ///
3390    /// // `sync` feature will produce an identical method
3391    /// #[cfg(feature = "sync")]
3392    /// let results = bucket.list_multiparts_uploads(Some("/"), Some("/"))?;
3393    ///
3394    /// // Blocking variant, generated with `blocking` feature in combination
3395    /// // with `tokio` or `async-std` features.
3396    /// #[cfg(feature = "blocking")]
3397    /// let results = bucket.list_multiparts_uploads_blocking(Some("/"), Some("/"))?;
3398    /// #
3399    /// # Ok(())
3400    /// # }
3401    /// ```
3402    #[maybe_async::maybe_async]
3403    pub async fn list_multiparts_uploads(
3404        &self,
3405        prefix: Option<&str>,
3406        delimiter: Option<&str>,
3407    ) -> Result<Vec<ListMultipartUploadsResult>, S3Error> {
3408        let the_bucket = self.to_owned();
3409        let mut results = Vec::new();
3410        let mut key_marker: Option<String> = None;
3411        let mut upload_id_marker: Option<String> = None;
3412        let mut seen_cursors = HashSet::new();
3413
3414        loop {
3415            if !seen_cursors.insert((key_marker.clone(), upload_id_marker.clone())) {
3416                return Err(S3Error::InvalidMultipartUploadsPagination(
3417                    "pagination cursor repeated",
3418                ));
3419            }
3420
3421            let (list_multiparts_uploads_result, _) = the_bucket
3422                .list_multiparts_uploads_page(prefix, delimiter, key_marker, upload_id_marker, None)
3423                .await?;
3424
3425            if !list_multiparts_uploads_result.is_truncated {
3426                results.push(list_multiparts_uploads_result);
3427                break;
3428            }
3429
3430            let next_key_marker = list_multiparts_uploads_result.next_marker.clone().ok_or(
3431                S3Error::InvalidMultipartUploadsPagination(
3432                    "truncated response has no next key marker",
3433                ),
3434            )?;
3435            let next_upload_id_marker =
3436                list_multiparts_uploads_result.next_upload_id_marker.clone();
3437            if seen_cursors
3438                .contains(&(Some(next_key_marker.clone()), next_upload_id_marker.clone()))
3439            {
3440                return Err(S3Error::InvalidMultipartUploadsPagination(
3441                    "pagination cursor repeated",
3442                ));
3443            }
3444
3445            key_marker = Some(next_key_marker);
3446            upload_id_marker = next_upload_id_marker;
3447            results.push(list_multiparts_uploads_result);
3448        }
3449
3450        Ok(results)
3451    }
3452
3453    /// Abort a running multipart upload.
3454    ///
3455    /// # Example:
3456    ///
3457    /// ```no_run
3458    /// use s3::bucket::Bucket;
3459    /// use s3::creds::Credentials;
3460    /// use anyhow::Result;
3461    ///
3462    /// # #[tokio::main]
3463    /// # async fn main() -> Result<()> {
3464    ///
3465    /// let bucket_name = "rust-s3-test";
3466    /// let region = "us-east-1".parse()?;
3467    /// let credentials = Credentials::default()?;
3468    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
3469    ///
3470    /// // Async variant with `tokio` or `async-std` features
3471    /// #[cfg(not(feature = "sync"))]
3472    /// let results = bucket.abort_upload("/some/file.txt", "ZDFjM2I0YmEtMzU3ZC00OTQ1LTlkNGUtMTgxZThjYzIwNjA2").await?;
3473    ///
3474    /// // `sync` feature will produce an identical method
3475    /// #[cfg(feature = "sync")]
3476    /// let results = bucket.abort_upload("/some/file.txt", "ZDFjM2I0YmEtMzU3ZC00OTQ1LTlkNGUtMTgxZThjYzIwNjA2")?;
3477    ///
3478    /// // Blocking variant, generated with `blocking` feature in combination
3479    /// // with `tokio` or `async-std` features.
3480    /// #[cfg(feature = "blocking")]
3481    /// let results = bucket.abort_upload_blocking("/some/file.txt", "ZDFjM2I0YmEtMzU3ZC00OTQ1LTlkNGUtMTgxZThjYzIwNjA2")?;
3482    /// #
3483    /// # Ok(())
3484    /// # }
3485    /// ```
3486    #[maybe_async::maybe_async]
3487    pub async fn abort_upload(&self, key: &str, upload_id: &str) -> Result<(), S3Error> {
3488        let abort = Command::AbortMultipartUpload { upload_id };
3489        let abort_request = RequestImpl::new(self, key, abort).await?;
3490        let response_data = abort_request.response_data(false).await?;
3491
3492        if (200..300).contains(&response_data.status_code()) {
3493            Ok(())
3494        } else {
3495            let utf8_content = String::from_utf8(response_data.as_slice().to_vec())?;
3496            Err(S3Error::HttpFailWithBody(
3497                response_data.status_code(),
3498                utf8_content,
3499            ))
3500        }
3501    }
3502
3503    /// Get path_style field of the Bucket struct
3504    pub fn is_path_style(&self) -> bool {
3505        self.path_style
3506    }
3507
3508    /// Get negated path_style field of the Bucket struct
3509    pub fn is_subdomain_style(&self) -> bool {
3510        !self.path_style
3511    }
3512
3513    /// Configure bucket to use path-style urls and headers
3514    pub fn set_path_style(&mut self) {
3515        self.path_style = true;
3516    }
3517
3518    /// Configure bucket to use subdomain style urls and headers \[default\]
3519    pub fn set_subdomain_style(&mut self) {
3520        self.path_style = false;
3521    }
3522
3523    /// Configure the total per-attempt timeout for HTTP requests, or disable
3524    /// the library-level deadline with `None`. Defaults to 60 seconds.
3525    ///
3526    /// Async backends apply this deadline from HTTP send through response-body
3527    /// reads used by buffered downloads, writer copies, and lazy streams.
3528    /// The synchronous backend uses its transport's timeout behavior; disabling
3529    /// the library-level deadline does not remove transport-specific limits.
3530    pub fn set_request_timeout(&mut self, timeout: Option<Duration>) {
3531        self.request_timeout = timeout;
3532    }
3533
3534    /// Configure bucket to use the older ListObjects API
3535    ///
3536    /// If your provider doesn't support the ListObjectsV2 interface, set this to
3537    /// use the v1 ListObjects interface instead. This is currently needed at least
3538    /// for Google Cloud Storage.
3539    pub fn set_listobjects_v1(&mut self) {
3540        self.listobjects_v2 = false;
3541    }
3542
3543    /// Configure bucket to use the newer ListObjectsV2 API
3544    pub fn set_listobjects_v2(&mut self) {
3545        self.listobjects_v2 = true;
3546    }
3547
3548    /// Get a reference to the name of the S3 bucket.
3549    pub fn name(&self) -> String {
3550        self.name.to_string()
3551    }
3552
3553    // Get a reference to the hostname of the S3 API endpoint.
3554    pub fn host(&self) -> String {
3555        if self.path_style {
3556            self.path_style_host()
3557        } else {
3558            self.subdomain_style_host()
3559        }
3560    }
3561
3562    pub fn url(&self) -> String {
3563        if self.path_style {
3564            format!(
3565                "{}://{}/{}",
3566                self.scheme(),
3567                self.path_style_host(),
3568                self.name()
3569            )
3570        } else {
3571            format!("{}://{}", self.scheme(), self.subdomain_style_host())
3572        }
3573    }
3574
3575    /// Get a paths-style reference to the hostname of the S3 API endpoint.
3576    pub fn path_style_host(&self) -> String {
3577        self.region.host()
3578    }
3579
3580    pub fn subdomain_style_host(&self) -> String {
3581        format!("{}.{}", self.name, self.region.host())
3582    }
3583
3584    // pub fn self_host(&self) -> String {
3585    //     format!("{}.{}", self.name, self.region.host())
3586    // }
3587
3588    pub fn scheme(&self) -> String {
3589        self.region.scheme()
3590    }
3591
3592    /// Get the region this object will connect to.
3593    pub fn region(&self) -> Region {
3594        self.region.clone()
3595    }
3596
3597    /// Get a reference to the AWS access key.
3598    #[maybe_async::maybe_async]
3599    pub async fn access_key(&self) -> Result<Option<String>, S3Error> {
3600        Ok(self.credentials().await?.access_key)
3601    }
3602
3603    /// Get a reference to the AWS secret key.
3604    #[maybe_async::maybe_async]
3605    pub async fn secret_key(&self) -> Result<Option<String>, S3Error> {
3606        Ok(self.credentials().await?.secret_key)
3607    }
3608
3609    /// Get a reference to the AWS security token.
3610    #[maybe_async::maybe_async]
3611    pub async fn security_token(&self) -> Result<Option<String>, S3Error> {
3612        Ok(self.credentials().await?.security_token)
3613    }
3614
3615    /// Get a reference to the AWS session token.
3616    #[maybe_async::maybe_async]
3617    pub async fn session_token(&self) -> Result<Option<String>, S3Error> {
3618        Ok(self.credentials().await?.session_token)
3619    }
3620
3621    /// Get a reference to the full [`Credentials`](struct.Credentials.html)
3622    /// object used by this `Bucket`.
3623    #[maybe_async::async_impl]
3624    pub async fn credentials(&self) -> Result<Credentials, S3Error> {
3625        Ok(self.credentials.read().await.clone())
3626    }
3627
3628    #[maybe_async::sync_impl]
3629    pub fn credentials(&self) -> Result<Credentials, S3Error> {
3630        match self.credentials.read() {
3631            Ok(credentials) => Ok(credentials.clone()),
3632            Err(_) => Err(S3Error::CredentialsReadLock),
3633        }
3634    }
3635
3636    /// Change the credentials used by the Bucket.
3637    pub fn set_credentials(&mut self, credentials: Credentials) {
3638        self.credentials = Arc::new(RwLock::new(credentials));
3639        #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
3640        {
3641            self.credentials_refresh_gate = Arc::new(AsyncMutex::new(()));
3642        }
3643    }
3644
3645    /// Add an extra header to send with requests to S3.
3646    ///
3647    /// Add an extra header to send with requests. Note that the library
3648    /// already sets a number of headers - headers set with this method will be
3649    /// overridden by the library headers:
3650    ///   * Host
3651    ///   * Content-Type
3652    ///   * Date
3653    ///   * Content-Length
3654    ///   * Authorization
3655    ///   * X-Amz-Content-Sha256
3656    ///   * X-Amz-Date
3657    pub fn add_header(&mut self, key: &str, value: &str) {
3658        self.extra_headers
3659            .insert(HeaderName::from_str(key).unwrap(), value.parse().unwrap());
3660    }
3661
3662    /// Get a reference to the extra headers to be passed to the S3 API.
3663    pub fn extra_headers(&self) -> &HeaderMap {
3664        &self.extra_headers
3665    }
3666
3667    /// Get a mutable reference to the extra headers to be passed to the S3
3668    /// API.
3669    pub fn extra_headers_mut(&mut self) -> &mut HeaderMap {
3670        &mut self.extra_headers
3671    }
3672
3673    /// Add an extra query pair to the URL used for S3 API access.
3674    pub fn add_query(&mut self, key: &str, value: &str) {
3675        self.extra_query.insert(key.into(), value.into());
3676    }
3677
3678    /// Get a reference to the extra query pairs to be passed to the S3 API.
3679    pub fn extra_query(&self) -> &Query {
3680        &self.extra_query
3681    }
3682
3683    /// Get a mutable reference to the extra query pairs to be passed to the S3
3684    /// API.
3685    pub fn extra_query_mut(&mut self) -> &mut Query {
3686        &mut self.extra_query
3687    }
3688
3689    pub fn request_timeout(&self) -> Option<Duration> {
3690        self.request_timeout
3691    }
3692}
3693
3694#[cfg(test)]
3695mod test {
3696
3697    use super::validate_success_xml_response;
3698
3699    use crate::BucketConfiguration;
3700    use crate::Tag;
3701    use crate::creds::Credentials;
3702    use crate::error::S3Error;
3703    use crate::post_policy::{PostPolicyField, PostPolicyValue};
3704    use crate::region::Region;
3705    use crate::request::ResponseData;
3706    use crate::serde_types::{
3707        BucketLifecycleConfiguration, CorsConfiguration, CorsRule, Expiration, LifecycleFilter,
3708        LifecycleRule, Part,
3709    };
3710    use crate::{Bucket, PostPolicy};
3711    use http::header::{CACHE_CONTROL, HeaderMap, HeaderName, HeaderValue};
3712    use std::collections::HashMap;
3713    use std::env;
3714    #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3715    use std::io::{Read, Write};
3716    #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3717    use std::net::TcpListener;
3718    #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3719    use std::sync::mpsc::{self, TryRecvError};
3720    #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3721    use std::thread;
3722    #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
3723    use std::time::{Duration, Instant};
3724
3725    fn init() {
3726        let _ = env_logger::builder().is_test(true).try_init();
3727    }
3728
3729    #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
3730    #[test]
3731    fn concurrent_chunk_calculation_is_bounded_and_has_a_fallback() {
3732        let per_chunk = super::CHUNK_SIZE as u64 * 3;
3733        assert_eq!(Bucket::max_concurrent_chunks_for_memory(0), 3);
3734        assert_eq!(Bucket::max_concurrent_chunks_for_memory(per_chunk - 1), 2);
3735        assert_eq!(Bucket::max_concurrent_chunks_for_memory(per_chunk * 9), 9);
3736        assert_eq!(Bucket::max_concurrent_chunks_for_memory(per_chunk * 10), 10);
3737        assert_eq!(Bucket::max_concurrent_chunks_for_memory(u64::MAX), 10);
3738    }
3739
3740    fn test_object_key(suffix: &str) -> String {
3741        format!(
3742            "{}{suffix}",
3743            env::var("RUST_S3_TEST_PREFIX").unwrap_or_default()
3744        )
3745    }
3746
3747    fn test_object_path(suffix: &str) -> String {
3748        format!("/{}", test_object_key(suffix))
3749    }
3750
3751    fn xml_response(body: &str) -> ResponseData {
3752        ResponseData::new(
3753            bytes::Bytes::copy_from_slice(body.as_bytes()),
3754            200,
3755            HashMap::new(),
3756        )
3757    }
3758
3759    fn make_mock_stream_blocking(
3760        stream: std::net::TcpStream,
3761    ) -> std::io::Result<std::net::TcpStream> {
3762        // BSD accept can carry over O_NONBLOCK from the listener.
3763        stream.set_nonblocking(false)?;
3764        Ok(stream)
3765    }
3766
3767    #[maybe_async::maybe_async]
3768    async fn run_multipart_upload_listing_case(
3769        responses: Vec<String>,
3770    ) -> (
3771        Result<Vec<crate::serde_types::ListMultipartUploadsResult>, S3Error>,
3772        Result<Vec<String>, String>,
3773    ) {
3774        use std::io::{Read, Write};
3775        use std::net::TcpListener;
3776        use std::sync::mpsc;
3777        use std::thread;
3778        use std::time::{Duration, Instant};
3779
3780        fn read_request_target(
3781            stream: &mut std::net::TcpStream,
3782            deadline: Instant,
3783        ) -> Result<String, String> {
3784            let mut headers = Vec::new();
3785            let mut byte = [0u8; 1];
3786            while !headers.ends_with(b"\r\n\r\n") {
3787                if headers.len() >= 16 * 1024 || Instant::now() >= deadline {
3788                    return Err("request headers exceeded the fixture bound".to_owned());
3789                }
3790                match stream.read(&mut byte) {
3791                    Ok(0) => return Err("client closed before sending request headers".to_owned()),
3792                    Ok(_) => headers.push(byte[0]),
3793                    Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
3794                    Err(error) => return Err(format!("failed to read request headers: {error}")),
3795                }
3796            }
3797            let request = String::from_utf8_lossy(&headers);
3798            let mut fields = request
3799                .lines()
3800                .next()
3801                .unwrap_or_default()
3802                .split_whitespace();
3803            let method = fields.next().unwrap_or_default();
3804            let target = fields.next().unwrap_or_default();
3805            if method != "GET" || target.is_empty() {
3806                return Err("unexpected request line in multipart listing fixture".to_owned());
3807            }
3808            Ok(target.to_owned())
3809        }
3810
3811        fn send_listing_response(
3812            stream: &mut std::net::TcpStream,
3813            body: &str,
3814        ) -> std::io::Result<()> {
3815            write!(
3816                stream,
3817                "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
3818                body.len(),
3819                body
3820            )
3821        }
3822
3823        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
3824        listener.set_nonblocking(true).unwrap();
3825        let endpoint = format!("http://{}", listener.local_addr().unwrap());
3826        let (done_tx, done_rx) = mpsc::channel();
3827        let server = thread::spawn(move || {
3828            let deadline = Instant::now() + Duration::from_secs(8);
3829            let mut requests = Vec::new();
3830            for body in &responses {
3831                let (stream, _) = loop {
3832                    match listener.accept() {
3833                        Ok(connection) => break connection,
3834                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
3835                            if Instant::now() >= deadline {
3836                                return Err("timed out waiting for a pagination request".to_owned());
3837                            }
3838                            if done_rx.try_recv().is_ok() {
3839                                return Err(
3840                                    "client finished before all scripted requests".to_owned()
3841                                );
3842                            }
3843                            thread::sleep(Duration::from_millis(2));
3844                        }
3845                        Err(error) => return Err(format!("failed to accept request: {error}")),
3846                    }
3847                };
3848                let mut stream = make_mock_stream_blocking(stream)
3849                    .map_err(|error| format!("failed to configure accepted stream: {error}"))?;
3850                stream
3851                    .set_read_timeout(Some(Duration::from_secs(3)))
3852                    .map_err(|error| format!("failed to set read timeout: {error}"))?;
3853                stream
3854                    .set_write_timeout(Some(Duration::from_secs(3)))
3855                    .map_err(|error| format!("failed to set write timeout: {error}"))?;
3856                let target = read_request_target(&mut stream, deadline)?;
3857                requests.push(target);
3858                send_listing_response(&mut stream, body)
3859                    .map_err(|error| format!("failed to send listing page: {error}"))?;
3860            }
3861
3862            loop {
3863                match listener.accept() {
3864                    Ok((stream, _)) => {
3865                        let mut stream = make_mock_stream_blocking(stream).map_err(|error| {
3866                            format!("failed to configure extra stream: {error}")
3867                        })?;
3868                        stream
3869                            .set_read_timeout(Some(Duration::from_secs(3)))
3870                            .map_err(|error| format!("failed to set read timeout: {error}"))?;
3871                        let target = read_request_target(&mut stream, deadline)?;
3872                        requests.push(target);
3873                        // Let an unexpected extra request complete so the client does not hang.
3874                        let final_page = "<ListMultipartUploadsResult><Bucket>test-bucket</Bucket><IsTruncated>false</IsTruncated></ListMultipartUploadsResult>";
3875                        send_listing_response(&mut stream, final_page)
3876                            .map_err(|error| format!("failed to answer extra request: {error}"))?;
3877                        return Err("client made more pagination requests than scripted".to_owned());
3878                    }
3879                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
3880                        if done_rx.recv_timeout(Duration::from_millis(10)).is_ok() {
3881                            return Ok(requests);
3882                        }
3883                        if Instant::now() >= deadline {
3884                            return Err("client did not finish before fixture deadline".to_owned());
3885                        }
3886                    }
3887                    Err(error) => return Err(format!("failed to accept extra request: {error}")),
3888                }
3889            }
3890        });
3891
3892        let credentials = Credentials::new(
3893            Some("test_access_key"),
3894            Some("test_secret_key"),
3895            None,
3896            None,
3897            None,
3898        )
3899        .unwrap();
3900        let bucket = Bucket::new(
3901            "test-bucket",
3902            Region::Custom {
3903                region: "us-east-1".to_owned(),
3904                endpoint,
3905            },
3906            credentials,
3907        )
3908        .unwrap()
3909        .with_path_style()
3910        .with_request_timeout(Duration::from_secs(4))
3911        .unwrap();
3912        let result = bucket.list_multiparts_uploads(Some("same-key"), None).await;
3913        let _ = done_tx.send(());
3914        let server_result = server.join().expect("multipart listing server panicked");
3915        (result, server_result)
3916    }
3917
3918    fn multipart_listing_xml(
3919        next_key_marker: Option<&str>,
3920        next_upload_id_marker: Option<&str>,
3921        is_truncated: bool,
3922        upload: Option<(&str, &str)>,
3923    ) -> String {
3924        let mut xml = String::from("<ListMultipartUploadsResult><Bucket>test-bucket</Bucket>");
3925        if let Some(marker) = next_key_marker {
3926            xml.push_str(&format!("<NextKeyMarker>{marker}</NextKeyMarker>"));
3927        }
3928        if let Some(marker) = next_upload_id_marker {
3929            xml.push_str(&format!(
3930                "<NextUploadIdMarker>{marker}</NextUploadIdMarker>"
3931            ));
3932        }
3933        xml.push_str(&format!("<IsTruncated>{is_truncated}</IsTruncated>"));
3934        if let Some((key, upload_id)) = upload {
3935            xml.push_str(&format!(
3936                "<Upload><Initiated>2026-01-01T00:00:00.000Z</Initiated><StorageClass>STANDARD</StorageClass><Key>{key}</Key><UploadId>{upload_id}</UploadId></Upload>"
3937            ));
3938        }
3939        xml.push_str("</ListMultipartUploadsResult>");
3940        xml
3941    }
3942
3943    #[maybe_async::test(
3944        feature = "sync",
3945        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
3946        async(
3947            all(not(feature = "sync"), feature = "with-async-std"),
3948            async_std::test
3949        )
3950    )]
3951    async fn multipart_upload_pagination_uses_both_markers_and_clears_missing_upload_marker() {
3952        fn query(target: &str) -> HashMap<String, String> {
3953            url::Url::parse(&format!("http://localhost{target}"))
3954                .unwrap()
3955                .query_pairs()
3956                .into_owned()
3957                .collect()
3958        }
3959
3960        let (result, requests) = run_multipart_upload_listing_case(vec![
3961            multipart_listing_xml(
3962                Some("same-key"),
3963                Some("upload +/2"),
3964                true,
3965                Some(("same-key", "upload +/2")),
3966            ),
3967            multipart_listing_xml(None, None, false, Some(("same-key", "upload-2"))),
3968        ])
3969        .await;
3970        let pages = result.expect("two-page listing should succeed");
3971        let requests = requests.expect("mock server request sequence should succeed");
3972        assert_eq!(pages.len(), 2);
3973        let mut listed_ids = pages
3974            .iter()
3975            .flat_map(|page| page.uploads.iter().map(|upload| upload.id.clone()))
3976            .collect::<Vec<_>>();
3977        listed_ids.sort();
3978        assert_eq!(listed_ids, ["upload +/2", "upload-2"]);
3979        assert_eq!(requests.len(), 2);
3980        let second_query = query(&requests[1]);
3981        assert_eq!(second_query.get("key-marker").unwrap(), "same-key");
3982        assert_eq!(second_query.get("upload-id-marker").unwrap(), "upload +/2");
3983
3984        let (result, requests) = run_multipart_upload_listing_case(vec![
3985            multipart_listing_xml(Some("same-key"), Some("id-one"), true, None),
3986            multipart_listing_xml(Some("later-key"), None, true, None),
3987            multipart_listing_xml(None, None, false, None),
3988        ])
3989        .await;
3990        assert_eq!(result.unwrap().len(), 3);
3991        let requests = requests.expect("key-only continuation should succeed");
3992        assert_eq!(requests.len(), 3);
3993        let second_query = query(&requests[1]);
3994        assert_eq!(second_query.get("upload-id-marker").unwrap(), "id-one");
3995        let third_query = query(&requests[2]);
3996        assert_eq!(third_query.get("key-marker").unwrap(), "later-key");
3997        assert!(!third_query.contains_key("upload-id-marker"));
3998    }
3999
4000    #[maybe_async::test(
4001        feature = "sync",
4002        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
4003        async(
4004            all(not(feature = "sync"), feature = "with-async-std"),
4005            async_std::test
4006        )
4007    )]
4008    async fn multipart_upload_pagination_rejects_missing_repeated_and_cyclic_cursors() {
4009        let cases = vec![
4010            (
4011                "truncated response has no next key marker",
4012                vec![multipart_listing_xml(None, Some("id-a"), true, None)],
4013                1,
4014            ),
4015            (
4016                "pagination cursor repeated",
4017                vec![
4018                    multipart_listing_xml(Some("same-key"), Some("id-a"), true, None),
4019                    multipart_listing_xml(Some("same-key"), Some("id-a"), true, None),
4020                ],
4021                2,
4022            ),
4023            (
4024                "pagination cursor repeated",
4025                vec![
4026                    multipart_listing_xml(Some("same-key"), Some("id-a"), true, None),
4027                    multipart_listing_xml(Some("same-key"), Some("id-b"), true, None),
4028                    multipart_listing_xml(Some("same-key"), Some("id-a"), true, None),
4029                ],
4030                3,
4031            ),
4032        ];
4033
4034        for (expected_reason, responses, expected_requests) in cases {
4035            let (result, requests) = run_multipart_upload_listing_case(responses).await;
4036            match result {
4037                Err(S3Error::InvalidMultipartUploadsPagination(reason)) => {
4038                    assert_eq!(reason, expected_reason);
4039                }
4040                other => panic!("expected pagination error, got {other:?}"),
4041            }
4042            let requests = requests.expect("mock server request sequence should succeed");
4043            assert_eq!(requests.len(), expected_requests);
4044        }
4045    }
4046
4047    #[cfg(all(
4048        not(feature = "sync"),
4049        any(feature = "with-tokio", feature = "with-async-std")
4050    ))]
4051    #[derive(Debug)]
4052    struct MultipartWireRequest {
4053        line: String,
4054        headers: HashMap<String, String>,
4055        content_length: usize,
4056    }
4057
4058    #[cfg(all(
4059        not(feature = "sync"),
4060        any(feature = "with-tokio", feature = "with-async-std")
4061    ))]
4062    #[maybe_async::maybe_async]
4063    async fn run_multipart_header_case(
4064        size: usize,
4065        fail_reader: bool,
4066        custom_headers: HeaderMap,
4067    ) -> (
4068        Result<(), S3Error>,
4069        Result<Vec<MultipartWireRequest>, String>,
4070    ) {
4071        use std::io::{Read, Write};
4072        use std::net::TcpListener;
4073        use std::sync::mpsc;
4074        use std::thread;
4075        use std::time::{Duration, Instant};
4076
4077        fn read_request(
4078            stream: &mut std::net::TcpStream,
4079            deadline: Instant,
4080        ) -> Result<MultipartWireRequest, String> {
4081            let mut raw_headers = Vec::new();
4082            let mut byte = [0u8; 1];
4083            while !raw_headers.ends_with(b"\r\n\r\n") {
4084                if raw_headers.len() >= 16 * 1024 || Instant::now() >= deadline {
4085                    return Err("header limit or deadline exceeded".to_owned());
4086                }
4087                stream
4088                    .read_exact(&mut byte)
4089                    .map_err(|error| format!("read headers: {error}"))?;
4090                raw_headers.push(byte[0]);
4091            }
4092
4093            let header_text = String::from_utf8_lossy(&raw_headers);
4094            let mut lines = header_text.lines();
4095            let line = lines.next().unwrap_or_default().to_owned();
4096            let mut headers = HashMap::new();
4097            for header in lines {
4098                if let Some((name, value)) = header.split_once(':') {
4099                    let name = name.trim().to_ascii_lowercase();
4100                    if name != "authorization" && name != "x-amz-security-token" {
4101                        headers.insert(name, value.trim().to_owned());
4102                    }
4103                }
4104            }
4105            let content_length = headers
4106                .get("content-length")
4107                .and_then(|value| value.parse::<usize>().ok())
4108                .unwrap_or(0);
4109            if content_length > super::CHUNK_SIZE + 16 * 1024 {
4110                return Err("request body exceeded test cap".to_owned());
4111            }
4112            let mut body = vec![0; content_length];
4113            stream
4114                .read_exact(&mut body)
4115                .map_err(|error| format!("read request body: {error}"))?;
4116
4117            Ok(MultipartWireRequest {
4118                line,
4119                headers,
4120                content_length,
4121            })
4122        }
4123
4124        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4125        listener.set_nonblocking(true).unwrap();
4126        let endpoint = format!("http://{}", listener.local_addr().unwrap());
4127        let (done_tx, done_rx) = mpsc::channel();
4128        let rejected_headers = custom_headers.clone();
4129        let server = thread::spawn(move || {
4130            let expected = if rejected_headers.contains_key("content-length")
4131                || rejected_headers.contains_key("content-md5")
4132                || rejected_headers.contains_key("transfer-encoding")
4133                || rejected_headers.contains_key("x-amz-content-sha256")
4134                || rejected_headers.keys().any(|name| {
4135                    name.as_str().starts_with("x-amz-checksum-")
4136                        || name.as_str().starts_with("x-amz-sdk-checksum-")
4137                }) {
4138                Vec::new()
4139            } else if fail_reader {
4140                vec!["init", "abort"]
4141            } else if size < super::CHUNK_SIZE {
4142                vec!["put"]
4143            } else {
4144                let part_count = size.div_ceil(super::CHUNK_SIZE);
4145                let mut steps = vec!["init"];
4146                steps.extend(std::iter::repeat_n("part", part_count));
4147                steps.push("complete");
4148                steps
4149            };
4150
4151            let deadline = Instant::now() + Duration::from_secs(12);
4152            let mut requests = Vec::new();
4153            for step in expected {
4154                let (mut stream, _) = loop {
4155                    if Instant::now() >= deadline {
4156                        return Err(format!("accept deadline after {} requests", requests.len()));
4157                    }
4158                    match listener.accept() {
4159                        Ok(pair) => break pair,
4160                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
4161                            thread::sleep(Duration::from_millis(5));
4162                        }
4163                        Err(error) => return Err(format!("accept request: {error}")),
4164                    }
4165                };
4166                stream
4167                    .set_nonblocking(false)
4168                    .map_err(|error| error.to_string())?;
4169                stream
4170                    .set_read_timeout(Some(Duration::from_secs(4)))
4171                    .map_err(|error| error.to_string())?;
4172                stream
4173                    .set_write_timeout(Some(Duration::from_secs(4)))
4174                    .map_err(|error| error.to_string())?;
4175                let request = read_request(&mut stream, deadline)?;
4176
4177                let (status, body, etag) = match step {
4178                    "init" => {
4179                        if !request.line.starts_with("POST ") || !request.line.contains("uploads") {
4180                            return Err(format!("unexpected initiation request: {}", request.line));
4181                        }
4182                        (
4183                            200,
4184                            "<InitiateMultipartUploadResult><Bucket>test-bucket</Bucket><Key>multipart-header-test</Key><UploadId>header-upload-id</UploadId></InitiateMultipartUploadResult>",
4185                            "",
4186                        )
4187                    }
4188                    "part" => {
4189                        if !request.line.starts_with("PUT ")
4190                            || !request.line.contains("partNumber=")
4191                        {
4192                            return Err(format!("unexpected part request: {}", request.line));
4193                        }
4194                        (200, "", "ETag: \"part-etag\"\r\n")
4195                    }
4196                    "complete" => {
4197                        if !request.line.starts_with("POST ") || !request.line.contains("uploadId=")
4198                        {
4199                            return Err(format!("unexpected completion request: {}", request.line));
4200                        }
4201                        (200, "<CompleteMultipartUploadResult/>", "")
4202                    }
4203                    "abort" => {
4204                        if !request.line.starts_with("DELETE ")
4205                            || !request.line.contains("uploadId=")
4206                        {
4207                            return Err(format!("unexpected abort request: {}", request.line));
4208                        }
4209                        (204, "", "")
4210                    }
4211                    _ => {
4212                        if !request.line.starts_with("PUT ") || request.line.contains('?') {
4213                            return Err(format!("unexpected small PUT request: {}", request.line));
4214                        }
4215                        (200, "", "ETag: \"small-etag\"\r\n")
4216                    }
4217                };
4218
4219                let response = format!(
4220                    "HTTP/1.1 {status} OK\r\nContent-Length: {}\r\n{etag}Connection: close\r\n\r\n{body}",
4221                    body.len()
4222                );
4223                stream
4224                    .write_all(response.as_bytes())
4225                    .map_err(|error| format!("write response: {error}"))?;
4226                requests.push(request);
4227            }
4228
4229            // The operation signals only after returning, so this also catches
4230            // an unexpected extra request without a timing-based sleep.
4231            loop {
4232                if done_rx.try_recv().is_ok() {
4233                    match listener.accept() {
4234                        Ok((mut stream, _)) => {
4235                            stream
4236                                .set_nonblocking(false)
4237                                .map_err(|error| error.to_string())?;
4238                            stream
4239                                .set_read_timeout(Some(Duration::from_secs(4)))
4240                                .map_err(|error| error.to_string())?;
4241                            requests.push(read_request(&mut stream, deadline)?);
4242                        }
4243                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
4244                        Err(error) => return Err(format!("final accept check: {error}")),
4245                    }
4246                    return Ok(requests);
4247                }
4248                if Instant::now() >= deadline {
4249                    return Err(format!(
4250                        "operation completion deadline after {} requests",
4251                        requests.len()
4252                    ));
4253                }
4254                match listener.accept() {
4255                    Ok((mut stream, _)) => {
4256                        stream
4257                            .set_nonblocking(false)
4258                            .map_err(|error| error.to_string())?;
4259                        stream
4260                            .set_read_timeout(Some(Duration::from_secs(4)))
4261                            .map_err(|error| error.to_string())?;
4262                        let request = read_request(&mut stream, deadline)?;
4263                        requests.push(request);
4264                        return Ok(requests);
4265                    }
4266                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
4267                        thread::sleep(Duration::from_millis(5));
4268                    }
4269                    Err(error) => return Err(format!("accept extra request: {error}")),
4270                }
4271            }
4272        });
4273
4274        let credentials = Credentials::new(
4275            Some("test-access-key"),
4276            Some("test-secret-key"),
4277            None,
4278            None,
4279            None,
4280        )
4281        .unwrap();
4282        let mut bucket = Bucket::new(
4283            "test-bucket",
4284            Region::Custom {
4285                region: "us-east-1".to_owned(),
4286                endpoint,
4287            },
4288            credentials,
4289        )
4290        .unwrap()
4291        .with_path_style();
4292        bucket.add_header("x-amz-meta-uploader", "bucket-level-value");
4293        bucket.add_header("x-amz-request-payer", "owner-level-value");
4294        bucket.add_header("x-test-global-option", "raw-global-value");
4295        let mut reader = MultipartFaultReader {
4296            remaining: size,
4297            fail_at_eof: fail_reader,
4298        };
4299        let result = bucket
4300            .put_object_stream_builder("/multipart-header-test")
4301            .with_content_type("text/plain")
4302            .with_headers(custom_headers)
4303            .execute_stream(&mut reader)
4304            .await
4305            .map(|_| ());
4306        let _ = done_tx.send(());
4307        let server_result = server.join().expect("multipart header server panicked");
4308        (result, server_result)
4309    }
4310
4311    #[cfg(all(
4312        not(feature = "sync"),
4313        any(feature = "with-tokio", feature = "with-async-std")
4314    ))]
4315    fn multipart_test_headers() -> HeaderMap {
4316        let mut headers = HeaderMap::new();
4317        for (name, value) in [
4318            ("x-amz-meta-uploader", "synthetic-test"),
4319            ("content-type", "application/x-custom-overridden"),
4320            ("cache-control", "public, max-age=120"),
4321            ("content-disposition", "attachment; filename=test.bin"),
4322            ("x-amz-storage-class", "STANDARD_IA"),
4323            ("x-amz-server-side-encryption", "aws:kms"),
4324            (
4325                "x-amz-server-side-encryption-aws-kms-key-id",
4326                "synthetic-kms-key",
4327            ),
4328            ("x-test-provider-option", "init-only"),
4329            ("x-amz-server-side-encryption-customer-algorithm", "AES256"),
4330            (
4331                "x-amz-server-side-encryption-customer-key",
4332                "synthetic-key-value",
4333            ),
4334            (
4335                "x-amz-server-side-encryption-customer-key-md5",
4336                "synthetic-key-md5",
4337            ),
4338            ("x-amz-expected-bucket-owner", "123456789012"),
4339            ("x-amz-request-payer", "requester"),
4340            ("if-match", "\"existing-etag\""),
4341            ("if-none-match", "\"other-etag\""),
4342        ] {
4343            headers.insert(
4344                HeaderName::from_bytes(name.as_bytes()).unwrap(),
4345                HeaderValue::from_str(value).unwrap(),
4346            );
4347        }
4348        headers
4349    }
4350
4351    #[cfg(all(
4352        not(feature = "sync"),
4353        any(feature = "with-tokio", feature = "with-async-std")
4354    ))]
4355    fn assert_wire_header(request: &MultipartWireRequest, name: &str, expected: Option<&str>) {
4356        assert_eq!(
4357            request.headers.get(name).map(String::as_str),
4358            expected,
4359            "{} {name}",
4360            request.line
4361        );
4362    }
4363
4364    #[cfg(all(
4365        not(feature = "sync"),
4366        any(feature = "with-tokio", feature = "with-async-std")
4367    ))]
4368    #[maybe_async::maybe_async]
4369    async fn assert_multipart_builder_header_routing() {
4370        let headers = multipart_test_headers();
4371        for size in [
4372            super::CHUNK_SIZE - 1,
4373            super::CHUNK_SIZE,
4374            super::CHUNK_SIZE + 1,
4375        ] {
4376            let (result, server_result) =
4377                run_multipart_header_case(size, false, headers.clone()).await;
4378            assert!(result.is_ok(), "stream upload failed: {result:?}");
4379            let requests = server_result.expect("local HTTP fixture failed");
4380
4381            if size < super::CHUNK_SIZE {
4382                assert_eq!(requests.len(), 1);
4383                let put = &requests[0];
4384                assert!(put.line.starts_with("PUT "));
4385                for name in [
4386                    "x-amz-meta-uploader",
4387                    "cache-control",
4388                    "content-disposition",
4389                    "x-amz-storage-class",
4390                    "x-amz-server-side-encryption",
4391                    "x-amz-server-side-encryption-aws-kms-key-id",
4392                    "x-test-provider-option",
4393                    "x-amz-server-side-encryption-customer-algorithm",
4394                    "x-amz-server-side-encryption-customer-key",
4395                    "x-amz-server-side-encryption-customer-key-md5",
4396                    "x-amz-expected-bucket-owner",
4397                    "x-amz-request-payer",
4398                    "if-match",
4399                    "if-none-match",
4400                ] {
4401                    assert_wire_header(put, name, Some(headers[name].to_str().unwrap()));
4402                }
4403                assert_wire_header(put, "x-test-global-option", Some("raw-global-value"));
4404                assert_wire_header(put, "content-type", Some("text/plain"));
4405                assert_wire_header(put, "content-length", Some(&size.to_string()));
4406                assert!(put.headers.contains_key("content-md5"));
4407                continue;
4408            }
4409
4410            let initiate = requests
4411                .iter()
4412                .find(|request| request.line.contains("uploads"))
4413                .expect("multipart initiation request missing");
4414            assert_wire_header(initiate, "content-type", Some("text/plain"));
4415            assert_wire_header(initiate, "content-length", Some("0"));
4416            for name in [
4417                "x-amz-meta-uploader",
4418                "cache-control",
4419                "content-disposition",
4420                "x-amz-storage-class",
4421                "x-amz-server-side-encryption",
4422                "x-amz-server-side-encryption-aws-kms-key-id",
4423                "x-test-provider-option",
4424                "x-amz-server-side-encryption-customer-algorithm",
4425                "x-amz-server-side-encryption-customer-key",
4426                "x-amz-server-side-encryption-customer-key-md5",
4427                "x-amz-expected-bucket-owner",
4428                "x-amz-request-payer",
4429            ] {
4430                assert_wire_header(initiate, name, Some(headers[name].to_str().unwrap()));
4431            }
4432            assert_wire_header(initiate, "x-test-global-option", Some("raw-global-value"));
4433            for name in ["if-match", "if-none-match"] {
4434                assert_wire_header(initiate, name, None);
4435            }
4436
4437            let parts = requests
4438                .iter()
4439                .filter(|request| request.line.contains("partNumber="))
4440                .collect::<Vec<_>>();
4441            let mut part_numbers_and_lengths = parts
4442                .iter()
4443                .map(|request| {
4444                    let target = request.line.split_whitespace().nth(1).unwrap();
4445                    let query = target.split_once('?').unwrap().1;
4446                    let part_number = query
4447                        .split('&')
4448                        .find_map(|pair| pair.strip_prefix("partNumber="))
4449                        .unwrap()
4450                        .parse::<u32>()
4451                        .unwrap();
4452                    (part_number, request.content_length)
4453                })
4454                .collect::<Vec<_>>();
4455            part_numbers_and_lengths.sort_unstable_by_key(|(part_number, _)| *part_number);
4456            let expected_parts = if size == super::CHUNK_SIZE {
4457                vec![(1, super::CHUNK_SIZE)]
4458            } else {
4459                vec![(1, super::CHUNK_SIZE), (2, 1)]
4460            };
4461            assert_eq!(part_numbers_and_lengths, expected_parts);
4462            for part in parts {
4463                assert_wire_header(part, "content-type", Some("text/plain"));
4464                assert_wire_header(
4465                    part,
4466                    "content-length",
4467                    Some(&part.content_length.to_string()),
4468                );
4469                assert!(part.headers.contains_key("content-md5"));
4470                for name in [
4471                    "x-amz-server-side-encryption-customer-algorithm",
4472                    "x-amz-server-side-encryption-customer-key",
4473                    "x-amz-server-side-encryption-customer-key-md5",
4474                    "x-amz-expected-bucket-owner",
4475                    "x-amz-request-payer",
4476                ] {
4477                    assert_wire_header(part, name, Some(headers[name].to_str().unwrap()));
4478                }
4479                assert_wire_header(part, "x-test-global-option", Some("raw-global-value"));
4480                for name in [
4481                    "x-amz-meta-uploader",
4482                    "cache-control",
4483                    "content-disposition",
4484                    "x-amz-storage-class",
4485                    "x-amz-server-side-encryption",
4486                    "x-amz-server-side-encryption-aws-kms-key-id",
4487                    "x-test-provider-option",
4488                    "if-match",
4489                    "if-none-match",
4490                ] {
4491                    assert_wire_header(part, name, None);
4492                }
4493            }
4494
4495            let complete = requests
4496                .iter()
4497                .find(|request| {
4498                    request.line.starts_with("POST ") && request.line.contains("uploadId=")
4499                })
4500                .expect("multipart completion request missing");
4501            assert_wire_header(complete, "content-type", Some("application/xml"));
4502            assert!(complete.content_length > 0);
4503            for name in [
4504                "x-amz-server-side-encryption-customer-algorithm",
4505                "x-amz-server-side-encryption-customer-key",
4506                "x-amz-server-side-encryption-customer-key-md5",
4507                "x-amz-expected-bucket-owner",
4508                "x-amz-request-payer",
4509                "if-match",
4510                "if-none-match",
4511            ] {
4512                assert_wire_header(complete, name, Some(headers[name].to_str().unwrap()));
4513            }
4514            assert_wire_header(complete, "x-test-global-option", Some("raw-global-value"));
4515            for name in [
4516                "x-amz-meta-uploader",
4517                "cache-control",
4518                "content-disposition",
4519                "x-amz-storage-class",
4520                "x-amz-server-side-encryption",
4521                "x-amz-server-side-encryption-aws-kms-key-id",
4522                "x-test-provider-option",
4523            ] {
4524                assert_wire_header(complete, name, None);
4525            }
4526        }
4527
4528        let (result, server_result) =
4529            run_multipart_header_case(super::CHUNK_SIZE, true, headers.clone()).await;
4530        assert!(matches!(
4531            result,
4532            Err(S3Error::Io(error)) if error.to_string() == "injected reader failure"
4533        ));
4534        let requests = server_result.expect("abort fixture failed");
4535        assert_eq!(requests.len(), 2);
4536        let abort = requests
4537            .iter()
4538            .find(|request| request.line.starts_with("DELETE "))
4539            .expect("multipart abort request missing");
4540        for name in ["x-amz-expected-bucket-owner", "x-amz-request-payer"] {
4541            assert_wire_header(abort, name, Some(headers[name].to_str().unwrap()));
4542        }
4543        assert_wire_header(abort, "x-test-global-option", Some("raw-global-value"));
4544        for name in [
4545            "x-amz-server-side-encryption-customer-algorithm",
4546            "x-amz-server-side-encryption-customer-key",
4547            "x-amz-server-side-encryption-customer-key-md5",
4548            "x-amz-meta-uploader",
4549            "cache-control",
4550            "if-match",
4551            "if-none-match",
4552        ] {
4553            assert_wire_header(abort, name, None);
4554        }
4555        assert!(matches!(
4556            abort.headers.get("content-length").map(String::as_str),
4557            None | Some("0")
4558        ));
4559        assert!(matches!(
4560            abort.headers.get("content-type").map(String::as_str),
4561            None | Some("application/octet-stream")
4562        ));
4563
4564        for rejected_name in [
4565            "content-md5",
4566            "content-length",
4567            "transfer-encoding",
4568            "x-amz-content-sha256",
4569            "x-amz-checksum-sha256",
4570            "x-amz-sdk-checksum-algorithm",
4571        ] {
4572            let mut rejected = HeaderMap::new();
4573            rejected.insert(
4574                HeaderName::from_bytes(rejected_name.as_bytes()).unwrap(),
4575                HeaderValue::from_static("synthetic-value"),
4576            );
4577            let (result, server_result) =
4578                run_multipart_header_case(super::CHUNK_SIZE, false, rejected).await;
4579            assert!(matches!(
4580                result,
4581                Err(S3Error::UnsupportedMultipartHeader(ref name)) if name.as_str() == rejected_name
4582            ));
4583            assert!(
4584                server_result.expect("rejection fixture failed").is_empty(),
4585                "rejected headers must fail before initiation"
4586            );
4587        }
4588    }
4589
4590    #[cfg(all(
4591        not(feature = "sync"),
4592        any(feature = "with-tokio", feature = "with-async-std")
4593    ))]
4594    struct MultipartTrackedReader {
4595        remaining: usize,
4596        bytes_read: std::sync::Arc<std::sync::atomic::AtomicUsize>,
4597    }
4598
4599    #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
4600    impl tokio::io::AsyncRead for MultipartTrackedReader {
4601        fn poll_read(
4602            self: std::pin::Pin<&mut Self>,
4603            _context: &mut std::task::Context<'_>,
4604            buffer: &mut tokio::io::ReadBuf<'_>,
4605        ) -> std::task::Poll<std::io::Result<()>> {
4606            use std::sync::atomic::Ordering;
4607            let this = self.get_mut();
4608            let count = this.remaining.min(buffer.remaining());
4609            let offset = this.bytes_read.fetch_add(count, Ordering::SeqCst);
4610            for (index, byte) in buffer.initialize_unfilled()[..count].iter_mut().enumerate() {
4611                *byte = ((offset + index) % 251) as u8;
4612            }
4613            buffer.advance(count);
4614            this.remaining -= count;
4615            std::task::Poll::Ready(Ok(()))
4616        }
4617    }
4618
4619    #[cfg(all(not(feature = "sync"), feature = "with-async-std"))]
4620    impl async_std::io::Read for MultipartTrackedReader {
4621        fn poll_read(
4622            self: std::pin::Pin<&mut Self>,
4623            _context: &mut std::task::Context<'_>,
4624            buffer: &mut [u8],
4625        ) -> std::task::Poll<std::io::Result<usize>> {
4626            use std::sync::atomic::Ordering;
4627            let this = self.get_mut();
4628            let count = this.remaining.min(buffer.len());
4629            let offset = this.bytes_read.fetch_add(count, Ordering::SeqCst);
4630            for (index, byte) in buffer[..count].iter_mut().enumerate() {
4631                *byte = ((offset + index) % 251) as u8;
4632            }
4633            this.remaining -= count;
4634            std::task::Poll::Ready(Ok(count))
4635        }
4636    }
4637
4638    #[cfg(all(
4639        not(feature = "sync"),
4640        any(feature = "with-tokio", feature = "with-async-std")
4641    ))]
4642    #[maybe_async::maybe_async]
4643    async fn run_multipart_builder_success_case(
4644        max_concurrent_chunks: usize,
4645    ) -> Result<(), String> {
4646        use std::io::{Read, Write};
4647        use std::net::{TcpListener, TcpStream};
4648        use std::sync::atomic::{AtomicUsize, Ordering};
4649        use std::sync::{Arc, mpsc};
4650        use std::thread;
4651        use std::time::{Duration, Instant};
4652
4653        const TAIL: usize = 17;
4654        let total = 3 * super::CHUNK_SIZE + TAIL;
4655        let bytes_read = Arc::new(AtomicUsize::new(0));
4656        let server_bytes_read = Arc::clone(&bytes_read);
4657        let listener = TcpListener::bind("127.0.0.1:0").map_err(|error| error.to_string())?;
4658        listener
4659            .set_nonblocking(true)
4660            .map_err(|error| error.to_string())?;
4661        let endpoint = format!(
4662            "http://{}",
4663            listener.local_addr().map_err(|error| error.to_string())?
4664        );
4665        let (done_tx, done_rx) = mpsc::channel::<String>();
4666        let server = thread::spawn(move || -> Result<(), String> {
4667            fn accept_until(
4668                listener: &TcpListener,
4669                done_rx: &mpsc::Receiver<String>,
4670                deadline: Instant,
4671            ) -> Result<TcpStream, String> {
4672                loop {
4673                    match listener.accept() {
4674                        Ok((stream, _)) => {
4675                            stream.set_nonblocking(false).map_err(|e| e.to_string())?;
4676                            stream
4677                                .set_read_timeout(Some(Duration::from_secs(3)))
4678                                .map_err(|e| e.to_string())?;
4679                            stream
4680                                .set_write_timeout(Some(Duration::from_secs(3)))
4681                                .map_err(|e| e.to_string())?;
4682                            return Ok(stream);
4683                        }
4684                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
4685                            if let Ok(result) = done_rx.try_recv() {
4686                                return Err(format!(
4687                                    "client finished before expected request: {result}"
4688                                ));
4689                            }
4690                            if Instant::now() >= deadline {
4691                                return Err("multipart server accept deadline".to_owned());
4692                            }
4693                            thread::sleep(Duration::from_millis(2));
4694                        }
4695                        Err(error) => return Err(error.to_string()),
4696                    }
4697                }
4698            }
4699            fn read_request(
4700                stream: &mut TcpStream,
4701                deadline: Instant,
4702            ) -> Result<(String, Vec<u8>), String> {
4703                let mut headers = Vec::new();
4704                let mut byte = [0];
4705                while !headers.ends_with(b"\r\n\r\n") {
4706                    if headers.len() > 16 * 1024 || Instant::now() >= deadline {
4707                        return Err("request header bound or deadline exceeded".to_owned());
4708                    }
4709                    stream.read_exact(&mut byte).map_err(|e| e.to_string())?;
4710                    headers.push(byte[0]);
4711                }
4712                let text = String::from_utf8_lossy(&headers);
4713                let line = text.lines().next().unwrap_or_default().to_owned();
4714                let len = text
4715                    .lines()
4716                    .find_map(|line| {
4717                        line.to_ascii_lowercase()
4718                            .strip_prefix("content-length:")
4719                            .map(str::to_owned)
4720                    })
4721                    .and_then(|v| v.trim().parse::<usize>().ok())
4722                    .unwrap_or(0);
4723                if len > super::CHUNK_SIZE + 64 * 1024 {
4724                    return Err("request body exceeded bound".to_owned());
4725                }
4726                let mut body = vec![0; len];
4727                stream.read_exact(&mut body).map_err(|e| e.to_string())?;
4728                Ok((line, body))
4729            }
4730            fn respond(
4731                stream: &mut TcpStream,
4732                status: u16,
4733                body: &str,
4734                etag: Option<&str>,
4735            ) -> Result<(), String> {
4736                let tag = etag
4737                    .map(|value| format!("ETag: \"{value}\"\r\n"))
4738                    .unwrap_or_default();
4739                write!(stream, "HTTP/1.1 {status} OK\r\nContent-Length: {}\r\n{tag}Connection: close\r\n\r\n{body}", body.len()).map_err(|e| e.to_string())
4740            }
4741
4742            let deadline = Instant::now() + Duration::from_secs(20);
4743            let mut init = accept_until(&listener, &done_rx, deadline)?;
4744            let (line, _) = read_request(&mut init, deadline)?;
4745            if !line.starts_with("POST ") || !line.contains("uploads") {
4746                return Err(format!("unexpected init: {line}"));
4747            }
4748            respond(
4749                &mut init,
4750                200,
4751                "<InitiateMultipartUploadResult><Bucket>test-bucket</Bucket><Key>multipart-test</Key><UploadId>upload-id</UploadId></InitiateMultipartUploadResult>",
4752                None,
4753            )?;
4754
4755            fn number(line: &str) -> Result<usize, String> {
4756                line.split("partNumber=")
4757                    .nth(1)
4758                    .and_then(|value| value.split('&').next())
4759                    .and_then(|value| value.parse().ok())
4760                    .ok_or_else(|| format!("missing part number in {line}"))
4761            }
4762            fn verify_part(number: usize, body: &[u8]) -> Result<(), String> {
4763                let expected_len = if number == 4 { TAIL } else { super::CHUNK_SIZE };
4764                if body.len() != expected_len {
4765                    return Err(format!(
4766                        "part {number} length {} != {expected_len}",
4767                        body.len()
4768                    ));
4769                }
4770                let offset = (number - 1) * super::CHUNK_SIZE;
4771                if body
4772                    .iter()
4773                    .enumerate()
4774                    .any(|(index, byte)| *byte != ((offset + index) % 251) as u8)
4775                {
4776                    return Err(format!("part {number} bytes did not match source offsets"));
4777                }
4778                Ok(())
4779            }
4780            let mut parts = Vec::new();
4781            for _ in 0..max_concurrent_chunks {
4782                let mut stream = accept_until(&listener, &done_rx, deadline)?;
4783                let (line, body) = read_request(&mut stream, deadline)?;
4784                let part_number = number(&line)?;
4785                if part_number > max_concurrent_chunks {
4786                    return Err(format!(
4787                        "part {part_number} arrived beyond cap {max_concurrent_chunks}"
4788                    ));
4789                }
4790                verify_part(part_number, &body)?;
4791                let observed = server_bytes_read.load(Ordering::SeqCst);
4792                if observed > max_concurrent_chunks * super::CHUNK_SIZE {
4793                    return Err(format!(
4794                        "reader exceeded cap before response: {observed} bytes"
4795                    ));
4796                }
4797                parts.push((part_number, stream));
4798            }
4799
4800            if max_concurrent_chunks == 2 {
4801                let index = parts
4802                    .iter()
4803                    .position(|(part, _)| *part == 2)
4804                    .ok_or_else(|| "part 2 not received".to_owned())?;
4805                respond(&mut parts[index].1, 200, "", Some("etag-2"))?;
4806            } else {
4807                respond(&mut parts[0].1, 200, "", Some("etag-1"))?;
4808                parts.clear();
4809            }
4810
4811            let start = if max_concurrent_chunks == 2 { 3 } else { 2 };
4812            let mut acknowledged_bytes = super::CHUNK_SIZE;
4813            for part_number in start..=4 {
4814                let mut stream = accept_until(&listener, &done_rx, deadline)?;
4815                let (line, body) = read_request(&mut stream, deadline)?;
4816                let received_number = number(&line)?;
4817                if received_number != part_number {
4818                    return Err(format!("expected part {part_number}, got {line}"));
4819                }
4820                verify_part(part_number, &body)?;
4821                let allowed = acknowledged_bytes + max_concurrent_chunks * super::CHUNK_SIZE;
4822                let observed = server_bytes_read.load(Ordering::SeqCst);
4823                if observed > allowed.min(total) {
4824                    return Err(format!(
4825                        "reader exceeded concurrency cap at part {part_number}: read {observed}, allowed {allowed}"
4826                    ));
4827                }
4828                respond(&mut stream, 200, "", Some(&format!("etag-{part_number}")))?;
4829                acknowledged_bytes += body.len();
4830            }
4831            if max_concurrent_chunks == 2 {
4832                // Release part 1 last to force out-of-order completion.
4833                let index = parts
4834                    .iter()
4835                    .position(|(part, _)| *part == 1)
4836                    .ok_or_else(|| "part 1 not received".to_owned())?;
4837                respond(&mut parts[index].1, 200, "", Some("etag-1"))?;
4838            }
4839
4840            let mut completion = accept_until(&listener, &done_rx, deadline)?;
4841            let (line, body) = read_request(&mut completion, deadline)?;
4842            if !line.contains("uploadId=upload-id") {
4843                return Err(format!("unexpected completion: {line}"));
4844            }
4845            let xml = String::from_utf8(body).map_err(|e| e.to_string())?;
4846            let expected_blocks = (1..=4)
4847                .map(|part| {
4848                    format!(
4849                        "<Part><PartNumber>{part}</PartNumber><ETag>\"etag-{part}\"</ETag></Part>"
4850                    )
4851                })
4852                .collect::<String>();
4853            if !xml.contains(&expected_blocks) {
4854                return Err(format!("completion parts were not ordered: {xml}"));
4855            }
4856            respond(
4857                &mut completion,
4858                200,
4859                "<CompleteMultipartUploadResult/>",
4860                None,
4861            )?;
4862            Ok(())
4863        });
4864
4865        let credentials = Credentials::new(Some("access"), Some("secret"), None, None, None)
4866            .map_err(|e| e.to_string())?;
4867        let bucket = Bucket::new(
4868            "test-bucket",
4869            Region::Custom {
4870                region: "us-east-1".to_owned(),
4871                endpoint,
4872            },
4873            credentials,
4874        )
4875        .map_err(|e| e.to_string())?
4876        .with_path_style()
4877        .with_request_timeout(Duration::from_secs(5))
4878        .map_err(|e| e.to_string())?;
4879        let mut reader = MultipartTrackedReader {
4880            remaining: total,
4881            bytes_read,
4882        };
4883        let result = bucket
4884            .put_object_stream_builder("/multipart-test")
4885            .with_max_concurrent_chunks(max_concurrent_chunks)
4886            .execute_stream(&mut reader)
4887            .await;
4888        let _ = done_tx.send(format!("{result:?}"));
4889        let server_result = server
4890            .join()
4891            .map_err(|_| "multipart server panicked".to_owned())?;
4892        server_result?;
4893        let response = result.map_err(|error| format!("upload failed: {error:?}"))?;
4894        if response.uploaded_bytes() != total {
4895            return Err(format!(
4896                "uploaded byte count {} != {total}",
4897                response.uploaded_bytes()
4898            ));
4899        }
4900        Ok(())
4901    }
4902
4903    #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
4904    #[tokio::test]
4905    async fn multipart_stream_builder_routes_headers_by_operation() {
4906        assert_multipart_builder_header_routing().await;
4907    }
4908
4909    #[cfg(all(
4910        not(feature = "sync"),
4911        not(feature = "with-tokio"),
4912        feature = "with-async-std"
4913    ))]
4914    #[async_std::test]
4915    async fn multipart_stream_builder_routes_headers_by_operation() {
4916        assert_multipart_builder_header_routing().await;
4917    }
4918
4919    #[allow(dead_code)]
4920    #[derive(Clone, Copy, Debug)]
4921    enum MultipartFailureCase {
4922        Reader,
4923        Part,
4924        Completion,
4925        CompletionEmbeddedError,
4926        Abort,
4927        SmallPut,
4928    }
4929
4930    struct MultipartFaultReader {
4931        remaining: usize,
4932        fail_at_eof: bool,
4933    }
4934
4935    impl MultipartFaultReader {
4936        fn read_into(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
4937            if self.remaining == 0 {
4938                return if self.fail_at_eof {
4939                    Err(std::io::Error::other("injected reader failure"))
4940                } else {
4941                    Ok(0)
4942                };
4943            }
4944            let read = self.remaining.min(buffer.len());
4945            buffer[..read].fill(b'x');
4946            self.remaining -= read;
4947            Ok(read)
4948        }
4949    }
4950
4951    impl std::io::Read for MultipartFaultReader {
4952        fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
4953            self.read_into(buffer)
4954        }
4955    }
4956
4957    #[cfg(feature = "with-tokio")]
4958    impl tokio::io::AsyncRead for MultipartFaultReader {
4959        fn poll_read(
4960            self: std::pin::Pin<&mut Self>,
4961            _context: &mut std::task::Context<'_>,
4962            buffer: &mut tokio::io::ReadBuf<'_>,
4963        ) -> std::task::Poll<std::io::Result<()>> {
4964            let this = self.get_mut();
4965            match this.read_into(buffer.initialize_unfilled()) {
4966                Ok(read) => {
4967                    buffer.advance(read);
4968                    std::task::Poll::Ready(Ok(()))
4969                }
4970                Err(error) => std::task::Poll::Ready(Err(error)),
4971            }
4972        }
4973    }
4974
4975    #[cfg(feature = "with-async-std")]
4976    impl async_std::io::Read for MultipartFaultReader {
4977        fn poll_read(
4978            self: std::pin::Pin<&mut Self>,
4979            _context: &mut std::task::Context<'_>,
4980            buffer: &mut [u8],
4981        ) -> std::task::Poll<std::io::Result<usize>> {
4982            std::task::Poll::Ready(self.get_mut().read_into(buffer))
4983        }
4984    }
4985
4986    #[maybe_async::maybe_async]
4987    async fn run_multipart_failure_case(
4988        case: MultipartFailureCase,
4989        max_concurrent_chunks: Option<usize>,
4990    ) -> (Result<u16, S3Error>, Result<Vec<String>, String>) {
4991        use super::CHUNK_SIZE;
4992        use std::io::{Read, Write};
4993        use std::net::TcpListener;
4994        use std::sync::mpsc;
4995        use std::thread;
4996        use std::time::{Duration, Instant};
4997
4998        fn read_request(
4999            stream: &mut std::net::TcpStream,
5000            request_index: usize,
5001            deadline: Instant,
5002        ) -> Result<(String, usize), String> {
5003            let mut header_bytes = Vec::new();
5004            let mut byte = [0u8; 1];
5005            while !header_bytes.ends_with(b"\r\n\r\n") {
5006                if header_bytes.len() >= 16 * 1024 || Instant::now() >= deadline {
5007                    return Err(format!("header bounds exceeded at request {request_index}"));
5008                }
5009                stream
5010                    .read_exact(&mut byte)
5011                    .map_err(|error| format!("read headers at {request_index}: {error}"))?;
5012                header_bytes.push(byte[0]);
5013            }
5014            let header_text = String::from_utf8_lossy(&header_bytes);
5015            let request_line = header_text.lines().next().unwrap_or_default().to_owned();
5016            let content_length = header_text
5017                .lines()
5018                .find_map(|line| {
5019                    line.to_ascii_lowercase()
5020                        .strip_prefix("content-length:")
5021                        .map(str::to_owned)
5022                })
5023                .and_then(|value| value.trim().parse::<usize>().ok())
5024                .unwrap_or(0);
5025            if content_length > CHUNK_SIZE + 64 * 1024 {
5026                return Err(format!("request body exceeded cap at {request_index}"));
5027            }
5028            let mut request_body = vec![0u8; content_length];
5029            stream
5030                .read_exact(&mut request_body)
5031                .map_err(|error| format!("read body at {request_index}: {error}"))?;
5032            Ok((request_line, content_length))
5033        }
5034
5035        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
5036        listener.set_nonblocking(true).unwrap();
5037        let endpoint = format!("http://{}", listener.local_addr().unwrap());
5038        let (done_tx, done_rx) = mpsc::channel::<String>();
5039        let server = thread::spawn(move || {
5040            let init_body = "<InitiateMultipartUploadResult><Bucket>test-bucket</Bucket><Key>multipart-test</Key><UploadId>upload-id</UploadId></InitiateMultipartUploadResult>";
5041            let part_failure = "part failure";
5042            let completion_failure = "completion failure";
5043            let completion_embedded_error =
5044                "<Error><Code>InternalError</Code><RequestId>completion-req</RequestId></Error>";
5045            let abort_failure = "abort failure";
5046            let put_failure = "fallback put failure";
5047            let failure_attempts = if cfg!(feature = "fail-on-err") {
5048                crate::get_retries() as usize + 1
5049            } else {
5050                1
5051            };
5052            let mut steps = vec![("POST ", "uploads", 200, init_body, false)];
5053            match case {
5054                MultipartFailureCase::Reader | MultipartFailureCase::Abort => {
5055                    if cfg!(feature = "sync") || max_concurrent_chunks == Some(1) {
5056                        steps.push(("PUT ", "partNumber=1", 200, "", true));
5057                    }
5058                    let (status, body) = if matches!(case, MultipartFailureCase::Abort) {
5059                        (500, abort_failure)
5060                    } else {
5061                        (204, "")
5062                    };
5063                    // Cleanup is an ambiguous state-changing request and is never replayed.
5064                    steps.push(("DELETE ", "uploadId=upload-id", status, body, false));
5065                }
5066                MultipartFailureCase::SmallPut => {
5067                    steps.push(("DELETE ", "uploadId=upload-id", 204, "", false));
5068                    // Ordinary PUT is also not replayed after an ambiguous failure.
5069                    steps.push(("PUT ", "multipart-test", 500, put_failure, false));
5070                }
5071                MultipartFailureCase::Part => {
5072                    for _ in 0..failure_attempts {
5073                        steps.push(("PUT ", "partNumber=1", 500, part_failure, false));
5074                    }
5075                    steps.push(("DELETE ", "uploadId=upload-id", 204, "", false));
5076                }
5077                MultipartFailureCase::Completion
5078                | MultipartFailureCase::CompletionEmbeddedError => {
5079                    steps.push(("PUT ", "partNumber=1", 200, "", true));
5080                    if cfg!(feature = "sync") {
5081                        steps.push(("PUT ", "partNumber=2", 200, "", true));
5082                    }
5083                    let (status, body) = if matches!(case, MultipartFailureCase::Completion) {
5084                        (500, completion_failure)
5085                    } else {
5086                        (200, completion_embedded_error)
5087                    };
5088                    // Completion may have succeeded remotely even when its response failed.
5089                    steps.push(("POST ", "uploadId=upload-id", status, body, false));
5090                    steps.push(("DELETE ", "uploadId=upload-id", 204, "", false));
5091                }
5092            }
5093            let deadline = Instant::now() + Duration::from_secs(10);
5094            let mut requests = Vec::new();
5095
5096            for (step_index, (expected_method, expected_query, status, body, with_etag)) in
5097                steps.into_iter().enumerate()
5098            {
5099                let (mut stream, _) = loop {
5100                    match listener.accept() {
5101                        Ok(pair) => break pair,
5102                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5103                            if let Ok(client_result) = done_rx.try_recv() {
5104                                return Err(format!(
5105                                    "case {case:?}: client completed before request {} (expected {expected_method}...{expected_query}) after {} requests; client result: {client_result}",
5106                                    step_index + 1,
5107                                    requests.len(),
5108                                ));
5109                            }
5110                            if Instant::now() >= deadline {
5111                                return Err(format!(
5112                                    "case {case:?}: accept deadline after {} requests; awaiting request {} expected {expected_method}...{expected_query}",
5113                                    requests.len(),
5114                                    step_index + 1,
5115                                ));
5116                            }
5117                            thread::sleep(Duration::from_millis(5));
5118                        }
5119                        Err(error) => {
5120                            return Err(format!(
5121                                "case {case:?}: accept failed before request {} expected {expected_method}...{expected_query}: {error}",
5122                                step_index + 1,
5123                            ));
5124                        }
5125                    }
5126                };
5127                stream
5128                    .set_nonblocking(false)
5129                    .map_err(|error| format!("set blocking: {error}"))?;
5130                stream
5131                    .set_read_timeout(Some(Duration::from_secs(3)))
5132                    .map_err(|error| format!("set read timeout: {error}"))?;
5133                stream
5134                    .set_write_timeout(Some(Duration::from_secs(3)))
5135                    .map_err(|error| format!("set write timeout: {error}"))?;
5136                let (request_line, _) = read_request(&mut stream, requests.len() + 1, deadline)
5137                    .map_err(|error| {
5138                        format!(
5139                            "case {case:?}: reading request {} expected {expected_method}...{expected_query}: {error}",
5140                            step_index + 1,
5141                        )
5142                    })?;
5143                if !request_line.starts_with(expected_method)
5144                    || !request_line.contains(expected_query)
5145                {
5146                    return Err(format!(
5147                        "case {case:?}: unexpected request {} while expecting {expected_method}...{expected_query}: received {request_line:?}",
5148                        step_index + 1,
5149                    ));
5150                }
5151                let reason = if (200..300).contains(&status) {
5152                    "OK"
5153                } else {
5154                    "Internal Server Error"
5155                };
5156                let etag = if with_etag {
5157                    "ETag: \"part-etag\"\r\n"
5158                } else {
5159                    ""
5160                };
5161                let response = format!(
5162                    "HTTP/1.1 {status} {reason}\r\nContent-Length: {}\r\n{etag}Connection: close\r\n\r\n{body}",
5163                    body.len()
5164                );
5165                stream.write_all(response.as_bytes()).map_err(|error| {
5166                    format!(
5167                        "case {case:?}: writing response for request {} expected {expected_method}...{expected_query}: {error}",
5168                        step_index + 1,
5169                    )
5170                })?;
5171                requests.push(request_line);
5172            }
5173
5174            // Keep accepting until the client signals its call has returned;
5175            // this detects an unexpected duplicate abort without a fixed sleep.
5176            loop {
5177                match listener.accept() {
5178                    Ok((mut stream, _)) => {
5179                        stream
5180                            .set_nonblocking(false)
5181                            .map_err(|error| format!("set blocking for extra request: {error}"))?;
5182                        stream
5183                            .set_read_timeout(Some(Duration::from_secs(3)))
5184                            .map_err(|error| format!("set extra read timeout: {error}"))?;
5185                        stream
5186                            .set_write_timeout(Some(Duration::from_secs(3)))
5187                            .map_err(|error| format!("set extra write timeout: {error}"))?;
5188                        let (request_line, _) = read_request(
5189                            &mut stream,
5190                            requests.len() + 1,
5191                            deadline,
5192                        )
5193                        .map_err(|error| {
5194                            format!(
5195                                "case {case:?}: reading unexpected request after {} expected requests: {error}",
5196                                requests.len(),
5197                            )
5198                        })?;
5199                        requests.push(format!("unexpected extra request: {request_line}"));
5200                        stream
5201                            .write_all(b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
5202                            .map_err(|error| format!("write extra response: {error}"))?;
5203                    }
5204                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5205                        if done_rx.try_recv().is_ok() {
5206                            return Ok(requests);
5207                        }
5208                        if Instant::now() >= deadline {
5209                            return Err(format!(
5210                                "case {case:?}: post-request deadline after {} requests while awaiting client completion",
5211                                requests.len()
5212                            ));
5213                        }
5214                        thread::sleep(Duration::from_millis(5));
5215                    }
5216                    Err(error) => {
5217                        return Err(format!(
5218                            "case {case:?}: accept unexpected request after {} expected requests failed: {error}",
5219                            requests.len(),
5220                        ));
5221                    }
5222                }
5223            }
5224        });
5225
5226        let credentials = Credentials::new(
5227            Some("test_access_key"),
5228            Some("test_secret_key"),
5229            None,
5230            None,
5231            None,
5232        )
5233        .unwrap();
5234        let bucket = Bucket::new(
5235            "test-bucket",
5236            Region::Custom {
5237                region: "us-east-1".to_owned(),
5238                endpoint,
5239            },
5240            credentials,
5241        )
5242        .unwrap()
5243        .with_path_style()
5244        .with_request_timeout(Duration::from_secs(4))
5245        .unwrap();
5246        let mut reader = MultipartFaultReader {
5247            remaining: if matches!(case, MultipartFailureCase::SmallPut) {
5248                1
5249            } else {
5250                CHUNK_SIZE
5251            },
5252            fail_at_eof: matches!(
5253                case,
5254                MultipartFailureCase::Reader | MultipartFailureCase::Abort
5255            ),
5256        };
5257        #[cfg(feature = "sync")]
5258        let _ = max_concurrent_chunks;
5259        #[cfg(feature = "sync")]
5260        let result = bucket
5261            .put_object_stream_with_content_type(
5262                &mut reader,
5263                "/multipart-test",
5264                "application/octet-stream",
5265            )
5266            .await;
5267        #[cfg(not(feature = "sync"))]
5268        let result = if let Some(limit) = max_concurrent_chunks {
5269            bucket
5270                .put_object_stream_builder("/multipart-test")
5271                .with_content_type("application/octet-stream")
5272                .with_max_concurrent_chunks(limit)
5273                .execute_stream(&mut reader)
5274                .await
5275        } else {
5276            bucket
5277                .put_object_stream_with_content_type(
5278                    &mut reader,
5279                    "/multipart-test",
5280                    "application/octet-stream",
5281                )
5282                .await
5283        };
5284        #[cfg(not(feature = "sync"))]
5285        let result = result.map(|response| response.status_code());
5286        let _ = done_tx.send(format!("{result:?}"));
5287        let server_result = server.join().expect("mock multipart server panicked");
5288        (result, server_result)
5289    }
5290
5291    #[maybe_async::test(
5292        feature = "sync",
5293        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
5294        async(
5295            all(not(feature = "sync"), feature = "with-async-std"),
5296            async_std::test
5297        )
5298    )]
5299    async fn multipart_stream_errors_attempt_abort_and_preserve_primary_error() {
5300        let failure_attempts = if cfg!(feature = "fail-on-err") {
5301            crate::get_retries() as usize + 1
5302        } else {
5303            1
5304        };
5305        for case in [
5306            MultipartFailureCase::Reader,
5307            MultipartFailureCase::Part,
5308            MultipartFailureCase::Completion,
5309            MultipartFailureCase::CompletionEmbeddedError,
5310            MultipartFailureCase::Abort,
5311            #[cfg(feature = "sync")]
5312            MultipartFailureCase::SmallPut,
5313        ] {
5314            let (result, requests) = run_multipart_failure_case(case, None).await;
5315            let requests = requests.unwrap_or_else(|server_error| {
5316                panic!(
5317                    "{case:?}: mock server request sequence failed; client result: {result:?}; {server_error}"
5318                )
5319            });
5320            let expected_requests = match case {
5321                MultipartFailureCase::Reader => 2 + usize::from(cfg!(feature = "sync")),
5322                MultipartFailureCase::Part => 2 + failure_attempts,
5323                MultipartFailureCase::Completion => 4 + usize::from(cfg!(feature = "sync")),
5324                MultipartFailureCase::CompletionEmbeddedError => {
5325                    4 + usize::from(cfg!(feature = "sync"))
5326                }
5327                MultipartFailureCase::Abort => 2 + usize::from(cfg!(feature = "sync")),
5328                MultipartFailureCase::SmallPut => 3,
5329            };
5330            assert_eq!(requests.len(), expected_requests);
5331            if !matches!(case, MultipartFailureCase::SmallPut) {
5332                assert!(result.is_err(), "{case:?} should return an error");
5333            }
5334
5335            match case {
5336                MultipartFailureCase::Reader | MultipartFailureCase::Abort => {
5337                    assert!(
5338                        matches!(result, Err(S3Error::Io(error)) if error.to_string() == "injected reader failure")
5339                    );
5340                }
5341                MultipartFailureCase::Part => {
5342                    assert!(matches!(
5343                        result,
5344                        Err(S3Error::HttpFail | S3Error::HttpFailWithBody(500, _))
5345                    ));
5346                }
5347                MultipartFailureCase::Completion => match result {
5348                    Err(S3Error::HttpFailWithBody(500, body)) => {
5349                        assert_eq!(body, "completion failure");
5350                    }
5351                    Err(S3Error::HttpFail) if cfg!(feature = "fail-on-err") => {}
5352                    other => panic!("completion failure was not preserved: {other:?}"),
5353                },
5354                MultipartFailureCase::CompletionEmbeddedError => {
5355                    assert!(
5356                        matches!(result, Err(S3Error::HttpFailWithBody(200, body)) if body.contains("completion-req"))
5357                    );
5358                }
5359                MultipartFailureCase::SmallPut => {
5360                    if cfg!(feature = "fail-on-err") {
5361                        assert!(matches!(
5362                            result,
5363                            Err(S3Error::HttpFail | S3Error::HttpFailWithBody(500, _))
5364                        ));
5365                    } else {
5366                        assert!(matches!(result, Ok(500)));
5367                    }
5368                }
5369            }
5370        }
5371    }
5372
5373    #[cfg(all(
5374        not(feature = "sync"),
5375        any(feature = "with-tokio", feature = "with-async-std")
5376    ))]
5377    #[maybe_async::test(
5378        feature = "sync",
5379        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
5380        async(
5381            all(not(feature = "sync"), feature = "with-async-std"),
5382            async_std::test
5383        )
5384    )]
5385    async fn multipart_stream_builder_limits_preserve_abort_errors() {
5386        let failure_attempts = if cfg!(feature = "fail-on-err") {
5387            crate::get_retries() as usize + 1
5388        } else {
5389            1
5390        };
5391        for (limit, case, expected_requests) in [
5392            (1, MultipartFailureCase::Reader, 3),
5393            (1, MultipartFailureCase::Abort, 3),
5394            (1, MultipartFailureCase::Part, 2 + failure_attempts),
5395            (1, MultipartFailureCase::Completion, 4),
5396            (1, MultipartFailureCase::CompletionEmbeddedError, 4),
5397            (2, MultipartFailureCase::Reader, 2),
5398            (2, MultipartFailureCase::Abort, 2),
5399            (2, MultipartFailureCase::Part, 2 + failure_attempts),
5400            (2, MultipartFailureCase::Completion, 4),
5401            (2, MultipartFailureCase::CompletionEmbeddedError, 4),
5402        ] {
5403            let (result, requests) = run_multipart_failure_case(case, Some(limit)).await;
5404            let requests = requests.unwrap_or_else(|server_error| {
5405                panic!("limit {limit} {case:?}: client result {result:?}; {server_error}")
5406            });
5407            assert_eq!(requests.len(), expected_requests, "limit {limit} {case:?}");
5408            assert!(result.is_err(), "limit {limit} {case:?} should fail");
5409            match case {
5410                MultipartFailureCase::Reader | MultipartFailureCase::Abort => assert!(
5411                    matches!(result, Err(S3Error::Io(error)) if error.to_string() == "injected reader failure")
5412                ),
5413                MultipartFailureCase::Part => assert!(matches!(
5414                    result,
5415                    Err(S3Error::HttpFail | S3Error::HttpFailWithBody(500, _))
5416                )),
5417                MultipartFailureCase::Completion => match result {
5418                    Err(S3Error::HttpFailWithBody(500, body)) => {
5419                        assert_eq!(body, "completion failure")
5420                    }
5421                    Err(S3Error::HttpFail) if cfg!(feature = "fail-on-err") => {}
5422                    other => panic!("limit {limit}: completion error was not preserved: {other:?}"),
5423                },
5424                MultipartFailureCase::CompletionEmbeddedError => assert!(
5425                    matches!(result, Err(S3Error::HttpFailWithBody(200, body)) if body.contains("completion-req"))
5426                ),
5427                MultipartFailureCase::SmallPut => unreachable!(),
5428            }
5429        }
5430    }
5431
5432    #[cfg(all(
5433        not(feature = "sync"),
5434        any(feature = "with-tokio", feature = "with-async-std")
5435    ))]
5436    #[maybe_async::test(
5437        feature = "sync",
5438        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
5439        async(
5440            all(not(feature = "sync"), feature = "with-async-std"),
5441            async_std::test
5442        )
5443    )]
5444    async fn multipart_stream_builder_limits_bound_and_order_parts() {
5445        for limit in [1, 2] {
5446            run_multipart_builder_success_case(limit)
5447                .await
5448                .unwrap_or_else(|error| panic!("limit {limit}: {error}"));
5449        }
5450    }
5451
5452    #[test]
5453    fn accepted_mock_stream_blocks_until_delayed_client_byte() {
5454        use std::io::{Read, Write};
5455        use std::net::{TcpListener, TcpStream};
5456        use std::sync::mpsc;
5457        use std::thread;
5458        use std::time::{Duration, Instant};
5459
5460        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
5461        listener.set_nonblocking(true).unwrap();
5462        let address = listener.local_addr().unwrap();
5463        let (accepted_tx, accepted_rx) = mpsc::sync_channel(0);
5464        let (read_result_tx, read_result_rx) = mpsc::channel();
5465        let server = thread::spawn(move || {
5466            let deadline = Instant::now() + Duration::from_secs(2);
5467            let (stream, _) = loop {
5468                match listener.accept() {
5469                    Ok(pair) => break pair,
5470                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5471                        assert!(Instant::now() < deadline, "mock accept timed out");
5472                        thread::sleep(Duration::from_millis(2));
5473                    }
5474                    Err(error) => panic!("mock accept failed: {error}"),
5475                }
5476            };
5477            let mut stream = make_mock_stream_blocking(stream).unwrap();
5478            stream
5479                .set_read_timeout(Some(Duration::from_secs(1)))
5480                .unwrap();
5481            accepted_tx.send(()).unwrap();
5482            let mut byte = [0];
5483            read_result_tx
5484                .send(stream.read_exact(&mut byte).map(|()| byte[0]))
5485                .unwrap();
5486        });
5487
5488        let mut client = TcpStream::connect(address).unwrap();
5489        accepted_rx
5490            .recv_timeout(Duration::from_secs(1))
5491            .expect("server did not accept the client");
5492        assert!(matches!(
5493            read_result_rx.recv_timeout(Duration::from_millis(50)),
5494            Err(mpsc::RecvTimeoutError::Timeout)
5495        ));
5496        client.write_all(b"x").unwrap();
5497        assert_eq!(
5498            read_result_rx
5499                .recv_timeout(Duration::from_secs(1))
5500                .expect("server did not finish reading")
5501                .unwrap(),
5502            b'x'
5503        );
5504        server.join().unwrap();
5505    }
5506
5507    #[test]
5508    fn xml_response_embedded_error_rejects_invalid_or_unexpected_documents() {
5509        for body in [
5510            "",
5511            "<CopyObjectResult>",
5512            "<CopyObjectResult></CopyObjectResult><Extra/>",
5513            "<CopyObjectResult></CopyObjectResult>trailing",
5514            "<CopyObjectResult/>&amp;",
5515            "<CopyObjectResult><A></CopyObjectResult>",
5516            "<CopyObjectResult A=\"unterminated></CopyObjectResult>",
5517            "<CopyObjectResult>&unknown;</CopyObjectResult>",
5518            "<?xml version=\"1.0\"?><?xml version=\"1.0\"?><CopyObjectResult/>",
5519            "<?xml bogus?><CopyObjectResult/>",
5520            "<?xml version=\"1.0\"?><CopyObjectResult><?xml version=\"1.0\"?></CopyObjectResult>",
5521            "<CopyObjectResult><!-- invalid -- comment --></CopyObjectResult>",
5522        ] {
5523            assert!(
5524                matches!(
5525                    validate_success_xml_response(xml_response(body), "CopyObjectResult"),
5526                    Err(S3Error::SerdeXml(_))
5527                ),
5528                "accepted invalid XML: {body:?}"
5529            );
5530        }
5531        assert!(matches!(
5532            validate_success_xml_response(xml_response("<Other/>"), "CopyObjectResult"),
5533            Err(S3Error::HttpFailWithBody(200, _))
5534        ));
5535    }
5536
5537    #[test]
5538    fn xml_response_embedded_error_accepts_xml_11_text_and_decoded_attributes() {
5539        let body = "<?xml version=\"1.1\"?>\n<CopyObjectResult note=\"café&amp;tea\">first\r\u{0085}second</CopyObjectResult>\n";
5540        assert!(validate_success_xml_response(xml_response(body), "CopyObjectResult").is_ok());
5541    }
5542
5543    #[test]
5544    fn bucket_debug_redacts_nested_credentials() {
5545        let credentials = Credentials::new(
5546            Some("BUCKET_ACCESS_KEY_SENTINEL"),
5547            Some("BUCKET_SECRET_KEY_SENTINEL"),
5548            Some("BUCKET_SECURITY_TOKEN_SENTINEL"),
5549            Some("BUCKET_SESSION_TOKEN_SENTINEL"),
5550            None,
5551        )
5552        .unwrap();
5553        let bucket = Bucket::new(
5554            "debug-test-bucket",
5555            "us-east-1".parse::<Region>().unwrap(),
5556            credentials,
5557        )
5558        .unwrap();
5559
5560        for rendered in [format!("{bucket:?}"), format!("{bucket:#?}")] {
5561            for sentinel in [
5562                "BUCKET_ACCESS_KEY_SENTINEL",
5563                "BUCKET_SECRET_KEY_SENTINEL",
5564                "BUCKET_SECURITY_TOKEN_SENTINEL",
5565                "BUCKET_SESSION_TOKEN_SENTINEL",
5566            ] {
5567                assert!(
5568                    !rendered.contains(sentinel),
5569                    "bucket debug output leaked a credential"
5570                );
5571            }
5572        }
5573    }
5574
5575    #[test]
5576    fn xml_response_embedded_error_preserves_success_framing_and_service_error_body() {
5577        let success = xml_response(
5578            "<s:CopyObjectResult xmlns:s=\"urn:s3\"><s:ETag>\"abc\"</s:ETag><Extra/></s:CopyObjectResult>",
5579        );
5580        assert!(validate_success_xml_response(success, "CopyObjectResult").is_ok());
5581
5582        let error_body = "<Error><Code>SlowDown</Code><RequestId>req-123</RequestId></Error>";
5583        assert!(matches!(
5584            validate_success_xml_response(xml_response(error_body), "CopyObjectResult"),
5585            Err(S3Error::HttpFailWithBody(200, body)) if body == error_body
5586        ));
5587
5588        let non_success =
5589            ResponseData::new(bytes::Bytes::from_static(b"not xml"), 404, HashMap::new());
5590        assert_eq!(
5591            validate_success_xml_response(non_success, "CopyObjectResult")
5592                .unwrap()
5593                .status_code(),
5594            404
5595        );
5596    }
5597
5598    #[maybe_async::test(
5599        feature = "sync",
5600        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
5601        async(
5602            all(not(feature = "sync"), feature = "with-async-std"),
5603            async_std::test
5604        )
5605    )]
5606    async fn xml_response_embedded_error_is_checked_by_copy_and_multipart_operations() {
5607        use std::io::{Read, Write};
5608        use std::net::TcpListener;
5609        use std::thread;
5610        use std::time::{Duration, Instant};
5611
5612        fn server_error(
5613            stage: &str,
5614            completed_requests: usize,
5615            error: &dyn std::fmt::Display,
5616        ) -> String {
5617            format!(
5618                "stage={stage} completed_requests={completed_requests} request_index={} error={error}",
5619                completed_requests + 1
5620            )
5621        }
5622
5623        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
5624        listener.set_nonblocking(true).unwrap();
5625        let endpoint = format!("http://{}", listener.local_addr().unwrap());
5626        let server = thread::spawn(move || {
5627            let responses = [
5628                "<CopyObjectResult><ETag>\"copy-etag\"</ETag></CopyObjectResult>",
5629                " \n<?xml version=\"1.0\"?>\n<CompleteMultipartUploadResult> \n<ETag>\"multipart-etag\"</ETag></CompleteMultipartUploadResult> \n",
5630                "<Error><Code>SlowDown</Code><RequestId>copy-req</RequestId></Error>",
5631                " \n<?xml version=\"1.0\"?>\n<Error><Code>InternalError</Code><RequestId>multipart-req</RequestId></Error> \n",
5632                "<CopyObjectResult>",
5633            ];
5634            let mut count = 0;
5635            while count < responses.len() {
5636                // This test makes five sequential requests, each with its own
5637                // client timeout. Bound each accept separately so time spent
5638                // on earlier calls does not consume later calls' window.
5639                let request_deadline = Instant::now() + Duration::from_secs(8);
5640                let (stream, _) = loop {
5641                    match listener.accept() {
5642                        Ok(pair) => break pair,
5643                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5644                            if Instant::now() < request_deadline {
5645                                thread::sleep(Duration::from_millis(5));
5646                                continue;
5647                            }
5648                            return Err(format!(
5649                                "stage=accept_deadline completed_requests={count} expected_requests={} deadline_elapsed=true",
5650                                responses.len()
5651                            ));
5652                        }
5653                        Err(error) => {
5654                            return Err(format!(
5655                                "stage=accept completed_requests={count} next_request={} error_kind={:?} error={error}",
5656                                count + 1,
5657                                error.kind()
5658                            ));
5659                        }
5660                    }
5661                };
5662                let mut request = Vec::new();
5663                let mut stream = make_mock_stream_blocking(stream)
5664                    .map_err(|error| server_error("set_blocking", count, &error))?;
5665                stream
5666                    .set_read_timeout(Some(Duration::from_secs(2)))
5667                    .map_err(|error| server_error("set_read_timeout", count, &error))?;
5668                stream
5669                    .set_write_timeout(Some(Duration::from_secs(2)))
5670                    .map_err(|error| server_error("set_write_timeout", count, &error))?;
5671                let mut byte = [0u8; 1];
5672                while !request.ends_with(b"\r\n\r\n") {
5673                    if request.len() >= 16 * 1024 || Instant::now() >= request_deadline {
5674                        return Err(server_error(
5675                            "read_headers_bounds",
5676                            count,
5677                            &"request headers exceeded bounds",
5678                        ));
5679                    }
5680                    stream
5681                        .read_exact(&mut byte)
5682                        .map_err(|error| server_error("read_headers", count, &error))?;
5683                    request.push(byte[0]);
5684                }
5685                let headers = String::from_utf8_lossy(&request).to_ascii_lowercase();
5686                let content_length = headers
5687                    .lines()
5688                    .find_map(|line| line.strip_prefix("content-length:"))
5689                    .and_then(|value| value.trim().parse::<usize>().ok())
5690                    .unwrap_or(0);
5691                let content_length_value = content_length;
5692                if content_length_value > 64 * 1024 {
5693                    return Err(server_error(
5694                        "validate_content_length",
5695                        count,
5696                        &"request body exceeded mock server limit",
5697                    ));
5698                }
5699                let mut body = vec![0; content_length_value];
5700                stream
5701                    .read_exact(&mut body)
5702                    .map_err(|error| server_error("read_body", count, &error))?;
5703                let response_body = responses[count];
5704                let response = format!(
5705                    "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
5706                    response_body.len(),
5707                    response_body
5708                );
5709                stream
5710                    .write_all(response.as_bytes())
5711                    .map_err(|error| server_error("write_response", count, &error))?;
5712                count += 1;
5713            }
5714            Ok(count)
5715        });
5716
5717        let credentials = Credentials::new(
5718            Some("test_access_key"),
5719            Some("test_secret_key"),
5720            None,
5721            None,
5722            None,
5723        )
5724        .unwrap();
5725        let bucket = Bucket::new(
5726            "test-bucket",
5727            Region::Custom {
5728                region: "us-east-1".to_owned(),
5729                endpoint,
5730            },
5731            credentials,
5732        )
5733        .unwrap()
5734        .with_path_style()
5735        .with_request_timeout(Duration::from_secs(4))
5736        .unwrap();
5737
5738        let copy_ok = bucket.copy_object_internal("/source", "/destination").await;
5739        let multipart_ok = bucket
5740            .complete_multipart_upload(
5741                "/destination",
5742                "upload-id",
5743                vec![Part {
5744                    etag: "\"part-etag\"".to_owned(),
5745                    part_number: 1,
5746                }],
5747            )
5748            .await;
5749        let copy_error = bucket.copy_object_internal("/source", "/destination").await;
5750        let multipart_error = bucket
5751            .complete_multipart_upload(
5752                "/destination",
5753                "upload-id",
5754                vec![Part {
5755                    etag: "\"part-etag\"".to_owned(),
5756                    part_number: 1,
5757                }],
5758            )
5759            .await;
5760        let malformed_copy = bucket.copy_object_internal("/source", "/destination").await;
5761
5762        let server_result = server.join().expect("mock server panicked");
5763        assert_eq!(server_result.unwrap(), 5);
5764        assert_eq!(copy_ok.unwrap(), 200);
5765        assert_eq!(multipart_ok.unwrap().status_code(), 200);
5766        assert!(
5767            matches!(copy_error, Err(S3Error::HttpFailWithBody(200, body)) if body.contains("SlowDown") && body.contains("copy-req"))
5768        );
5769        assert!(
5770            matches!(multipart_error, Err(S3Error::HttpFailWithBody(200, body)) if body.contains("InternalError") && body.contains("multipart-req"))
5771        );
5772        assert!(matches!(malformed_copy, Err(S3Error::SerdeXml(_))));
5773    }
5774
5775    #[cfg(all(
5776        not(feature = "sync"),
5777        not(feature = "with-tokio"),
5778        feature = "with-async-std"
5779    ))]
5780    #[async_std::test]
5781    async fn copy_object_opens_new_connection_after_server_close() {
5782        use std::io::{Read, Write};
5783        use std::net::TcpListener;
5784        use std::thread;
5785        use std::time::{Duration, Instant};
5786
5787        fn read_request(
5788            stream: &mut std::net::TcpStream,
5789            deadline: Instant,
5790        ) -> Result<String, String> {
5791            let mut request = Vec::new();
5792            let mut byte = [0u8; 1];
5793            while !request.ends_with(b"\r\n\r\n") {
5794                let now = Instant::now();
5795                if now >= deadline {
5796                    return Err("deadline reading request headers".to_owned());
5797                }
5798                stream
5799                    .set_read_timeout(Some((deadline - now).min(Duration::from_millis(100))))
5800                    .map_err(|error| format!("set request read timeout: {error}"))?;
5801                if request.len() >= 16 * 1024 {
5802                    return Err("request headers exceeded test limit".to_owned());
5803                }
5804                match stream.read_exact(&mut byte) {
5805                    Ok(()) => {}
5806                    Err(error)
5807                        if matches!(
5808                            error.kind(),
5809                            std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
5810                        ) && Instant::now() < deadline =>
5811                    {
5812                        continue;
5813                    }
5814                    Err(error) => return Err(format!("read request headers: {error}")),
5815                }
5816                request.push(byte[0]);
5817            }
5818            Ok(String::from_utf8_lossy(&request).into_owned())
5819        }
5820
5821        fn write_response(stream: &mut std::net::TcpStream) -> Result<(), String> {
5822            let body = "<CopyObjectResult><ETag>\"copy-etag\"</ETag></CopyObjectResult>";
5823            let response = format!(
5824                "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
5825                body.len()
5826            );
5827            stream
5828                .write_all(response.as_bytes())
5829                .map_err(|error| format!("write close response: {error}"))
5830        }
5831
5832        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
5833        listener.set_nonblocking(true).unwrap();
5834        let endpoint = format!("http://{}", listener.local_addr().unwrap());
5835        let server = thread::spawn(move || -> Result<String, String> {
5836            let deadline = Instant::now() + Duration::from_secs(6);
5837            let (mut first, _) = loop {
5838                match listener.accept() {
5839                    Ok(pair) => break pair,
5840                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5841                        if Instant::now() >= deadline {
5842                            return Err("deadline waiting for first copy request".to_owned());
5843                        }
5844                        thread::sleep(Duration::from_millis(1));
5845                    }
5846                    Err(error) => return Err(format!("accept first request: {error}")),
5847                }
5848            };
5849            first
5850                .set_write_timeout(Some(Duration::from_secs(1)))
5851                .map_err(|error| format!("set first connection write timeout: {error}"))?;
5852            first
5853                .set_nonblocking(false)
5854                .map_err(|error| format!("set first connection blocking: {error}"))?;
5855            let first_line = read_request(&mut first, deadline)?;
5856            if !first_line.starts_with("PUT ") {
5857                return Err(format!("first request was not a copy PUT: {first_line:?}"));
5858            }
5859            write_response(&mut first)?;
5860            first
5861                .set_nonblocking(true)
5862                .map_err(|error| format!("set first connection nonblocking: {error}"))?;
5863
5864            // Watch the just-used socket and the listener together. Reuse is a
5865            // failure; a fresh accept is the expected connection-close path.
5866            loop {
5867                let mut byte = [0u8; 1];
5868                match first.read(&mut byte) {
5869                    Ok(0) => {}
5870                    Ok(_) => return Ok(format!("reused closed connection: {first_line:?}")),
5871                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
5872                    Err(error) => {
5873                        return Err(format!("read old connection during reuse check: {error}"));
5874                    }
5875                }
5876
5877                match listener.accept() {
5878                    Ok((mut second, _)) => {
5879                        second
5880                            .set_write_timeout(Some(Duration::from_secs(1)))
5881                            .map_err(|error| {
5882                                format!("set second connection write timeout: {error}")
5883                            })?;
5884                        second
5885                            .set_nonblocking(false)
5886                            .map_err(|error| format!("set second connection blocking: {error}"))?;
5887                        let second_line = read_request(&mut second, deadline)?;
5888                        if !second_line.starts_with("PUT ") {
5889                            return Err(format!(
5890                                "second request was not a copy PUT: {second_line:?}"
5891                            ));
5892                        }
5893                        write_response(&mut second)?;
5894                        return Ok(format!(
5895                            "fresh connection; first={first_line:?}; second={second_line:?}"
5896                        ));
5897                    }
5898                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
5899                    Err(error) => return Err(format!("accept second request: {error}")),
5900                }
5901
5902                if Instant::now() >= deadline {
5903                    return Err("deadline waiting for a fresh second connection".to_owned());
5904                }
5905                thread::sleep(Duration::from_millis(1));
5906            }
5907        });
5908
5909        let credentials = Credentials::new(
5910            Some("test_access_key"),
5911            Some("test_secret_key"),
5912            None,
5913            None,
5914            None,
5915        )
5916        .unwrap();
5917        let bucket = Bucket::new(
5918            "test-bucket",
5919            Region::Custom {
5920                region: "us-east-1".to_owned(),
5921                endpoint,
5922            },
5923            credentials,
5924        )
5925        .unwrap()
5926        .with_path_style()
5927        .with_request_timeout(Duration::from_secs(4))
5928        .unwrap();
5929
5930        let first_result = bucket.copy_object_internal("/source", "/destination").await;
5931        let second_result = bucket.copy_object_internal("/source", "/destination").await;
5932        let server_result = server.join().unwrap();
5933
5934        assert!(
5935            matches!(&server_result, Ok(result) if result.starts_with("fresh connection;")),
5936            "server={server_result:?}; first copy={first_result:?}; second copy={second_result:?}"
5937        );
5938        assert_eq!(first_result.unwrap(), 200);
5939        assert_eq!(second_result.unwrap(), 200);
5940    }
5941
5942    #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
5943    #[tokio::test]
5944    async fn test_object_exists_404_does_not_retry() {
5945        init();
5946
5947        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
5948        let endpoint = format!("http://{}", listener.local_addr().unwrap());
5949        let credentials = Credentials::new(
5950            Some("test_access_key"),
5951            Some("test_secret_key"),
5952            None,
5953            None,
5954            None,
5955        )
5956        .unwrap();
5957        let bucket = Bucket::new(
5958            "test-bucket",
5959            Region::Custom {
5960                region: "us-east-1".to_owned(),
5961                endpoint,
5962            },
5963            credentials,
5964        )
5965        .unwrap()
5966        .with_path_style();
5967        listener.set_nonblocking(true).unwrap();
5968        let (stop_server, stop_rx) = mpsc::channel();
5969
5970        let server = thread::spawn(move || {
5971            let mut requests = 0;
5972            let accept_deadline = Instant::now() + Duration::from_secs(15);
5973            loop {
5974                match stop_rx.try_recv() {
5975                    Ok(()) | Err(TryRecvError::Disconnected) => break,
5976                    Err(TryRecvError::Empty) => {}
5977                }
5978                assert!(
5979                    Instant::now() < accept_deadline,
5980                    "mock server did not receive a stop signal before its deadline"
5981                );
5982
5983                match listener.accept() {
5984                    Ok((stream, _)) => {
5985                        requests += 1;
5986                        let mut stream = make_mock_stream_blocking(stream).unwrap();
5987                        stream
5988                            .set_read_timeout(Some(Duration::from_secs(3)))
5989                            .unwrap();
5990                        stream
5991                            .set_write_timeout(Some(Duration::from_secs(3)))
5992                            .unwrap();
5993
5994                        let mut request = Vec::new();
5995                        let mut byte = [0; 1];
5996                        let header_deadline = Instant::now() + Duration::from_secs(3);
5997                        while !request.ends_with(b"\r\n\r\n") {
5998                            assert!(
5999                                request.len() < 16 * 1024,
6000                                "mock server received oversized HTTP headers"
6001                            );
6002                            assert!(
6003                                Instant::now() < header_deadline,
6004                                "mock server timed out reading HTTP headers"
6005                            );
6006                            match stream.read(&mut byte) {
6007                                Ok(0) => panic!("client closed before sending HTTP headers"),
6008                                Ok(_) => request.push(byte[0]),
6009                                Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {
6010                                    continue;
6011                                }
6012                                Err(error) => panic!("failed to read HTTP request: {error}"),
6013                            }
6014                        }
6015                        stream.write_all(
6016                            b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
6017                        )
6018                        .unwrap();
6019                    }
6020                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
6021                        if Instant::now() >= accept_deadline {
6022                            break;
6023                        }
6024                        thread::sleep(Duration::from_millis(5));
6025                    }
6026                    Err(_) => break,
6027                }
6028            }
6029            requests
6030        });
6031
6032        let exists =
6033            tokio::time::timeout(Duration::from_secs(5), bucket.object_exists("/missing.txt"))
6034                .await;
6035        let _ = stop_server.send(());
6036        let request_count = server.join().unwrap();
6037
6038        assert!(!exists.expect("object_exists request timed out").unwrap());
6039        assert_eq!(request_count, 1);
6040    }
6041
6042    #[test]
6043    #[cfg(any(
6044        feature = "tokio-native-tls",
6045        feature = "tokio-rustls-tls",
6046        feature = "tokio-rustls-tls-ring"
6047    ))]
6048    #[allow(deprecated)]
6049    fn dangerous_config_correct_spelling_and_compat_alias_set_same_options() {
6050        let bucket = Bucket::new(
6051            "test-bucket",
6052            Region::Custom {
6053                region: "test-region".to_owned(),
6054                endpoint: "https://example.com".to_owned(),
6055            },
6056            Credentials::anonymous().unwrap(),
6057        )
6058        .unwrap();
6059
6060        let corrected = bucket.set_dangerous_config(true, true).unwrap();
6061        let deprecated_alias = bucket.set_dangereous_config(true, true).unwrap();
6062
6063        assert!(corrected.client_options.accept_invalid_certs);
6064        assert!(corrected.client_options.accept_invalid_hostnames);
6065        assert_eq!(
6066            corrected.client_options.accept_invalid_certs,
6067            deprecated_alias.client_options.accept_invalid_certs
6068        );
6069        assert_eq!(
6070            corrected.client_options.accept_invalid_hostnames,
6071            deprecated_alias.client_options.accept_invalid_hostnames
6072        );
6073    }
6074    fn test_aws_credentials() -> Credentials {
6075        Credentials::new(
6076            Some(&env::var("EU_AWS_ACCESS_KEY_ID").unwrap()),
6077            Some(&env::var("EU_AWS_SECRET_ACCESS_KEY").unwrap()),
6078            None,
6079            None,
6080            None,
6081        )
6082        .unwrap()
6083    }
6084
6085    fn test_gc_credentials() -> Credentials {
6086        Credentials::new(
6087            Some(&env::var("GC_ACCESS_KEY_ID").unwrap()),
6088            Some(&env::var("GC_SECRET_ACCESS_KEY").unwrap()),
6089            None,
6090            None,
6091            None,
6092        )
6093        .unwrap()
6094    }
6095
6096    fn test_wasabi_credentials() -> Credentials {
6097        Credentials::new(
6098            Some(&env::var("WASABI_ACCESS_KEY_ID").unwrap()),
6099            Some(&env::var("WASABI_SECRET_ACCESS_KEY").unwrap()),
6100            None,
6101            None,
6102            None,
6103        )
6104        .unwrap()
6105    }
6106
6107    fn test_minio_credentials() -> Credentials {
6108        Credentials::new(
6109            Some(&env::var("MINIO_ACCESS_KEY_ID").unwrap()),
6110            Some(&env::var("MINIO_SECRET_ACCESS_KEY").unwrap()),
6111            None,
6112            None,
6113            None,
6114        )
6115        .unwrap()
6116    }
6117
6118    fn test_digital_ocean_credentials() -> Credentials {
6119        Credentials::new(
6120            Some(&env::var("DIGITAL_OCEAN_ACCESS_KEY_ID").unwrap()),
6121            Some(&env::var("DIGITAL_OCEAN_SECRET_ACCESS_KEY").unwrap()),
6122            None,
6123            None,
6124            None,
6125        )
6126        .unwrap()
6127    }
6128
6129    fn test_r2_credentials() -> Credentials {
6130        Credentials::new(
6131            Some(&env::var("R2_ACCESS_KEY_ID").unwrap()),
6132            Some(&env::var("R2_SECRET_ACCESS_KEY").unwrap()),
6133            None,
6134            None,
6135            None,
6136        )
6137        .unwrap()
6138    }
6139
6140    fn test_aws_bucket() -> Box<Bucket> {
6141        Bucket::new(
6142            "rust-s3-test",
6143            "eu-central-1".parse().unwrap(),
6144            test_aws_credentials(),
6145        )
6146        .unwrap()
6147    }
6148
6149    fn isolated_test_bucket_name() -> String {
6150        std::env::var("RUST_S3_TEST_BUCKET")
6151            .unwrap_or_else(|_| format!("rust-s3-test-{}", uuid::Uuid::new_v4()))
6152    }
6153
6154    #[maybe_async::maybe_async]
6155    async fn report_bucket_cleanup(bucket: &Bucket, context: &str) {
6156        match bucket.delete().await {
6157            Ok(status) if status < 300 => {}
6158            Ok(status) => eprintln!("{context}: bucket cleanup returned HTTP {status}"),
6159            Err(error) => eprintln!("{context}: bucket cleanup failed: {error}"),
6160        }
6161    }
6162
6163    fn check_status(status: u16, expected: u16, operation: &str) -> Result<(), String> {
6164        if status == expected {
6165            Ok(())
6166        } else {
6167            Err(format!(
6168                "{operation} returned HTTP {status}, expected {expected}"
6169            ))
6170        }
6171    }
6172
6173    fn test_wasabi_bucket() -> Box<Bucket> {
6174        Bucket::new(
6175            "rust-s3",
6176            "wa-eu-central-1".parse().unwrap(),
6177            test_wasabi_credentials(),
6178        )
6179        .unwrap()
6180    }
6181
6182    fn test_gc_bucket() -> Box<Bucket> {
6183        let mut bucket = Bucket::new(
6184            "rust-s3",
6185            Region::Custom {
6186                region: "us-east1".to_owned(),
6187                endpoint: "https://storage.googleapis.com".to_owned(),
6188            },
6189            test_gc_credentials(),
6190        )
6191        .unwrap();
6192        bucket.set_listobjects_v1();
6193        bucket
6194    }
6195
6196    fn test_minio_bucket() -> Box<Bucket> {
6197        Bucket::new(
6198            "rust-s3",
6199            Region::Custom {
6200                region: "us-east-1".to_owned(),
6201                endpoint: "http://localhost:9000".to_owned(),
6202            },
6203            test_minio_credentials(),
6204        )
6205        .unwrap()
6206        .with_path_style()
6207    }
6208
6209    /// Bucket with hardcoded fake credentials for tests that only exercise
6210    /// local signing logic and never hit the network.
6211    fn test_presign_bucket() -> Box<Bucket> {
6212        Bucket::new(
6213            "rust-s3",
6214            Region::Custom {
6215                region: "us-east-1".to_owned(),
6216                endpoint: "http://localhost:9000".to_owned(),
6217            },
6218            Credentials::new(
6219                Some("test_access_key"),
6220                Some("test_secret_key"),
6221                None,
6222                None,
6223                None,
6224            )
6225            .unwrap(),
6226        )
6227        .unwrap()
6228        .with_path_style()
6229    }
6230
6231    #[allow(dead_code)]
6232    fn test_digital_ocean_bucket() -> Box<Bucket> {
6233        Bucket::new("rust-s3", Region::DoFra1, test_digital_ocean_credentials()).unwrap()
6234    }
6235
6236    fn test_r2_bucket() -> Box<Bucket> {
6237        Bucket::new(
6238            "rust-s3",
6239            Region::R2 {
6240                account_id: "f048f3132be36fa1aaa8611992002b3f".to_string(),
6241            },
6242            test_r2_credentials(),
6243        )
6244        .unwrap()
6245    }
6246
6247    fn object(size: u32) -> Vec<u8> {
6248        (0..size).map(|_| 33).collect()
6249    }
6250
6251    #[maybe_async::maybe_async]
6252    async fn put_head_get_delete_object(bucket: Bucket, head: bool) {
6253        let s3_path = test_object_path("+test.file");
6254        let non_existant_path = test_object_path("+non_existant.file");
6255        let test: Vec<u8> = object(3072);
6256
6257        let response_data = bucket.put_object(&s3_path, &test).await.unwrap();
6258        assert_eq!(response_data.status_code(), 200);
6259
6260        // let attributes = bucket
6261        //     .get_object_attributes(s3_path, "904662384344", None)
6262        //     .await
6263        //     .unwrap();
6264
6265        let response_data = bucket.get_object(&s3_path).await.unwrap();
6266        assert_eq!(response_data.status_code(), 200);
6267        assert_eq!(test, response_data.as_slice());
6268
6269        let exists = bucket.object_exists(&s3_path).await.unwrap();
6270        assert!(exists);
6271
6272        let not_exists = bucket.object_exists(&non_existant_path).await.unwrap();
6273        assert!(!not_exists);
6274
6275        let response_data = bucket
6276            .get_object_range(&s3_path, 100, Some(1000))
6277            .await
6278            .unwrap();
6279        assert_eq!(response_data.status_code(), 206);
6280        assert_eq!(test[100..1001].to_vec(), response_data.as_slice());
6281
6282        // Test single-byte range read (start == end)
6283        let response_data = bucket
6284            .get_object_range(&s3_path, 100, Some(100))
6285            .await
6286            .unwrap();
6287        assert_eq!(response_data.status_code(), 206);
6288        assert_eq!(vec![test[100]], response_data.as_slice());
6289
6290        if head {
6291            let (_head_object_result, code) = bucket.head_object(&s3_path).await.unwrap();
6292            // println!("{:?}", head_object_result);
6293            assert_eq!(code, 200);
6294        }
6295
6296        // println!("{:?}", head_object_result);
6297        let response_data = bucket.delete_object(&s3_path).await.unwrap();
6298        assert_eq!(response_data.status_code(), 204);
6299    }
6300
6301    #[maybe_async::maybe_async]
6302    async fn put_head_delete_object_with_headers(bucket: Bucket) {
6303        let s3_path = test_object_path("+test.file");
6304        let non_existant_path = test_object_path("+non_existant.file");
6305        let test: Vec<u8> = object(3072);
6306        let header_value = "max-age=42";
6307
6308        let mut custom_headers = HeaderMap::new();
6309        custom_headers.insert(CACHE_CONTROL, HeaderValue::from_static(header_value));
6310        custom_headers.insert(
6311            HeaderName::from_static("test-key"),
6312            "value".parse().unwrap(),
6313        );
6314
6315        let response_data = bucket
6316            .put_object_with_headers(&s3_path, &test, Some(custom_headers.clone()))
6317            .await
6318            .expect("Put object with custom headers failed");
6319        assert_eq!(response_data.status_code(), 200);
6320
6321        let response_data = bucket.get_object(&s3_path).await.unwrap();
6322        assert_eq!(response_data.status_code(), 200);
6323        assert_eq!(test, response_data.as_slice());
6324
6325        let exists = bucket.object_exists(&s3_path).await.unwrap();
6326        assert!(exists);
6327
6328        let not_exists = bucket.object_exists(&non_existant_path).await.unwrap();
6329        assert!(!not_exists);
6330
6331        let response_data = bucket
6332            .get_object_range(&s3_path, 100, Some(1000))
6333            .await
6334            .unwrap();
6335        assert_eq!(response_data.status_code(), 206);
6336        assert_eq!(test[100..1001].to_vec(), response_data.as_slice());
6337
6338        let (head_object_result, code) = bucket.head_object(&s3_path).await.unwrap();
6339        // println!("{:?}", head_object_result);
6340        assert_eq!(code, 200);
6341        assert_eq!(
6342            head_object_result.cache_control,
6343            Some(header_value.to_string())
6344        );
6345
6346        let response_data = bucket.delete_object(&s3_path).await.unwrap();
6347        assert_eq!(response_data.status_code(), 204);
6348    }
6349
6350    #[ignore]
6351    #[cfg(feature = "tags")]
6352    #[maybe_async::test(
6353        feature = "sync",
6354        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6355        async(
6356            all(not(feature = "sync"), feature = "with-async-std"),
6357            async_std::test
6358        )
6359    )]
6360    async fn test_tagging_aws() {
6361        let bucket = test_aws_bucket();
6362        let tagging_path = test_object_key("tagging_test");
6363        let target_tags = vec![
6364            Tag {
6365                key: "Tag1".to_string(),
6366                value: "Value1".to_string(),
6367            },
6368            Tag {
6369                key: "Tag2".to_string(),
6370                value: "Value2".to_string(),
6371            },
6372        ];
6373        let empty_tags: Vec<Tag> = Vec::new();
6374        let response_data = bucket
6375            .put_object(&tagging_path, b"Gimme tags")
6376            .await
6377            .unwrap();
6378        assert_eq!(response_data.status_code(), 200);
6379        let (tags, _code) = bucket.get_object_tagging(&tagging_path).await.unwrap();
6380        assert_eq!(tags, empty_tags);
6381        let response_data = bucket
6382            .put_object_tagging(&tagging_path, &[("Tag1", "Value1"), ("Tag2", "Value2")])
6383            .await
6384            .unwrap();
6385        assert_eq!(response_data.status_code(), 200);
6386        let (mut tags, _code) = bucket.get_object_tagging(&tagging_path).await.unwrap();
6387        tags.sort_by(|left, right| left.key.cmp(&right.key));
6388        let mut target_tags = target_tags;
6389        target_tags.sort_by(|left, right| left.key.cmp(&right.key));
6390        assert_eq!(tags, target_tags);
6391        let _response_data = bucket.delete_object(&tagging_path).await.unwrap();
6392    }
6393
6394    #[ignore]
6395    #[cfg(feature = "tags")]
6396    #[maybe_async::test(
6397        feature = "sync",
6398        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6399        async(
6400            all(not(feature = "sync"), feature = "with-async-std"),
6401            async_std::test
6402        )
6403    )]
6404    async fn test_tagging_minio() {
6405        let bucket = test_minio_bucket();
6406        let tagging_path = test_object_key("tagging_test");
6407        let target_tags = vec![
6408            Tag {
6409                key: "Tag1".to_string(),
6410                value: "Value1".to_string(),
6411            },
6412            Tag {
6413                key: "Tag2".to_string(),
6414                value: "Value2".to_string(),
6415            },
6416        ];
6417        let empty_tags: Vec<Tag> = Vec::new();
6418        let response_data = bucket
6419            .put_object(&tagging_path, b"Gimme tags")
6420            .await
6421            .unwrap();
6422        assert_eq!(response_data.status_code(), 200);
6423        let (tags, _code) = bucket.get_object_tagging(&tagging_path).await.unwrap();
6424        assert_eq!(tags, empty_tags);
6425        let response_data = bucket
6426            .put_object_tagging(&tagging_path, &[("Tag1", "Value1"), ("Tag2", "Value2")])
6427            .await
6428            .unwrap();
6429        assert_eq!(response_data.status_code(), 200);
6430        let (mut tags, _code) = bucket.get_object_tagging(&tagging_path).await.unwrap();
6431        tags.sort_by(|left, right| left.key.cmp(&right.key));
6432        let mut target_tags = target_tags;
6433        target_tags.sort_by(|left, right| left.key.cmp(&right.key));
6434        assert_eq!(tags, target_tags);
6435        let _response_data = bucket.delete_object(&tagging_path).await.unwrap();
6436    }
6437
6438    #[ignore]
6439    #[maybe_async::test(
6440        feature = "sync",
6441        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6442        async(
6443            all(not(feature = "sync"), feature = "with-async-std"),
6444            async_std::test
6445        )
6446    )]
6447    async fn streaming_big_aws_put_head_get_delete_object() {
6448        streaming_test_put_get_delete_big_object(*test_aws_bucket()).await;
6449    }
6450
6451    #[ignore]
6452    #[maybe_async::test(
6453        feature = "sync",
6454        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6455        async(
6456            all(not(feature = "sync"), feature = "with-async-std"),
6457            async_std::test
6458        )
6459    )]
6460    async fn streaming_big_gc_put_head_get_delete_object() {
6461        streaming_test_put_get_delete_big_object(*test_gc_bucket()).await;
6462    }
6463
6464    #[ignore]
6465    #[maybe_async::test(
6466        feature = "sync",
6467        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6468        async(
6469            all(not(feature = "sync"), feature = "with-async-std"),
6470            async_std::test
6471        )
6472    )]
6473    async fn streaming_big_minio_put_head_get_delete_object() {
6474        streaming_test_put_get_delete_big_object(*test_minio_bucket()).await;
6475    }
6476
6477    #[ignore]
6478    #[maybe_async::test(
6479        feature = "sync",
6480        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6481        async(
6482            all(not(feature = "sync"), feature = "with-async-std"),
6483            async_std::test
6484        )
6485    )]
6486    async fn streaming_big_wasabi_put_head_get_delete_object() {
6487        streaming_test_put_get_delete_big_object(*test_wasabi_bucket()).await;
6488    }
6489
6490    #[ignore]
6491    #[maybe_async::test(
6492        feature = "sync",
6493        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6494        async(
6495            all(not(feature = "sync"), feature = "with-async-std"),
6496            async_std::test
6497        )
6498    )]
6499    async fn streaming_big_digital_ocean_put_head_get_delete_object() {
6500        streaming_test_put_get_delete_big_object(*test_digital_ocean_bucket()).await;
6501    }
6502
6503    #[ignore]
6504    #[maybe_async::test(
6505        feature = "sync",
6506        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6507        async(
6508            all(not(feature = "sync"), feature = "with-async-std"),
6509            async_std::test
6510        )
6511    )]
6512    async fn streaming_big_r2_put_head_get_delete_object() {
6513        streaming_test_put_get_delete_big_object(*test_r2_bucket()).await;
6514    }
6515
6516    // Test multi-part upload
6517    #[maybe_async::maybe_async]
6518    async fn streaming_test_put_get_delete_big_object(bucket: Bucket) {
6519        #[cfg(feature = "with-async-std")]
6520        use async_std::fs::File;
6521        #[cfg(feature = "with-async-std")]
6522        use async_std::io::WriteExt;
6523        #[cfg(feature = "with-async-std")]
6524        use async_std::stream::StreamExt;
6525        #[cfg(feature = "with-tokio")]
6526        use futures_util::StreamExt;
6527        #[cfg(not(any(feature = "with-tokio", feature = "with-async-std")))]
6528        use std::fs::File;
6529        #[cfg(not(any(feature = "with-tokio", feature = "with-async-std")))]
6530        use std::io::Write;
6531        #[cfg(feature = "with-tokio")]
6532        use tokio::fs::File;
6533        #[cfg(feature = "with-tokio")]
6534        use tokio::io::AsyncWriteExt;
6535
6536        init();
6537        let remote_path = test_object_key("+stream_test_big");
6538        let local_path = "+stream_test_big";
6539        std::fs::remove_file(local_path).unwrap_or(());
6540        let content: Vec<u8> = (0..20_000_000).map(|i| (i % 251) as u8).collect();
6541
6542        let mut file = File::create(local_path).await.unwrap();
6543        file.write_all(&content).await.unwrap();
6544        file.flush().await.unwrap();
6545        let mut reader = File::open(local_path).await.unwrap();
6546
6547        #[cfg(not(feature = "sync"))]
6548        let response = bucket
6549            .put_object_stream_builder(&remote_path)
6550            .with_content_type("application/x-rust-s3-stream-test")
6551            .with_headers(streaming_builder_test_headers())
6552            .execute_stream(&mut reader)
6553            .await
6554            .unwrap();
6555        #[cfg(feature = "sync")]
6556        let response = bucket
6557            .put_object_stream(&mut reader, &remote_path)
6558            .await
6559            .unwrap();
6560        #[cfg(not(feature = "sync"))]
6561        assert_eq!(response.status_code(), 200);
6562        #[cfg(feature = "sync")]
6563        assert_eq!(response, 200);
6564        #[cfg(not(feature = "sync"))]
6565        {
6566            let (head, code) = bucket.head_object(&remote_path).await.unwrap();
6567            assert_eq!(code, 200);
6568            assert_eq!(
6569                head.content_type.as_deref(),
6570                Some("application/x-rust-s3-stream-test")
6571            );
6572            assert_eq!(head.cache_control.as_deref(), Some("public, max-age=120"));
6573            assert_eq!(
6574                head.metadata
6575                    .as_ref()
6576                    .and_then(|metadata| metadata.get("stream-marker"))
6577                    .map(String::as_str),
6578                Some("multipart-builder")
6579            );
6580        }
6581        let mut writer = Vec::new();
6582        let code = bucket
6583            .get_object_to_writer(&remote_path, &mut writer)
6584            .await
6585            .unwrap();
6586        assert_eq!(code, 200);
6587        assert!(
6588            content == writer,
6589            "writer download differs from uploaded bytes"
6590        );
6591        assert_eq!(content.len(), 20_000_000);
6592
6593        #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
6594        {
6595            let mut response_data_stream = bucket.get_object_stream(&remote_path).await.unwrap();
6596
6597            let mut streamed_len = 0;
6598            while let Some(chunk) = response_data_stream.bytes().next().await {
6599                let chunk = chunk.unwrap();
6600                let end = streamed_len + chunk.len();
6601                assert!(end <= content.len(), "stream returned too many bytes");
6602                assert!(
6603                    content[streamed_len..end] == chunk[..],
6604                    "streamed bytes differ from uploaded bytes at offset {streamed_len}"
6605                );
6606                streamed_len = end;
6607            }
6608            assert_eq!(streamed_len, content.len());
6609        }
6610
6611        let response_data = bucket.delete_object(&remote_path).await.unwrap();
6612        assert_eq!(response_data.status_code(), 204);
6613        std::fs::remove_file(local_path).unwrap_or(());
6614    }
6615
6616    #[ignore]
6617    #[maybe_async::test(
6618        feature = "sync",
6619        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6620        async(
6621            all(not(feature = "sync"), feature = "with-async-std"),
6622            async_std::test
6623        )
6624    )]
6625    async fn streaming_aws_put_head_get_delete_object() {
6626        streaming_test_put_get_delete_small_object(test_aws_bucket()).await;
6627    }
6628
6629    #[ignore]
6630    #[maybe_async::test(
6631        feature = "sync",
6632        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6633        async(
6634            all(not(feature = "sync"), feature = "with-async-std"),
6635            async_std::test
6636        )
6637    )]
6638    async fn streaming_gc_put_head_get_delete_object() {
6639        streaming_test_put_get_delete_small_object(test_gc_bucket()).await;
6640    }
6641
6642    #[ignore]
6643    #[maybe_async::test(
6644        feature = "sync",
6645        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6646        async(
6647            all(not(feature = "sync"), feature = "with-async-std"),
6648            async_std::test
6649        )
6650    )]
6651    async fn streaming_r2_put_head_get_delete_object() {
6652        streaming_test_put_get_delete_small_object(test_r2_bucket()).await;
6653    }
6654
6655    #[ignore]
6656    #[maybe_async::test(
6657        feature = "sync",
6658        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6659        async(
6660            all(not(feature = "sync"), feature = "with-async-std"),
6661            async_std::test
6662        )
6663    )]
6664    async fn streaming_minio_put_head_get_delete_object() {
6665        streaming_test_put_get_delete_small_object(test_minio_bucket()).await;
6666    }
6667
6668    #[ignore]
6669    #[cfg(not(feature = "sync"))]
6670    #[maybe_async::test(
6671        feature = "sync",
6672        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6673        async(
6674            all(not(feature = "sync"), feature = "with-async-std"),
6675            async_std::test
6676        )
6677    )]
6678    async fn streaming_aws_explicit_multipart_limits_roundtrip() {
6679        streaming_test_explicit_multipart_limits(test_aws_bucket()).await;
6680    }
6681
6682    #[ignore]
6683    #[cfg(not(feature = "sync"))]
6684    #[maybe_async::test(
6685        feature = "sync",
6686        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6687        async(
6688            all(not(feature = "sync"), feature = "with-async-std"),
6689            async_std::test
6690        )
6691    )]
6692    async fn streaming_minio_explicit_multipart_limits_roundtrip() {
6693        streaming_test_explicit_multipart_limits(test_minio_bucket()).await;
6694    }
6695
6696    #[cfg(not(feature = "sync"))]
6697    #[maybe_async::maybe_async]
6698    async fn streaming_test_explicit_multipart_limits(bucket: Box<Bucket>) {
6699        init();
6700        let content = object((2 * super::CHUNK_SIZE + 23) as u32);
6701        for limit in [1, 2] {
6702            let key = test_object_key(&format!("+stream_concurrency_{limit}"));
6703            #[cfg(feature = "with-tokio")]
6704            let mut reader = std::io::Cursor::new(content.clone());
6705            #[cfg(feature = "with-async-std")]
6706            let mut reader = async_std::io::Cursor::new(content.clone());
6707            let response = bucket
6708                .put_object_stream_builder(&key)
6709                .with_max_concurrent_chunks(limit)
6710                .execute_stream(&mut reader)
6711                .await
6712                .unwrap();
6713            assert_eq!(response.status_code(), 200);
6714            assert_eq!(response.uploaded_bytes(), content.len());
6715            let mut downloaded = Vec::new();
6716            assert_eq!(
6717                bucket
6718                    .get_object_to_writer(&key, &mut downloaded)
6719                    .await
6720                    .unwrap(),
6721                200
6722            );
6723            assert_eq!(downloaded, content);
6724            assert_eq!(bucket.delete_object(&key).await.unwrap().status_code(), 204);
6725        }
6726    }
6727
6728    #[maybe_async::maybe_async]
6729    async fn streaming_test_put_get_delete_small_object(bucket: Box<Bucket>) {
6730        init();
6731        let remote_path = test_object_key("+stream_test_small");
6732        let content: Vec<u8> = object(1000);
6733        #[cfg(feature = "with-tokio")]
6734        let mut reader = std::io::Cursor::new(&content);
6735        #[cfg(feature = "with-async-std")]
6736        let mut reader = async_std::io::Cursor::new(&content);
6737        #[cfg(feature = "sync")]
6738        let mut reader = std::io::Cursor::new(&content);
6739
6740        #[cfg(not(feature = "sync"))]
6741        let response = bucket
6742            .put_object_stream_builder(&remote_path)
6743            .with_content_type("application/x-rust-s3-stream-test")
6744            .with_headers(streaming_builder_test_headers())
6745            .execute_stream(&mut reader)
6746            .await
6747            .unwrap();
6748        #[cfg(feature = "sync")]
6749        let response = bucket
6750            .put_object_stream(&mut reader, &remote_path)
6751            .await
6752            .unwrap();
6753        #[cfg(not(feature = "sync"))]
6754        assert_eq!(response.status_code(), 200);
6755        #[cfg(feature = "sync")]
6756        assert_eq!(response, 200);
6757        #[cfg(not(feature = "sync"))]
6758        {
6759            let (head, code) = bucket.head_object(&remote_path).await.unwrap();
6760            assert_eq!(code, 200);
6761            assert_eq!(
6762                head.content_type.as_deref(),
6763                Some("application/x-rust-s3-stream-test")
6764            );
6765            assert_eq!(head.cache_control.as_deref(), Some("public, max-age=120"));
6766            assert_eq!(
6767                head.metadata
6768                    .as_ref()
6769                    .and_then(|metadata| metadata.get("stream-marker"))
6770                    .map(String::as_str),
6771                Some("multipart-builder")
6772            );
6773        }
6774        let mut writer = Vec::new();
6775        let code = bucket
6776            .get_object_to_writer(&remote_path, &mut writer)
6777            .await
6778            .unwrap();
6779        assert_eq!(code, 200);
6780        assert_eq!(content, writer);
6781
6782        let response_data = bucket.delete_object(&remote_path).await.unwrap();
6783        assert_eq!(response_data.status_code(), 204);
6784    }
6785
6786    #[cfg(all(
6787        not(feature = "sync"),
6788        any(feature = "with-tokio", feature = "with-async-std")
6789    ))]
6790    fn streaming_builder_test_headers() -> HeaderMap {
6791        let mut headers = HeaderMap::new();
6792        headers.insert(
6793            CACHE_CONTROL,
6794            HeaderValue::from_static("public, max-age=120"),
6795        );
6796        headers.insert(
6797            HeaderName::from_static("x-amz-meta-stream-marker"),
6798            HeaderValue::from_static("multipart-builder"),
6799        );
6800        headers
6801    }
6802
6803    #[cfg(feature = "blocking")]
6804    fn put_head_get_list_delete_object_blocking(bucket: Bucket) {
6805        let s3_key = test_object_key("test_blocking.file");
6806        let s3_key_2 = test_object_key("test_blocking.file2");
6807        let s3_key_3 = test_object_key("test_blocking.file3");
6808        let s3_path = format!("/{s3_key}");
6809        let s3_path_2 = format!("/{s3_key_2}");
6810        let s3_path_3 = format!("/{s3_key_3}");
6811        let list_prefix = test_object_key("test_blocking.");
6812        let test: Vec<u8> = object(3072);
6813
6814        // Test PutObject
6815        let response_data = bucket.put_object_blocking(&s3_path, &test).unwrap();
6816        assert_eq!(response_data.status_code(), 200);
6817
6818        // Test GetObject
6819        let response_data = bucket.get_object_blocking(&s3_path).unwrap();
6820        assert_eq!(response_data.status_code(), 200);
6821        assert_eq!(test, response_data.as_slice());
6822
6823        // Test GetObject with a range
6824        let response_data = bucket
6825            .get_object_range_blocking(&s3_path, 100, Some(1000))
6826            .unwrap();
6827        assert_eq!(response_data.status_code(), 206);
6828        assert_eq!(test[100..1001].to_vec(), response_data.as_slice());
6829
6830        // Test single-byte range read (start == end)
6831        let response_data = bucket
6832            .get_object_range_blocking(&s3_path, 100, Some(100))
6833            .unwrap();
6834        assert_eq!(response_data.status_code(), 206);
6835        assert_eq!(vec![test[100]], response_data.as_slice());
6836
6837        // Test HeadObject
6838        let (head_object_result, code) = bucket.head_object_blocking(&s3_path).unwrap();
6839        assert_eq!(code, 200);
6840        assert_eq!(
6841            head_object_result.content_type.unwrap(),
6842            "application/octet-stream".to_owned()
6843        );
6844        // println!("{:?}", head_object_result);
6845
6846        // Put some additional objects, so that we can test ListObjects
6847        let response_data = bucket.put_object_blocking(&s3_path_2, &test).unwrap();
6848        assert_eq!(response_data.status_code(), 200);
6849        let response_data = bucket.put_object_blocking(&s3_path_3, &test).unwrap();
6850        assert_eq!(response_data.status_code(), 200);
6851
6852        // Test ListObjects, with continuation
6853        let (result, code) = bucket
6854            .list_page_blocking(
6855                list_prefix.clone(),
6856                Some("/".to_string()),
6857                None,
6858                None,
6859                Some(2),
6860            )
6861            .unwrap();
6862        assert_eq!(code, 200);
6863        assert_eq!(result.contents.len(), 2);
6864        assert!(result.is_truncated);
6865        let mut listed_keys: Vec<String> =
6866            result.contents.into_iter().map(|item| item.key).collect();
6867
6868        let cont_token = result
6869            .next_continuation_token
6870            .expect("truncated listing should include a continuation token");
6871
6872        let (result, code) = bucket
6873            .list_page_blocking(
6874                list_prefix,
6875                Some("/".to_string()),
6876                Some(cont_token),
6877                None,
6878                Some(2),
6879            )
6880            .unwrap();
6881        assert_eq!(code, 200);
6882        assert_eq!(result.contents.len(), 1);
6883        assert!(!result.is_truncated);
6884        assert!(result.next_continuation_token.is_none());
6885        listed_keys.extend(result.contents.into_iter().map(|item| item.key));
6886        listed_keys.sort();
6887        let mut expected_keys = vec![s3_key, s3_key_2, s3_key_3];
6888        expected_keys.sort();
6889        assert_eq!(listed_keys, expected_keys);
6890
6891        // cleanup (and test Delete)
6892        let response_data = bucket.delete_object_blocking(&s3_path).unwrap();
6893        assert_eq!(response_data.status_code(), 204);
6894        let response_data = bucket.delete_object_blocking(&s3_path_2).unwrap();
6895        assert_eq!(response_data.status_code(), 204);
6896        let response_data = bucket.delete_object_blocking(&s3_path_3).unwrap();
6897        assert_eq!(response_data.status_code(), 204);
6898    }
6899
6900    #[ignore]
6901    #[cfg(all(
6902        any(feature = "with-tokio", feature = "with-async-std"),
6903        feature = "blocking"
6904    ))]
6905    #[test]
6906    fn aws_put_head_get_delete_object_blocking() {
6907        put_head_get_list_delete_object_blocking(*test_aws_bucket())
6908    }
6909
6910    #[ignore]
6911    #[cfg(all(
6912        any(feature = "with-tokio", feature = "with-async-std"),
6913        feature = "blocking"
6914    ))]
6915    #[test]
6916    fn gc_put_head_get_delete_object_blocking() {
6917        put_head_get_list_delete_object_blocking(*test_gc_bucket())
6918    }
6919
6920    #[ignore]
6921    #[cfg(all(
6922        any(feature = "with-tokio", feature = "with-async-std"),
6923        feature = "blocking"
6924    ))]
6925    #[test]
6926    fn wasabi_put_head_get_delete_object_blocking() {
6927        put_head_get_list_delete_object_blocking(*test_wasabi_bucket())
6928    }
6929
6930    #[ignore]
6931    #[cfg(all(
6932        any(feature = "with-tokio", feature = "with-async-std"),
6933        feature = "blocking"
6934    ))]
6935    #[test]
6936    fn minio_put_head_get_delete_object_blocking() {
6937        put_head_get_list_delete_object_blocking(*test_minio_bucket())
6938    }
6939
6940    #[ignore]
6941    #[cfg(all(
6942        any(feature = "with-tokio", feature = "with-async-std"),
6943        feature = "blocking"
6944    ))]
6945    #[test]
6946    fn r2_put_head_get_delete_object_blocking() {
6947        put_head_get_list_delete_object_blocking(*test_r2_bucket())
6948    }
6949
6950    #[ignore]
6951    #[cfg(all(
6952        any(feature = "with-tokio", feature = "with-async-std"),
6953        feature = "blocking"
6954    ))]
6955    #[test]
6956    fn digital_ocean_put_head_get_delete_object_blocking() {
6957        put_head_get_list_delete_object_blocking(*test_digital_ocean_bucket())
6958    }
6959
6960    #[ignore]
6961    #[maybe_async::test(
6962        feature = "sync",
6963        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6964        async(
6965            all(not(feature = "sync"), feature = "with-async-std"),
6966            async_std::test
6967        )
6968    )]
6969    async fn aws_put_head_get_delete_object() {
6970        put_head_get_delete_object(*test_aws_bucket(), true).await;
6971        put_head_delete_object_with_headers(*test_aws_bucket()).await;
6972    }
6973
6974    #[ignore]
6975    #[maybe_async::test(
6976        feature = "sync",
6977        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6978        async(
6979            all(not(feature = "sync"), feature = "with-async-std"),
6980            async_std::test
6981        )
6982    )]
6983    async fn gc_test_put_head_get_delete_object() {
6984        put_head_get_delete_object(*test_gc_bucket(), true).await;
6985        put_head_delete_object_with_headers(*test_gc_bucket()).await;
6986    }
6987
6988    #[ignore]
6989    #[maybe_async::test(
6990        feature = "sync",
6991        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
6992        async(
6993            all(not(feature = "sync"), feature = "with-async-std"),
6994            async_std::test
6995        )
6996    )]
6997    async fn wasabi_test_put_head_get_delete_object() {
6998        put_head_get_delete_object(*test_wasabi_bucket(), true).await;
6999        put_head_delete_object_with_headers(*test_wasabi_bucket()).await;
7000    }
7001
7002    #[ignore]
7003    #[maybe_async::test(
7004        feature = "sync",
7005        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7006        async(
7007            all(not(feature = "sync"), feature = "with-async-std"),
7008            async_std::test
7009        )
7010    )]
7011    async fn minio_test_put_head_get_delete_object() {
7012        let bucket = *test_minio_bucket();
7013        put_head_get_delete_object(bucket.clone(), true).await;
7014        put_head_delete_object_with_headers(bucket.clone()).await;
7015
7016        let prefix = test_object_key(&format!("+copy_roundtrip_{}", uuid::Uuid::new_v4()));
7017        let source = format!("{prefix}_source");
7018        let destination = format!("{prefix}_destination");
7019        let content = b"MinIO copy object content verification";
7020        let copy_result = match bucket.put_object(&source, content).await {
7021            Err(error) => Err(error),
7022            Ok(_) => match bucket.copy_object_internal(&source, &destination).await {
7023                Err(error) => Err(error),
7024                Ok(status) => bucket
7025                    .get_object(&destination)
7026                    .await
7027                    .map(|copied| (status, copied.to_vec())),
7028            },
7029        };
7030        let source_cleanup = bucket.delete_object(&source).await;
7031        let destination_cleanup = bucket.delete_object(&destination).await;
7032
7033        assert_eq!(source_cleanup.unwrap().status_code(), 204);
7034        assert_eq!(destination_cleanup.unwrap().status_code(), 204);
7035        let (status, copied) = copy_result.unwrap();
7036        assert_eq!(status, 200);
7037        assert_eq!(copied.as_slice(), content);
7038    }
7039
7040    #[ignore]
7041    #[maybe_async::test(
7042        feature = "sync",
7043        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7044        async(
7045            all(not(feature = "sync"), feature = "with-async-std"),
7046            async_std::test
7047        )
7048    )]
7049    async fn minio_multipart_upload_pagination_uses_key_and_upload_id_markers() {
7050        let bucket = *test_minio_bucket();
7051        let key = test_object_key(&format!("multipart-pagination/{}", uuid::Uuid::new_v4()));
7052        let mut created_upload_ids = Vec::new();
7053        let listing_result =
7054            minio_list_multipart_pages(&bucket, &key, &mut created_upload_ids).await;
7055
7056        let mut cleanup_errors = Vec::new();
7057        for upload_id in &created_upload_ids {
7058            if let Err(error) = bucket.abort_upload(&key, upload_id).await {
7059                cleanup_errors.push(error.to_string());
7060            }
7061        }
7062
7063        let listed_ids = listing_result.unwrap_or_else(|primary| {
7064            panic!(
7065                "MinIO multipart pagination failed: {primary}; cleanup errors: {cleanup_errors:?}"
7066            )
7067        });
7068        assert!(
7069            cleanup_errors.is_empty(),
7070            "failed to abort test multipart uploads: {cleanup_errors:?}"
7071        );
7072        assert_eq!(listed_ids.len(), 2);
7073    }
7074
7075    #[maybe_async::maybe_async]
7076    async fn minio_list_multipart_pages(
7077        bucket: &Bucket,
7078        key: &str,
7079        created_upload_ids: &mut Vec<String>,
7080    ) -> Result<Vec<String>, String> {
7081        for _ in 0..2 {
7082            let upload = bucket
7083                .initiate_multipart_upload(key, "application/octet-stream")
7084                .await
7085                .map_err(|error| error.to_string())?;
7086            created_upload_ids.push(upload.upload_id);
7087        }
7088
7089        let (first_page, first_status) = bucket
7090            .list_multiparts_uploads_page(Some(key), None, None, None, Some(1))
7091            .await
7092            .map_err(|error| error.to_string())?;
7093        if first_status != 200 || !first_page.is_truncated || first_page.uploads.len() != 1 {
7094            return Err("first MinIO page did not contain one truncated upload".to_owned());
7095        }
7096        let next_key_marker = first_page
7097            .next_marker
7098            .clone()
7099            .ok_or_else(|| "first MinIO page omitted NextKeyMarker".to_owned())?;
7100        let next_upload_id_marker = first_page
7101            .next_upload_id_marker
7102            .clone()
7103            .ok_or_else(|| "first MinIO page omitted NextUploadIdMarker".to_owned())?;
7104
7105        let (second_page, second_status) = bucket
7106            .list_multiparts_uploads_page(
7107                Some(key),
7108                None,
7109                Some(next_key_marker),
7110                Some(next_upload_id_marker),
7111                Some(1),
7112            )
7113            .await
7114            .map_err(|error| error.to_string())?;
7115        if second_status != 200 || second_page.is_truncated || second_page.uploads.len() != 1 {
7116            return Err("second MinIO page did not contain one upload".to_owned());
7117        }
7118
7119        let mut listed_ids = first_page
7120            .uploads
7121            .iter()
7122            .chain(second_page.uploads.iter())
7123            .map(|upload| upload.id.clone())
7124            .collect::<Vec<_>>();
7125        listed_ids.sort();
7126        let mut expected_ids = created_upload_ids.clone();
7127        expected_ids.sort();
7128        if listed_ids != expected_ids {
7129            return Err(
7130                "MinIO pagination did not return each created upload exactly once".to_owned(),
7131            );
7132        }
7133        Ok(listed_ids)
7134    }
7135
7136    #[ignore]
7137    #[maybe_async::test(
7138        feature = "sync",
7139        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7140        async(
7141            all(not(feature = "sync"), feature = "with-async-std"),
7142            async_std::test
7143        )
7144    )]
7145    async fn digital_ocean_test_put_head_get_delete_object() {
7146        let bucket = *test_digital_ocean_bucket();
7147        put_head_get_delete_object(bucket.clone(), true).await;
7148        put_head_delete_object_with_headers(bucket).await;
7149    }
7150
7151    #[ignore]
7152    #[maybe_async::test(
7153        feature = "sync",
7154        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7155        async(
7156            all(not(feature = "sync"), feature = "with-async-std"),
7157            async_std::test
7158        )
7159    )]
7160    async fn r2_test_put_head_get_delete_object() {
7161        put_head_get_delete_object(*test_r2_bucket(), false).await;
7162        put_head_delete_object_with_headers(*test_r2_bucket()).await;
7163    }
7164
7165    #[maybe_async::maybe_async]
7166    async fn put_delete_objects(bucket: Bucket) {
7167        use crate::serde_types::ObjectIdentifier;
7168
7169        let paths = [
7170            test_object_path("+bulk_delete_1.file"),
7171            test_object_path("+bulk_delete_2.file"),
7172            test_object_path("+bulk_delete_3.file"),
7173        ];
7174        let test: Vec<u8> = object(128);
7175
7176        // Put test objects
7177        for path in &paths {
7178            let response_data = bucket.put_object(path, &test).await.unwrap();
7179            assert_eq!(response_data.status_code(), 200);
7180        }
7181
7182        // Bulk delete them
7183        let objects: Vec<ObjectIdentifier> = paths
7184            .iter()
7185            .map(|path| ObjectIdentifier::new(path.as_str()))
7186            .collect();
7187        let result = bucket.delete_objects(objects).await.unwrap();
7188
7189        assert_eq!(result.deleted.len(), 3);
7190        assert!(result.errors.is_empty());
7191
7192        // Verify they are gone
7193        for path in &paths {
7194            let exists = bucket.object_exists(path).await.unwrap();
7195            assert!(!exists);
7196        }
7197    }
7198
7199    #[ignore]
7200    #[maybe_async::test(
7201        feature = "sync",
7202        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7203        async(
7204            all(not(feature = "sync"), feature = "with-async-std"),
7205            async_std::test
7206        )
7207    )]
7208    async fn aws_test_delete_objects() {
7209        put_delete_objects(*test_aws_bucket()).await;
7210    }
7211
7212    #[ignore]
7213    #[maybe_async::test(
7214        feature = "sync",
7215        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7216        async(
7217            all(not(feature = "sync"), feature = "with-async-std"),
7218            async_std::test
7219        )
7220    )]
7221    async fn minio_test_delete_objects() {
7222        put_delete_objects(*test_minio_bucket()).await;
7223    }
7224
7225    #[maybe_async::test(
7226        feature = "sync",
7227        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7228        async(
7229            all(not(feature = "sync"), feature = "with-async-std"),
7230            async_std::test
7231        )
7232    )]
7233    async fn test_presign_put() {
7234        let s3_path = "/test/test.file";
7235        let bucket = test_presign_bucket();
7236
7237        let mut custom_headers = HeaderMap::new();
7238        custom_headers.insert(
7239            HeaderName::from_static("custom_header"),
7240            "custom_value".parse().unwrap(),
7241        );
7242
7243        let url = bucket
7244            .presign_put(s3_path, 86400, Some(custom_headers), None)
7245            .await
7246            .unwrap();
7247
7248        assert!(url.contains("custom_header%3Bhost"));
7249        assert!(url.contains("/test/test.file"))
7250    }
7251
7252    #[maybe_async::test(
7253        feature = "sync",
7254        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7255        async(
7256            all(not(feature = "sync"), feature = "with-async-std"),
7257            async_std::test
7258        )
7259    )]
7260    async fn test_presign_post() {
7261        use std::borrow::Cow;
7262
7263        let bucket = test_presign_bucket();
7264
7265        // Policy from sample
7266        let policy = PostPolicy::new(86400)
7267            .condition(
7268                PostPolicyField::Key,
7269                PostPolicyValue::StartsWith(Cow::from("user/user1/")),
7270            )
7271            .unwrap();
7272
7273        let data = bucket.presign_post(policy).await.unwrap();
7274
7275        assert_eq!(data.url, "http://localhost:9000/rust-s3");
7276        assert_eq!(data.fields.len(), 6);
7277        assert_eq!(data.dynamic_fields.len(), 1);
7278    }
7279
7280    #[maybe_async::test(
7281        feature = "sync",
7282        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7283        async(
7284            all(not(feature = "sync"), feature = "with-async-std"),
7285            async_std::test
7286        )
7287    )]
7288    async fn test_presign_get() {
7289        let s3_path = "/test/test.file";
7290        let bucket = test_presign_bucket();
7291
7292        let url = bucket.presign_get(s3_path, 86400, None).await.unwrap();
7293        assert!(url.contains("/test/test.file?"))
7294    }
7295
7296    #[maybe_async::test(
7297        feature = "sync",
7298        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7299        async(
7300            all(not(feature = "sync"), feature = "with-async-std"),
7301            async_std::test
7302        )
7303    )]
7304    async fn test_presign_delete() {
7305        let s3_path = "/test/test.file";
7306        let bucket = test_presign_bucket();
7307
7308        let url = bucket.presign_delete(s3_path, 86400).await.unwrap();
7309        assert!(url.contains("/test/test.file?"))
7310    }
7311
7312    #[maybe_async::test(
7313        feature = "sync",
7314        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7315        async(
7316            all(not(feature = "sync"), feature = "with-async-std"),
7317            async_std::test
7318        )
7319    )]
7320    async fn test_presign_url_standard_ports() {
7321        // Test that presigned URLs preserve standard ports in the host header
7322        // This is crucial for signature validation
7323
7324        // Test with HTTP standard port 80
7325        let region_http_80 = Region::Custom {
7326            region: "eu-central-1".to_owned(),
7327            endpoint: "http://minio:80".to_owned(),
7328        };
7329        let credentials = Credentials::new(
7330            Some("test_access_key"),
7331            Some("test_secret_key"),
7332            None,
7333            None,
7334            None,
7335        )
7336        .unwrap();
7337        let bucket_http_80 = Bucket::new("test-bucket", region_http_80, credentials.clone())
7338            .unwrap()
7339            .with_path_style();
7340
7341        let presigned_url_80 = bucket_http_80
7342            .presign_get("/test.file", 3600, None)
7343            .await
7344            .unwrap();
7345        println!("Presigned URL with port 80: {}", presigned_url_80);
7346
7347        // Port 80 MUST be preserved in the URL for signature validation
7348        assert!(
7349            presigned_url_80.starts_with("http://minio:80/"),
7350            "URL must preserve port 80, got: {}",
7351            presigned_url_80
7352        );
7353
7354        // Test with HTTPS standard port 443
7355        let region_https_443 = Region::Custom {
7356            region: "eu-central-1".to_owned(),
7357            endpoint: "https://minio:443".to_owned(),
7358        };
7359        let bucket_https_443 = Bucket::new("test-bucket", region_https_443, credentials.clone())
7360            .unwrap()
7361            .with_path_style();
7362
7363        let presigned_url_443 = bucket_https_443
7364            .presign_get("/test.file", 3600, None)
7365            .await
7366            .unwrap();
7367        println!("Presigned URL with port 443: {}", presigned_url_443);
7368
7369        // Port 443 MUST be preserved in the URL for signature validation
7370        assert!(
7371            presigned_url_443.starts_with("https://minio:443/"),
7372            "URL must preserve port 443, got: {}",
7373            presigned_url_443
7374        );
7375
7376        // Test with non-standard port (should always include port)
7377        let region_http_9000 = Region::Custom {
7378            region: "eu-central-1".to_owned(),
7379            endpoint: "http://minio:9000".to_owned(),
7380        };
7381        let bucket_http_9000 = Bucket::new("test-bucket", region_http_9000, credentials)
7382            .unwrap()
7383            .with_path_style();
7384
7385        let presigned_url_9000 = bucket_http_9000
7386            .presign_get("/test.file", 3600, None)
7387            .await
7388            .unwrap();
7389        assert!(
7390            presigned_url_9000.contains("minio:9000"),
7391            "Non-standard port should be preserved in URL"
7392        );
7393    }
7394
7395    #[maybe_async::test(
7396        feature = "sync",
7397        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7398        async(
7399            all(not(feature = "sync"), feature = "with-async-std"),
7400            async_std::test
7401        )
7402    )]
7403    #[ignore]
7404    async fn test_bucket_create_delete_default_region() {
7405        let config = BucketConfiguration::default();
7406        let response = Bucket::create(
7407            &isolated_test_bucket_name(),
7408            "us-east-1".parse().unwrap(),
7409            test_aws_credentials(),
7410            config,
7411        )
7412        .await
7413        .unwrap();
7414
7415        let bucket = response.bucket;
7416        let result = check_status(response.response_code, 200, "create bucket").and_then(|()| {
7417            if response.response_text.is_empty() {
7418                Ok(())
7419            } else {
7420                Err(format!(
7421                    "create bucket response body was {:?}",
7422                    response.response_text
7423                ))
7424            }
7425        });
7426        if let Err(error) = result {
7427            report_bucket_cleanup(&bucket, "test_bucket_create_delete_default_region").await;
7428            panic!("{error}");
7429        }
7430        let response_code = bucket.delete().await.unwrap();
7431        assert!(response_code < 300);
7432    }
7433
7434    #[ignore]
7435    #[maybe_async::test(
7436        feature = "sync",
7437        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7438        async(
7439            all(not(feature = "sync"), feature = "with-async-std"),
7440            async_std::test
7441        )
7442    )]
7443    async fn test_bucket_create_delete_non_default_region() {
7444        let config = BucketConfiguration::default();
7445        let response = Bucket::create(
7446            &isolated_test_bucket_name(),
7447            "eu-central-1".parse().unwrap(),
7448            test_aws_credentials(),
7449            config,
7450        )
7451        .await
7452        .unwrap();
7453
7454        let bucket = response.bucket;
7455        let result = check_status(response.response_code, 200, "create bucket").and_then(|()| {
7456            if response.response_text.is_empty() {
7457                Ok(())
7458            } else {
7459                Err(format!(
7460                    "create bucket response body was {:?}",
7461                    response.response_text
7462                ))
7463            }
7464        });
7465        if let Err(error) = result {
7466            report_bucket_cleanup(&bucket, "test_bucket_create_delete_non_default_region").await;
7467            panic!("{error}");
7468        }
7469        let response_code = bucket.delete().await.unwrap();
7470        assert!(response_code < 300);
7471    }
7472
7473    #[ignore]
7474    #[maybe_async::test(
7475        feature = "sync",
7476        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7477        async(
7478            all(not(feature = "sync"), feature = "with-async-std"),
7479            async_std::test
7480        )
7481    )]
7482    async fn test_bucket_create_delete_non_default_region_public() {
7483        let config = BucketConfiguration::public();
7484        let response = Bucket::create(
7485            &isolated_test_bucket_name(),
7486            "eu-central-1".parse().unwrap(),
7487            test_aws_credentials(),
7488            config,
7489        )
7490        .await
7491        .unwrap();
7492
7493        let bucket = response.bucket;
7494        let result = check_status(response.response_code, 200, "create bucket").and_then(|()| {
7495            if response.response_text.is_empty() {
7496                Ok(())
7497            } else {
7498                Err(format!(
7499                    "create bucket response body was {:?}",
7500                    response.response_text
7501                ))
7502            }
7503        });
7504        if let Err(error) = result {
7505            report_bucket_cleanup(
7506                &bucket,
7507                "test_bucket_create_delete_non_default_region_public",
7508            )
7509            .await;
7510            panic!("{error}");
7511        }
7512        let response_code = bucket.delete().await.unwrap();
7513        assert!(response_code < 300);
7514    }
7515
7516    #[test]
7517    fn test_tag_has_key_and_value_functions() {
7518        let key = "key".to_owned();
7519        let value = "value".to_owned();
7520        let tag = Tag { key, value };
7521        assert_eq!["key", tag.key()];
7522        assert_eq!["value", tag.value()];
7523    }
7524
7525    #[test]
7526    #[ignore]
7527    fn test_builder_composition() {
7528        use std::time::Duration;
7529
7530        let bucket = Bucket::new(
7531            "test-bucket",
7532            "eu-central-1".parse().unwrap(),
7533            test_aws_credentials(),
7534        )
7535        .unwrap()
7536        .with_request_timeout(Duration::from_secs(10))
7537        .unwrap();
7538
7539        assert_eq!(bucket.request_timeout(), Some(Duration::from_secs(10)));
7540    }
7541
7542    #[cfg(all(
7543        feature = "with-tokio",
7544        any(
7545            feature = "tokio-native-tls",
7546            feature = "tokio-rustls-tls",
7547            feature = "tokio-rustls-tls-ring"
7548        )
7549    ))]
7550    #[test]
7551    fn with_request_timeout_preserves_tokio_client_options() {
7552        let bucket = Bucket::new(
7553            "test-bucket",
7554            "us-east-1".parse().unwrap(),
7555            Credentials::anonymous().unwrap(),
7556        )
7557        .unwrap()
7558        .set_dangerous_config(true, true)
7559        .unwrap()
7560        .set_proxy(reqwest::Proxy::all("http://127.0.0.1:1234").unwrap())
7561        .unwrap();
7562
7563        let updated = bucket
7564            .with_request_timeout(Duration::from_millis(125))
7565            .unwrap();
7566
7567        assert_eq!(updated.request_timeout(), Some(Duration::from_millis(125)));
7568        assert!(updated.client_options.proxy.is_some());
7569        assert!(updated.client_options.accept_invalid_certs);
7570        assert!(updated.client_options.accept_invalid_hostnames);
7571    }
7572
7573    #[maybe_async::test(
7574        feature = "sync",
7575        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7576        async(
7577            all(not(feature = "sync"), feature = "with-async-std"),
7578            async_std::test
7579        )
7580    )]
7581    #[ignore]
7582    async fn test_bucket_cors() {
7583        let expected_bucket_owner = std::env::var("RUST_S3_TEST_EXPECTED_BUCKET_OWNER")
7584            .expect("set RUST_S3_TEST_EXPECTED_BUCKET_OWNER to the account owning the test bucket");
7585        let created = Bucket::create(
7586            &isolated_test_bucket_name(),
7587            "eu-central-1".parse().unwrap(),
7588            test_aws_credentials(),
7589            BucketConfiguration::default(),
7590        )
7591        .await
7592        .unwrap();
7593        let bucket = created.bucket;
7594        let rule = CorsRule::new(
7595            None,
7596            vec!["GET".to_string()],
7597            vec!["*".to_string()],
7598            None,
7599            None,
7600            None,
7601        );
7602        let cors_config = CorsConfiguration::new(vec![rule]);
7603        let result: Result<(), String> = 'test: {
7604            if let Err(error) = check_status(created.response_code, 200, "create CORS test bucket")
7605            {
7606                break 'test Err(error);
7607            }
7608            let response = match bucket
7609                .put_bucket_cors(&expected_bucket_owner, &cors_config)
7610                .await
7611            {
7612                Ok(response) => response,
7613                Err(error) => break 'test Err(format!("put bucket CORS failed: {error}")),
7614            };
7615            if let Err(error) = check_status(response.status_code(), 200, "put bucket CORS") {
7616                break 'test Err(error);
7617            }
7618
7619            let cors_response = match bucket.get_bucket_cors(&expected_bucket_owner).await {
7620                Ok(response) => response,
7621                Err(error) => break 'test Err(format!("get bucket CORS failed: {error}")),
7622            };
7623            if cors_response != cors_config {
7624                break 'test Err("get bucket CORS returned a different configuration".to_owned());
7625            }
7626
7627            let response = match bucket.delete_bucket_cors(&expected_bucket_owner).await {
7628                Ok(response) => response,
7629                Err(error) => break 'test Err(format!("delete bucket CORS failed: {error}")),
7630            };
7631            check_status(response.status_code(), 204, "delete bucket CORS")
7632        };
7633        if let Err(error) = result {
7634            match bucket.delete_bucket_cors(&expected_bucket_owner).await {
7635                Ok(response) if response.status_code() == 204 => {}
7636                Ok(response) => eprintln!(
7637                    "test_bucket_cors: CORS cleanup returned HTTP {}",
7638                    response.status_code()
7639                ),
7640                Err(cleanup_error) => {
7641                    eprintln!("test_bucket_cors: CORS cleanup failed: {cleanup_error}")
7642                }
7643            }
7644            report_bucket_cleanup(&bucket, "test_bucket_cors").await;
7645            panic!("{error}");
7646        }
7647        let response_code = bucket.delete().await.unwrap();
7648        assert!(response_code < 300);
7649    }
7650
7651    #[maybe_async::test(
7652        feature = "sync",
7653        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7654        async(
7655            all(not(feature = "sync"), feature = "with-async-std"),
7656            async_std::test
7657        )
7658    )]
7659    #[ignore]
7660    async fn test_bucket_lifecycle() {
7661        let created = Bucket::create(
7662            &isolated_test_bucket_name(),
7663            "eu-central-1".parse().unwrap(),
7664            test_aws_credentials(),
7665            BucketConfiguration::default(),
7666        )
7667        .await
7668        .unwrap();
7669        let bucket = created.bucket;
7670
7671        // Create a simple lifecycle rule that expires objects with prefix "test/" after 1 day
7672        let rule = LifecycleRule::builder("Enabled")
7673            .id("test-rule")
7674            .filter(LifecycleFilter {
7675                prefix: Some("test/".to_string()),
7676                ..Default::default()
7677            })
7678            .expiration(Expiration {
7679                days: Some(1),
7680                ..Default::default()
7681            })
7682            .build();
7683
7684        let lifecycle_config = BucketLifecycleConfiguration::new(vec![rule]);
7685
7686        let result: Result<(), String> = 'test: {
7687            if let Err(error) =
7688                check_status(created.response_code, 200, "create lifecycle test bucket")
7689            {
7690                break 'test Err(error);
7691            }
7692            let response = match bucket.put_bucket_lifecycle(lifecycle_config.clone()).await {
7693                Ok(response) => response,
7694                Err(error) => break 'test Err(format!("put bucket lifecycle failed: {error}")),
7695            };
7696            if let Err(error) = check_status(response.status_code(), 200, "put bucket lifecycle") {
7697                break 'test Err(error);
7698            }
7699
7700            let retrieved_config = match bucket.get_bucket_lifecycle().await {
7701                Ok(config) => config,
7702                Err(error) => break 'test Err(format!("get bucket lifecycle failed: {error}")),
7703            };
7704            if retrieved_config.rules.len() != 1
7705                || retrieved_config.rules[0].id != Some("test-rule".to_string())
7706                || retrieved_config.rules[0].status != "Enabled"
7707            {
7708                break 'test Err(format!(
7709                    "get bucket lifecycle returned unexpected rules: {retrieved_config:?}"
7710                ));
7711            }
7712
7713            let response = match bucket.delete_bucket_lifecycle().await {
7714                Ok(response) => response,
7715                Err(error) => break 'test Err(format!("delete bucket lifecycle failed: {error}")),
7716            };
7717            check_status(response.status_code(), 204, "delete bucket lifecycle")
7718        };
7719        if let Err(error) = result {
7720            match bucket.delete_bucket_lifecycle().await {
7721                Ok(response) if response.status_code() == 204 => {}
7722                Ok(response) => eprintln!(
7723                    "test_bucket_lifecycle: lifecycle cleanup returned HTTP {}",
7724                    response.status_code()
7725                ),
7726                Err(cleanup_error) => {
7727                    eprintln!("test_bucket_lifecycle: lifecycle cleanup failed: {cleanup_error}")
7728                }
7729            }
7730            report_bucket_cleanup(&bucket, "test_bucket_lifecycle").await;
7731            panic!("{error}");
7732        }
7733        let response_code = bucket.delete().await.unwrap();
7734        assert!(response_code < 300);
7735    }
7736
7737    #[ignore]
7738    #[cfg(any(
7739        feature = "tokio-native-tls",
7740        feature = "tokio-rustls-tls",
7741        feature = "tokio-rustls-tls-ring"
7742    ))]
7743    #[maybe_async::test(
7744        feature = "sync",
7745        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7746        async(
7747            all(not(feature = "sync"), feature = "with-async-std"),
7748            async_std::test
7749        )
7750    )]
7751    async fn test_bucket_exists_with_dangerous_config() {
7752        init();
7753
7754        // This test verifies that Bucket::exists() honors the dangerous SSL config
7755        // which allows connections with invalid SSL certificates
7756
7757        // Create a bucket with dangerous config enabled
7758        // Note: This test requires a test environment with self-signed or invalid certs
7759        // For CI, we'll test with a regular bucket but verify the config is preserved
7760
7761        let credentials = test_aws_credentials();
7762        let region = "eu-central-1".parse().unwrap();
7763        let bucket_name = "rust-s3-test";
7764
7765        // Create bucket with dangerous config
7766        let bucket = Bucket::new(bucket_name, region, credentials)
7767            .unwrap()
7768            .with_path_style();
7769
7770        // Set dangerous config (allow invalid certs, allow invalid hostnames)
7771        let bucket = bucket.set_dangerous_config(true, true).unwrap();
7772
7773        // Test that exists() works with the dangerous config
7774        // This should not panic or fail due to SSL certificate issues
7775        let exists_result = bucket.exists().await;
7776
7777        // The bucket should exist (assuming test bucket is set up)
7778        assert!(
7779            exists_result.is_ok(),
7780            "Bucket::exists() failed with dangerous config"
7781        );
7782        let exists = exists_result.unwrap();
7783        assert!(exists, "Test bucket should exist");
7784
7785        // Verify that the dangerous config is preserved in the cloned bucket
7786        // by checking if we can perform other operations
7787        let list_result = bucket.list("".to_string(), Some("/".to_string())).await;
7788        assert!(
7789            list_result.is_ok(),
7790            "List operation should work with dangerous config"
7791        );
7792    }
7793
7794    #[ignore]
7795    #[maybe_async::test(
7796        feature = "sync",
7797        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7798        async(
7799            all(not(feature = "sync"), feature = "with-async-std"),
7800            async_std::test
7801        )
7802    )]
7803    async fn test_bucket_exists_without_dangerous_config() {
7804        init();
7805
7806        // This test verifies normal behavior without dangerous config
7807        let credentials = test_aws_credentials();
7808        let region = "eu-central-1".parse().unwrap();
7809        let bucket_name = "rust-s3-test";
7810
7811        // Create bucket without dangerous config
7812        let bucket = Bucket::new(bucket_name, region, credentials)
7813            .unwrap()
7814            .with_path_style();
7815
7816        // Test that exists() works normally
7817        let exists_result = bucket.exists().await;
7818        assert!(
7819            exists_result.is_ok(),
7820            "Bucket::exists() should work without dangerous config"
7821        );
7822        let exists = exists_result.unwrap();
7823        assert!(exists, "Test bucket should exist");
7824    }
7825
7826    #[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
7827    #[maybe_async::test(
7828        feature = "sync",
7829        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
7830        async(
7831            all(not(feature = "sync"), feature = "with-async-std"),
7832            async_std::test
7833        )
7834    )]
7835    async fn credentials_refresh_is_singleflight_and_does_not_hold_read_lock() {
7836        use std::sync::Arc;
7837        use std::sync::atomic::{AtomicUsize, Ordering};
7838        use std::sync::{Mutex, mpsc};
7839        use std::time::Duration;
7840
7841        let mut initial =
7842            Credentials::new(Some("old-access"), Some("old-secret"), None, None, None).unwrap();
7843        initial.expiration = Some(time::OffsetDateTime::from_unix_timestamp(0).unwrap().into());
7844        let bucket = Bucket::new("test-bucket", "us-east-1".parse().unwrap(), initial).unwrap();
7845        let cloned = (*bucket).clone();
7846
7847        let refresh_count = Arc::new(AtomicUsize::new(0));
7848        let (started_sender, started_receiver) = async_std::channel::bounded::<()>(1);
7849        let (release_sender, release_receiver) = mpsc::channel();
7850        let release_receiver = Arc::new(Mutex::new(release_receiver));
7851
7852        let first_count = refresh_count.clone();
7853        let first_started = started_sender.clone();
7854        let first_release = release_receiver.clone();
7855        let first_refresh = bucket.credentials_refresh_with(move |mut credentials| {
7856            first_count.fetch_add(1, Ordering::SeqCst);
7857            let _ = first_started.try_send(());
7858            first_release
7859                .lock()
7860                .unwrap()
7861                .recv_timeout(Duration::from_secs(5))
7862                .expect("test should release the blocked refresh");
7863            credentials.access_key = Some("refreshed-access".into());
7864            credentials.expiration =
7865                Some((time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).into());
7866            Ok(credentials)
7867        });
7868
7869        let second_count = refresh_count.clone();
7870        let second_started = started_sender;
7871        let second_release = release_receiver;
7872        let second_refresh = cloned.credentials_refresh_with(move |mut credentials| {
7873            second_count.fetch_add(1, Ordering::SeqCst);
7874            let _ = second_started.try_send(());
7875            second_release
7876                .lock()
7877                .unwrap()
7878                .recv_timeout(Duration::from_secs(5))
7879                .expect("test should release the blocked refresh");
7880            credentials.access_key = Some("refreshed-access".into());
7881            credentials.expiration =
7882                Some((time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).into());
7883            Ok(credentials)
7884        });
7885
7886        let read_bucket = (*bucket).clone();
7887        let read_during_refresh = async move {
7888            started_receiver.recv().await.unwrap();
7889            let credentials =
7890                async_std::future::timeout(Duration::from_secs(2), read_bucket.credentials())
7891                    .await
7892                    .expect("credential reads should not wait for the blocking refresh")
7893                    .unwrap();
7894            assert_eq!(credentials.access_key.as_deref(), Some("old-access"));
7895            release_sender.send(()).unwrap();
7896            release_sender.send(()).unwrap();
7897        };
7898
7899        let refreshes = futures_util::future::join(first_refresh, second_refresh);
7900        let (refresh_results, ()) = async_std::future::timeout(
7901            Duration::from_secs(8),
7902            futures_util::future::join(refreshes, read_during_refresh),
7903        )
7904        .await
7905        .expect("refresh coordination should finish within the test deadline");
7906        refresh_results.0.unwrap();
7907        refresh_results.1.unwrap();
7908
7909        assert_eq!(refresh_count.load(Ordering::SeqCst), 1);
7910        assert_eq!(
7911            bucket.credentials().await.unwrap().access_key.as_deref(),
7912            Some("refreshed-access")
7913        );
7914    }
7915
7916    #[derive(Clone, Copy)]
7917    enum BucketPolicyOperation {
7918        Get,
7919        Put,
7920        Delete,
7921        #[cfg(feature = "blocking")]
7922        BlockingGet,
7923    }
7924
7925    struct BucketPolicyWireRequest {
7926        method: String,
7927        target: String,
7928        headers: HashMap<String, String>,
7929        body: Vec<u8>,
7930    }
7931
7932    #[maybe_async::maybe_async]
7933    async fn run_bucket_policy_wire_case(
7934        operation: BucketPolicyOperation,
7935        policy: &str,
7936        response_status: u16,
7937        response_body: &str,
7938    ) -> (
7939        Result<ResponseData, S3Error>,
7940        Result<BucketPolicyWireRequest, String>,
7941    ) {
7942        use std::io::{Read, Write};
7943        use std::net::TcpListener;
7944        use std::thread;
7945        use std::time::{Duration, Instant};
7946
7947        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
7948        listener.set_nonblocking(true).unwrap();
7949        let endpoint = format!("http://{}", listener.local_addr().unwrap());
7950        let body = response_body.as_bytes().to_vec();
7951        let server = thread::spawn(move || -> Result<BucketPolicyWireRequest, String> {
7952            let deadline = Instant::now() + Duration::from_secs(10);
7953            let (mut stream, _) = loop {
7954                match listener.accept() {
7955                    Ok((stream, _)) => break (stream, ()),
7956                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
7957                        if Instant::now() >= deadline {
7958                            return Err("bucket policy request accept deadline".to_owned());
7959                        }
7960                        thread::sleep(Duration::from_millis(2));
7961                    }
7962                    Err(error) => return Err(format!("accept policy request: {error}")),
7963                }
7964            };
7965            stream.set_nonblocking(false).map_err(|e| e.to_string())?;
7966            stream
7967                .set_read_timeout(Some(Duration::from_secs(5)))
7968                .map_err(|e| e.to_string())?;
7969            stream
7970                .set_write_timeout(Some(Duration::from_secs(5)))
7971                .map_err(|e| e.to_string())?;
7972
7973            let mut raw_headers = Vec::new();
7974            let mut byte = [0u8; 1];
7975            while !raw_headers.ends_with(b"\r\n\r\n") {
7976                if raw_headers.len() >= 16 * 1024 || Instant::now() >= deadline {
7977                    return Err("bucket policy request headers exceeded bounds".to_owned());
7978                }
7979                stream.read_exact(&mut byte).map_err(|e| e.to_string())?;
7980                raw_headers.push(byte[0]);
7981            }
7982            let header_text = String::from_utf8_lossy(&raw_headers);
7983            let mut lines = header_text.lines();
7984            let request_line = lines.next().unwrap_or_default();
7985            let mut fields = HashMap::new();
7986            for line in lines {
7987                if let Some((name, value)) = line.split_once(':') {
7988                    fields.insert(name.trim().to_ascii_lowercase(), value.trim().to_owned());
7989                }
7990            }
7991            let content_length = fields
7992                .get("content-length")
7993                .and_then(|value| value.parse::<usize>().ok())
7994                .unwrap_or(0);
7995            if content_length > 16 * 1024 {
7996                return Err("bucket policy request body exceeded bound".to_owned());
7997            }
7998            let mut request_body = vec![0; content_length];
7999            stream
8000                .read_exact(&mut request_body)
8001                .map_err(|e| e.to_string())?;
8002
8003            let reason = match response_status {
8004                403 => "Forbidden",
8005                404 => "Not Found",
8006                204 => "No Content",
8007                _ => "OK",
8008            };
8009            write!(
8010                stream,
8011                "HTTP/1.1 {response_status} {reason}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
8012                body.len()
8013            )
8014            .map_err(|e| e.to_string())?;
8015            stream.write_all(&body).map_err(|e| e.to_string())?;
8016
8017            let mut first = request_line.split_whitespace();
8018            Ok(BucketPolicyWireRequest {
8019                method: first.next().unwrap_or_default().to_owned(),
8020                target: first.next().unwrap_or_default().to_owned(),
8021                headers: fields,
8022                body: request_body,
8023            })
8024        });
8025
8026        let credentials = Credentials::new(
8027            Some("test-access-key"),
8028            Some("test-secret-key"),
8029            None,
8030            None,
8031            None,
8032        )
8033        .unwrap();
8034        let bucket = Bucket::new(
8035            "test-bucket",
8036            Region::Custom {
8037                region: "us-east-1".to_owned(),
8038                endpoint,
8039            },
8040            credentials,
8041        )
8042        .unwrap()
8043        .with_path_style()
8044        .with_request_timeout(Duration::from_secs(5))
8045        .unwrap();
8046
8047        let result = match operation {
8048            BucketPolicyOperation::Get => bucket.get_bucket_policy().await,
8049            BucketPolicyOperation::Put => bucket.put_bucket_policy(policy).await,
8050            BucketPolicyOperation::Delete => bucket.delete_bucket_policy().await,
8051            #[cfg(feature = "blocking")]
8052            BucketPolicyOperation::BlockingGet => {
8053                let bucket = bucket.clone();
8054                thread::spawn(move || bucket.get_bucket_policy_blocking())
8055                    .join()
8056                    .expect("blocking bucket policy call panicked")
8057            }
8058        };
8059        let captured = server.join().expect("bucket policy server panicked");
8060        (result, captured)
8061    }
8062
8063    #[maybe_async::test(
8064        feature = "sync",
8065        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
8066        async(
8067            all(not(feature = "sync"), feature = "with-async-std"),
8068            async_std::test
8069        )
8070    )]
8071    async fn bucket_policy_wire_contract_and_errors() {
8072        use base64::Engine as _;
8073        use sha2::Digest as _;
8074
8075        let policy = r#"{"Version":"2012-10-17","Note":"café"}"#;
8076        let json_body = r#"{"Version":"2012-10-17","Statement":[]}"#;
8077        let missing_body = "<Error><Code>NoSuchBucketPolicy</Code></Error>";
8078        let forbidden_body = "<Error><Code>AccessDenied</Code></Error>";
8079
8080        let (get, capture) =
8081            run_bucket_policy_wire_case(BucketPolicyOperation::Get, "", 200, json_body).await;
8082        let get = get.unwrap();
8083        let capture = capture.unwrap();
8084        assert_eq!(get.status_code(), 200);
8085        assert_eq!(get.as_slice(), json_body.as_bytes());
8086        assert_eq!(capture.method, "GET");
8087        assert_eq!(capture.target, "/test-bucket/?policy");
8088        assert!(capture.body.is_empty());
8089        assert_eq!(
8090            capture
8091                .headers
8092                .get("x-amz-content-sha256")
8093                .map(String::as_str),
8094            Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
8095        );
8096        assert!(!capture.headers.contains_key("content-md5"));
8097        let get_authorization = capture.headers.get("authorization").unwrap();
8098        assert!(!get_authorization.contains("content-length"));
8099        assert!(!get_authorization.contains("content-type"));
8100        assert!(capture.headers.contains_key("authorization"));
8101
8102        let (put, capture) =
8103            run_bucket_policy_wire_case(BucketPolicyOperation::Put, policy, 204, "").await;
8104        assert_eq!(put.unwrap().status_code(), 204);
8105        let capture = capture.unwrap();
8106        assert_eq!(capture.method, "PUT");
8107        assert_eq!(capture.target, "/test-bucket/?policy");
8108        assert_eq!(capture.body, policy.as_bytes());
8109        assert_eq!(
8110            capture.headers.get("content-length").map(String::as_str),
8111            Some(policy.len().to_string().as_str())
8112        );
8113        assert_eq!(
8114            capture.headers.get("content-type").map(String::as_str),
8115            Some("application/json")
8116        );
8117        assert_eq!(
8118            capture.headers.get("content-md5").map(String::as_str),
8119            Some(
8120                base64::engine::general_purpose::STANDARD
8121                    .encode(md5::compute(policy.as_bytes()).0)
8122                    .as_str()
8123            )
8124        );
8125        let payload_hash = hex::encode(sha2::Sha256::digest(policy.as_bytes()));
8126        assert_eq!(
8127            capture.headers.get("x-amz-content-sha256"),
8128            Some(&payload_hash)
8129        );
8130        let authorization = capture.headers.get("authorization").unwrap();
8131        assert!(authorization.starts_with("AWS4-HMAC-SHA256 Credential=test-access-key/"));
8132        assert!(
8133            authorization.contains(
8134                "SignedHeaders=content-length;content-md5;content-type;host;x-amz-content-sha256;x-amz-date"
8135            ),
8136            "unexpected SigV4 signed headers: {authorization}"
8137        );
8138
8139        let (delete, capture) =
8140            run_bucket_policy_wire_case(BucketPolicyOperation::Delete, "", 204, "").await;
8141        assert_eq!(delete.unwrap().status_code(), 204);
8142        let capture = capture.unwrap();
8143        assert_eq!(capture.method, "DELETE");
8144        assert_eq!(capture.target, "/test-bucket/?policy");
8145        assert!(capture.body.is_empty());
8146        assert_eq!(
8147            capture
8148                .headers
8149                .get("x-amz-content-sha256")
8150                .map(String::as_str),
8151            Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
8152        );
8153        assert!(!capture.headers.contains_key("content-md5"));
8154        let delete_authorization = capture.headers.get("authorization").unwrap();
8155        assert!(!delete_authorization.contains("content-length"));
8156        assert!(!delete_authorization.contains("content-type"));
8157
8158        let (missing, capture) =
8159            run_bucket_policy_wire_case(BucketPolicyOperation::Get, "", 404, missing_body).await;
8160        if cfg!(feature = "fail-on-err") {
8161            assert!(
8162                matches!(missing, Err(S3Error::HttpFailWithBody(404, body)) if body.contains("NoSuchBucketPolicy"))
8163            );
8164        } else {
8165            let missing = missing.unwrap();
8166            assert_eq!(missing.status_code(), 404);
8167            assert!(missing.as_str().unwrap().contains("NoSuchBucketPolicy"));
8168        }
8169        assert_eq!(capture.unwrap().target, "/test-bucket/?policy");
8170
8171        let (forbidden, _) =
8172            run_bucket_policy_wire_case(BucketPolicyOperation::Get, "", 403, forbidden_body).await;
8173        if cfg!(feature = "fail-on-err") {
8174            assert!(
8175                matches!(forbidden, Err(S3Error::HttpFailWithBody(403, body)) if body.contains("AccessDenied"))
8176            );
8177        } else {
8178            let forbidden = forbidden.unwrap();
8179            assert_eq!(forbidden.status_code(), 403);
8180            assert!(forbidden.as_str().unwrap().contains("AccessDenied"));
8181        }
8182
8183        #[cfg(feature = "blocking")]
8184        {
8185            let (blocking, capture) =
8186                run_bucket_policy_wire_case(BucketPolicyOperation::BlockingGet, "", 200, json_body)
8187                    .await;
8188            assert_eq!(blocking.unwrap().status_code(), 200);
8189            assert_eq!(capture.unwrap().target, "/test-bucket/?policy");
8190        }
8191    }
8192
8193    #[maybe_async::test(
8194        feature = "sync",
8195        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
8196        async(
8197            all(not(feature = "sync"), feature = "with-async-std"),
8198            async_std::test
8199        )
8200    )]
8201    async fn bucket_policy_virtual_host_signing_uses_policy_query() {
8202        use super::RequestImpl;
8203        use crate::command::Command;
8204        use crate::request::Request as _;
8205        use http::header::{AUTHORIZATION, HOST};
8206
8207        let credentials = Credentials::new(
8208            Some("test-access-key"),
8209            Some("test-secret-key"),
8210            None,
8211            None,
8212            None,
8213        )
8214        .unwrap();
8215        let bucket = Bucket::new(
8216            "test-bucket",
8217            Region::Custom {
8218                region: "us-east-1".to_owned(),
8219                endpoint: "http://example.test:9000/base".to_owned(),
8220            },
8221            credentials,
8222        )
8223        .unwrap();
8224        let request = RequestImpl::new(&bucket, "", Command::GetBucketPolicy)
8225            .await
8226            .unwrap();
8227        let headers = request.headers().await.unwrap();
8228        assert_eq!(headers.get(HOST).unwrap(), "test-bucket.example.test:9000");
8229        let authorization = headers.get(AUTHORIZATION).unwrap().to_str().unwrap();
8230        assert!(authorization.starts_with("AWS4-HMAC-SHA256 Credential=test-access-key/"));
8231        assert!(authorization.contains("SignedHeaders=host;x-amz-content-sha256;x-amz-date"));
8232
8233        let canonical = request.canonical_request(&headers).unwrap();
8234        assert!(canonical.contains("GET\n/base/\npolicy=\n"), "{canonical}");
8235    }
8236
8237    #[ignore = "requires a disposable pre-created bucket and explicit provider credentials"]
8238    #[maybe_async::test(
8239        feature = "sync",
8240        async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
8241        async(
8242            all(not(feature = "sync"), feature = "with-async-std"),
8243            async_std::test
8244        )
8245    )]
8246    async fn bucket_policy_provider_roundtrip() {
8247        let bucket_name = env::var("RUST_S3_POLICY_BUCKET")
8248            .expect("RUST_S3_POLICY_BUCKET must name a disposable, pre-created bucket");
8249        let region = env::var("RUST_S3_POLICY_REGION").expect("RUST_S3_POLICY_REGION is required");
8250        let endpoint =
8251            env::var("RUST_S3_POLICY_ENDPOINT").expect("RUST_S3_POLICY_ENDPOINT is required");
8252        let access_key =
8253            env::var("RUST_S3_POLICY_ACCESS_KEY").expect("RUST_S3_POLICY_ACCESS_KEY is required");
8254        let secret_key =
8255            env::var("RUST_S3_POLICY_SECRET_KEY").expect("RUST_S3_POLICY_SECRET_KEY is required");
8256        let policy_json = env::var("RUST_S3_POLICY_JSON").expect("RUST_S3_POLICY_JSON is required");
8257        let expected: serde_json::Value = serde_json::from_str(&policy_json)
8258            .expect("RUST_S3_POLICY_JSON must contain valid JSON");
8259        let credentials =
8260            Credentials::new(Some(&access_key), Some(&secret_key), None, None, None).unwrap();
8261        let mut bucket = Bucket::new(
8262            &bucket_name,
8263            Region::Custom { region, endpoint },
8264            credentials,
8265        )
8266        .unwrap()
8267        .with_path_style();
8268        if let Ok(owner) = env::var("RUST_S3_POLICY_EXPECTED_OWNER") {
8269            bucket.add_header("x-amz-expected-bucket-owner", &owner);
8270        }
8271
8272        let initial = bucket.get_bucket_policy().await;
8273        if cfg!(feature = "fail-on-err") {
8274            assert!(
8275                matches!(initial, Err(S3Error::HttpFailWithBody(404, body)) if body.contains("NoSuchBucketPolicy"))
8276            );
8277        } else {
8278            let initial = initial.unwrap();
8279            assert_eq!(initial.status_code(), 404);
8280            assert!(initial.as_str().unwrap().contains("NoSuchBucketPolicy"));
8281        }
8282
8283        let put = bucket.put_bucket_policy(&policy_json).await.unwrap();
8284        assert!(matches!(put.status_code(), 200 | 204));
8285        let fetched = bucket.get_bucket_policy().await.unwrap();
8286        assert_eq!(fetched.status_code(), 200);
8287        let actual: serde_json::Value = serde_json::from_slice(fetched.as_slice()).unwrap();
8288        assert_eq!(actual, expected);
8289
8290        let deleted = bucket.delete_bucket_policy().await.unwrap();
8291        assert!(matches!(deleted.status_code(), 200 | 204));
8292        let missing = bucket.get_bucket_policy().await;
8293        if cfg!(feature = "fail-on-err") {
8294            assert!(
8295                matches!(missing, Err(S3Error::HttpFailWithBody(404, body)) if body.contains("NoSuchBucketPolicy"))
8296            );
8297        } else {
8298            let missing = missing.unwrap();
8299            assert_eq!(missing.status_code(), 404);
8300            assert!(missing.as_str().unwrap().contains("NoSuchBucketPolicy"));
8301        }
8302    }
8303}