1use std::{fmt, num::NonZeroU64, str::FromStr};
2
3use secrecy::{ExposeSecret, SecretString};
4
5pub const MAX_OBJECT_KEY_BYTES: usize = 1_024;
7
8#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub struct ObjectKey(String);
14
15impl ObjectKey {
16 pub fn new(value: impl Into<String>) -> Result<Self, ObjectKeyError> {
18 let value = value.into();
19 if value.is_empty() {
20 return Err(ObjectKeyError::Empty);
21 }
22 if value.len() > MAX_OBJECT_KEY_BYTES {
23 return Err(ObjectKeyError::TooLong {
24 actual: value.len(),
25 maximum: MAX_OBJECT_KEY_BYTES,
26 });
27 }
28 Ok(Self(value))
29 }
30
31 pub fn as_str(&self) -> &str {
33 &self.0
34 }
35
36 pub fn into_string(self) -> String {
38 self.0
39 }
40}
41
42impl fmt::Debug for ObjectKey {
43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 formatter.debug_tuple("ObjectKey").field(&self.0).finish()
45 }
46}
47
48impl fmt::Display for ObjectKey {
49 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50 formatter.write_str(&self.0)
51 }
52}
53
54impl AsRef<str> for ObjectKey {
55 fn as_ref(&self) -> &str {
56 self.as_str()
57 }
58}
59
60impl TryFrom<String> for ObjectKey {
61 type Error = ObjectKeyError;
62
63 fn try_from(value: String) -> Result<Self, Self::Error> {
64 Self::new(value)
65 }
66}
67
68impl TryFrom<&str> for ObjectKey {
69 type Error = ObjectKeyError;
70
71 fn try_from(value: &str) -> Result<Self, Self::Error> {
72 Self::new(value)
73 }
74}
75
76impl FromStr for ObjectKey {
77 type Err = ObjectKeyError;
78
79 fn from_str(value: &str) -> Result<Self, Self::Err> {
80 Self::new(value)
81 }
82}
83
84#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
86pub enum ObjectKeyError {
87 #[error("an S3 object key cannot be empty")]
89 Empty,
90 #[error("S3 object key is {actual} bytes; the maximum is {maximum}")]
92 TooLong {
93 actual: usize,
95 maximum: usize,
97 },
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub enum ByteRange {
103 Inclusive {
105 start: u64,
107 end: u64,
109 },
110 From(u64),
112 Suffix(NonZeroU64),
114}
115
116impl ByteRange {
117 pub fn inclusive(start: u64, end: u64) -> Result<Self, RangeError> {
119 if end < start {
120 return Err(RangeError { start, end });
121 }
122 Ok(Self::Inclusive { start, end })
123 }
124
125 pub const fn from(start: u64) -> Self {
127 Self::From(start)
128 }
129
130 pub fn suffix(length: u64) -> Option<Self> {
132 NonZeroU64::new(length).map(Self::Suffix)
133 }
134
135 pub fn to_header_value(self) -> String {
137 match self {
138 Self::Inclusive { start, end } => format!("bytes={start}-{end}"),
139 Self::From(start) => format!("bytes={start}-"),
140 Self::Suffix(length) => format!("bytes=-{length}"),
141 }
142 }
143}
144
145#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
147#[error("range end {end} precedes start {start}")]
148pub struct RangeError {
149 pub start: u64,
151 pub end: u64,
153}
154
155#[derive(Clone, Debug, Default, Eq, PartialEq)]
157pub struct Conditions {
158 pub if_match: Option<String>,
160 pub if_none_match: Option<String>,
162 pub if_modified_since: Option<time::OffsetDateTime>,
164 pub if_unmodified_since: Option<time::OffsetDateTime>,
166}
167
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
170#[non_exhaustive]
171pub enum ChecksumAlgorithm {
172 Crc32,
174 Crc32c,
176 Crc64Nvme,
178 Sha1,
180 Sha256,
182}
183
184#[derive(Clone, Debug, Default, Eq, PartialEq)]
186pub struct Checksum {
187 pub crc32: Option<String>,
189 pub crc32c: Option<String>,
191 pub crc64_nvme: Option<String>,
193 pub sha1: Option<String>,
195 pub sha256: Option<String>,
197}
198
199#[derive(Clone, Debug, Default, Eq, PartialEq)]
201pub struct RequestIds {
202 pub request_id: Option<String>,
204 pub host_id: Option<String>,
206}
207
208#[derive(Clone)]
213pub struct PresignedUrl(SecretString);
214
215impl PresignedUrl {
216 pub(crate) fn new(url: impl Into<String>) -> Self {
218 Self(url.into().into())
219 }
220
221 pub fn expose(&self) -> &str {
223 self.0.expose_secret()
224 }
225
226 pub fn into_exposed(self) -> String {
228 self.0.expose_secret().to_owned()
229 }
230}
231
232impl fmt::Debug for PresignedUrl {
233 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
234 formatter.write_str("PresignedUrl([REDACTED])")
235 }
236}
237
238impl fmt::Display for PresignedUrl {
239 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
240 formatter.write_str("[REDACTED PRESIGNED URL]")
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use proptest::prelude::*;
248
249 #[test]
250 fn byte_ranges_render_without_off_by_one_changes() {
251 assert_eq!(
252 ByteRange::inclusive(2, 9).unwrap().to_header_value(),
253 "bytes=2-9"
254 );
255 assert_eq!(ByteRange::from(2).to_header_value(), "bytes=2-");
256 assert_eq!(ByteRange::suffix(2).unwrap().to_header_value(), "bytes=-2");
257 assert!(ByteRange::inclusive(9, 2).is_err());
258 assert!(ByteRange::suffix(0).is_none());
259 }
260
261 #[test]
262 fn presigned_url_formatting_is_redacted() {
263 let signed = PresignedUrl::new("https://example.test/key?X-Amz-Signature=secret");
264 assert!(!format!("{signed:?}").contains("secret"));
265 assert!(!signed.to_string().contains("secret"));
266 assert!(signed.expose().contains("secret"));
267 }
268
269 proptest! {
270 #[test]
271 fn valid_object_keys_round_trip(value in ".{1,300}") {
272 prop_assume!(value.len() <= MAX_OBJECT_KEY_BYTES);
273 let key = ObjectKey::new(value.clone()).unwrap();
274 prop_assert_eq!(key.into_string(), value);
275 }
276 }
277}