Skip to main content

s3_wire/operation/multipart/
identifier.rs

1use std::{fmt, str::FromStr};
2
3/// An opaque, validated multipart upload identifier issued by an S3 service.
4///
5/// Upload identifiers may grant the ability to add parts to or abort an
6/// in-progress upload. Formatting therefore always redacts the value; use
7/// [`UploadId::as_str`] or [`UploadId::expose`] when the wire value is needed.
8#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct UploadId(String);
10
11impl UploadId {
12    /// Maximum accepted UTF-8 byte length for an upload identifier.
13    ///
14    /// S3 treats this value as opaque. The bound prevents untrusted service
15    /// responses from becoming unbounded query parameters while leaving ample
16    /// room for identifiers produced by S3-compatible implementations.
17    pub const MAX_LENGTH: usize = 2_048;
18
19    /// Validates and stores an upload identifier.
20    pub fn new(value: impl Into<String>) -> Result<Self, UploadIdError> {
21        let value = value.into();
22        if value.is_empty() {
23            return Err(UploadIdError::Empty);
24        }
25        if value.len() > Self::MAX_LENGTH {
26            return Err(UploadIdError::TooLong {
27                length: value.len(),
28                maximum: Self::MAX_LENGTH,
29            });
30        }
31        if let Some(index) = value.bytes().position(|byte| byte.is_ascii_control()) {
32            return Err(UploadIdError::AsciiControl { index });
33        }
34        Ok(Self(value))
35    }
36
37    /// Explicitly exposes the identifier for protocol and query construction.
38    pub fn expose(&self) -> &str {
39        &self.0
40    }
41
42    /// Returns the identifier for protocol and query construction.
43    pub fn as_str(&self) -> &str {
44        self.expose()
45    }
46}
47
48impl fmt::Debug for UploadId {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        formatter.write_str("UploadId([REDACTED])")
51    }
52}
53
54impl fmt::Display for UploadId {
55    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56        formatter.write_str("[REDACTED]")
57    }
58}
59
60impl FromStr for UploadId {
61    type Err = UploadIdError;
62
63    fn from_str(value: &str) -> Result<Self, Self::Err> {
64        Self::new(value)
65    }
66}
67
68impl TryFrom<String> for UploadId {
69    type Error = UploadIdError;
70
71    fn try_from(value: String) -> Result<Self, Self::Error> {
72        Self::new(value)
73    }
74}
75
76/// Why a service-issued multipart upload identifier was rejected.
77#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
78pub enum UploadIdError {
79    /// The identifier had no bytes.
80    #[error("multipart upload ID cannot be empty")]
81    Empty,
82    /// The identifier exceeded [`UploadId::MAX_LENGTH`].
83    #[error("multipart upload ID is {length} bytes; the maximum is {maximum}")]
84    TooLong {
85        /// Actual UTF-8 byte length.
86        length: usize,
87        /// Maximum accepted UTF-8 byte length.
88        maximum: usize,
89    },
90    /// The identifier contained a potentially log- or query-confusing ASCII control.
91    #[error("multipart upload ID contains an ASCII control at byte {index}")]
92    AsciiControl {
93        /// Byte offset of the first ASCII control.
94        index: usize,
95    },
96}