#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ByteRange {
pub(crate) start: usize,
pub(crate) end: usize,
total: usize,
}
impl ByteRange {
fn len(self) -> usize {
self.end - self.start + 1
}
fn content_range(self) -> String {
format!("bytes {}-{}/{}", self.start, self.end, self.total)
}
}
pub(crate) fn object_response(
mut result: GetObjectResult,
range_header: Option<&str>,
query: &BTreeMap<String, String>,
) -> Result<S3HttpResponse, RuntimeError> {
let range = range_header
.map(|value| parse_byte_range(value, result.body.len()))
.transpose()?;
if let Some(range) = range {
result.body = result.body[range.start..=range.end].to_vec();
}
let status = if range.is_some() { 206 } else { 200 };
let encryption = result.metadata.encryption.clone();
let mut response = S3HttpResponse::new(status)
.with_header("content-type", result.metadata.content_type)
.with_header("content-length", result.body.len().to_string())
.with_header("etag", quote_etag(&result.etag))
.with_header(
"last-modified",
result.last_modified_epoch_seconds.to_string(),
)
.with_header("x-amz-version-id", result.version_id)
.with_body(result.body);
if let Some(range) = range {
response
.headers
.insert("content-range".to_string(), range.content_range());
}
if let Some(content_encoding) = result.metadata.content_encoding {
response
.headers
.insert("content-encoding".to_string(), content_encoding);
}
for (key, value) in result.metadata.user_metadata {
response.headers.insert(format!("x-amz-meta-{key}"), value);
}
response = apply_encryption_response_headers(response, encryption.as_ref());
response = apply_replication_status_header(response, result.replication_status.as_deref());
response = apply_response_header_overrides(response, query);
Ok(response)
}
pub(crate) fn head_response(
result: HeadObjectResult,
range_header: Option<&str>,
query: &BTreeMap<String, String>,
) -> Result<S3HttpResponse, RuntimeError> {
let range = range_header
.map(|value| parse_byte_range(value, result.content_length))
.transpose()?;
let status = if range.is_some() { 206 } else { 200 };
let content_length = range
.map(ByteRange::len)
.unwrap_or(result.content_length)
.to_string();
let encryption = result.metadata.encryption.clone();
let mut response = S3HttpResponse::new(status)
.with_header("content-type", result.metadata.content_type)
.with_header("content-length", content_length)
.with_header("etag", quote_etag(&result.etag))
.with_header(
"last-modified",
result.last_modified_epoch_seconds.to_string(),
)
.with_header("x-amz-version-id", result.version_id);
if let Some(range) = range {
response
.headers
.insert("content-range".to_string(), range.content_range());
}
if let Some(content_encoding) = result.metadata.content_encoding {
response
.headers
.insert("content-encoding".to_string(), content_encoding);
}
for (key, value) in result.metadata.user_metadata {
response.headers.insert(format!("x-amz-meta-{key}"), value);
}
response = apply_encryption_response_headers(response, encryption.as_ref());
response = apply_replication_status_header(response, result.replication_status.as_deref());
response = apply_response_header_overrides(response, query);
Ok(response)
}
pub(crate) fn parse_byte_range(value: &str, total: usize) -> Result<ByteRange, RuntimeError> {
let Some(spec) = value.trim().strip_prefix("bytes=") else {
return Err(RuntimeError::InvalidRange(value.to_string()));
};
if total == 0 || spec.contains(',') {
return Err(RuntimeError::InvalidRange(value.to_string()));
}
let Some((start, end)) = spec.split_once('-') else {
return Err(RuntimeError::InvalidRange(value.to_string()));
};
let (start, end) = if start.is_empty() {
let suffix_len = end
.parse::<usize>()
.map_err(|_| RuntimeError::InvalidRange(value.to_string()))?;
if suffix_len == 0 {
return Err(RuntimeError::InvalidRange(value.to_string()));
}
(total.saturating_sub(suffix_len), total - 1)
} else {
let start = start
.parse::<usize>()
.map_err(|_| RuntimeError::InvalidRange(value.to_string()))?;
let end = if end.is_empty() {
total - 1
} else {
end.parse::<usize>()
.map_err(|_| RuntimeError::InvalidRange(value.to_string()))?
};
(start, end.min(total - 1))
};
if start > end || start >= total {
return Err(RuntimeError::InvalidRange(value.to_string()));
}
Ok(ByteRange { start, end, total })
}