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, Eq, PartialEq)]
186#[non_exhaustive]
187pub enum ChecksumType {
188 Composite,
190 FullObject,
192 Unknown(String),
194}
195
196impl ChecksumType {
197 pub fn as_str(&self) -> &str {
199 match self {
200 Self::Composite => "COMPOSITE",
201 Self::FullObject => "FULL_OBJECT",
202 Self::Unknown(value) => value,
203 }
204 }
205
206 pub(crate) fn parse(value: String) -> Self {
207 match value.as_str() {
208 "COMPOSITE" => Self::Composite,
209 "FULL_OBJECT" => Self::FullObject,
210 _ => Self::Unknown(value),
211 }
212 }
213}
214
215#[derive(Clone, Debug, Default, Eq, PartialEq)]
217pub struct Checksum {
218 pub crc32: Option<String>,
220 pub crc32c: Option<String>,
222 pub crc64_nvme: Option<String>,
224 pub sha1: Option<String>,
226 pub sha256: Option<String>,
228}
229
230#[derive(Clone, Debug, Default, Eq, PartialEq)]
232pub struct RequestIds {
233 pub request_id: Option<String>,
235 pub host_id: Option<String>,
237}
238
239#[derive(Clone)]
244pub struct PresignedUrl(SecretString);
245
246impl PresignedUrl {
247 pub(crate) fn new(url: impl Into<String>) -> Self {
249 Self(url.into().into())
250 }
251
252 pub fn expose(&self) -> &str {
254 self.0.expose_secret()
255 }
256
257 pub fn into_exposed(self) -> String {
259 self.0.expose_secret().to_owned()
260 }
261}
262
263impl fmt::Debug for PresignedUrl {
264 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
265 formatter.write_str("PresignedUrl([REDACTED])")
266 }
267}
268
269impl fmt::Display for PresignedUrl {
270 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
271 formatter.write_str("[REDACTED PRESIGNED URL]")
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use proptest::prelude::*;
279
280 #[test]
281 fn byte_ranges_render_without_off_by_one_changes() {
282 assert_eq!(
283 ByteRange::inclusive(2, 9).unwrap().to_header_value(),
284 "bytes=2-9"
285 );
286 assert_eq!(ByteRange::from(2).to_header_value(), "bytes=2-");
287 assert_eq!(ByteRange::suffix(2).unwrap().to_header_value(), "bytes=-2");
288 assert!(ByteRange::inclusive(9, 2).is_err());
289 assert!(ByteRange::suffix(0).is_none());
290 }
291
292 #[test]
293 fn presigned_url_formatting_is_redacted() {
294 let signed = PresignedUrl::new("https://example.test/key?X-Amz-Signature=secret");
295 assert!(!format!("{signed:?}").contains("secret"));
296 assert!(!signed.to_string().contains("secret"));
297 assert!(signed.expose().contains("secret"));
298 }
299
300 proptest! {
301 #[test]
302 fn valid_object_keys_round_trip(value in ".{1,300}") {
303 prop_assume!(value.len() <= MAX_OBJECT_KEY_BYTES);
304 let key = ObjectKey::new(value.clone()).unwrap();
305 prop_assert_eq!(key.into_string(), value);
306 }
307 }
308}