use alloc::string::{String, ToString};
use super::types::{HttpMethod, HttpRequest, S3Operation, S3Request};
pub fn parse_s3_request(http_req: &HttpRequest) -> S3Request {
let (bucket, key) = parse_path(&http_req.path);
let operation = determine_operation(http_req, bucket.as_deref(), key.as_deref());
S3Request {
method: http_req.method,
bucket,
key,
operation,
headers: http_req.headers.clone(),
query: http_req.query.clone(),
body: http_req.body.clone(),
}
}
fn parse_path(path: &str) -> (Option<String>, Option<String>) {
let path = path.trim_start_matches('/');
if path.is_empty() {
return (None, None);
}
if let Some(idx) = path.find('/') {
let bucket = path[..idx].to_string();
let key = path[idx + 1..].to_string();
if key.is_empty() {
(Some(bucket), None)
} else {
(Some(bucket), Some(key))
}
} else {
(Some(path.to_string()), None)
}
}
fn determine_operation(req: &HttpRequest, bucket: Option<&str>, key: Option<&str>) -> S3Operation {
match (req.method, bucket, key) {
(HttpMethod::Get, None, None) => S3Operation::ListBuckets,
(HttpMethod::Put, Some(_), None) => S3Operation::CreateBucket,
(HttpMethod::Delete, Some(_), None) => {
if req.query.contains_key("delete") {
S3Operation::DeleteObjects
} else {
S3Operation::DeleteBucket
}
}
(HttpMethod::Head, Some(_), None) => S3Operation::HeadBucket,
(HttpMethod::Get, Some(_), None) => {
if req.query.contains_key("location") {
S3Operation::GetBucketLocation
} else if req.query.contains_key("versioning") {
S3Operation::GetBucketVersioning
} else if req.query.contains_key("versions") {
S3Operation::ListObjectVersions
} else if req.query.contains_key("uploads") {
S3Operation::ListMultipartUploads
} else {
S3Operation::ListObjectsV2
}
}
(HttpMethod::Put, Some(_), None) if req.query.contains_key("versioning") => {
S3Operation::PutBucketVersioning
}
(HttpMethod::Put, Some(_), Some(_)) => {
if req
.headers
.iter()
.any(|(k, _)| k.to_lowercase() == "x-amz-copy-source")
{
return S3Operation::CopyObject;
}
if let Some(upload_id) = req.query.get("uploadId") {
if !upload_id.is_empty() {
return S3Operation::UploadPart;
}
}
S3Operation::PutObject
}
(HttpMethod::Get, Some(_), Some(_)) => {
if req.query.contains_key("uploadId") {
S3Operation::ListParts
} else {
S3Operation::GetObject
}
}
(HttpMethod::Delete, Some(_), Some(_)) => {
if req.query.contains_key("uploadId") {
S3Operation::AbortMultipartUpload
} else {
S3Operation::DeleteObject
}
}
(HttpMethod::Head, Some(_), Some(_)) => S3Operation::HeadObject,
(HttpMethod::Post, Some(_), Some(_)) => {
if req.query.contains_key("uploads") {
S3Operation::CreateMultipartUpload
} else if req.query.contains_key("uploadId") {
S3Operation::CompleteMultipartUpload
} else {
S3Operation::Unknown("POST object".into())
}
}
(HttpMethod::Post, Some(_), None) => {
if req.query.contains_key("delete") {
S3Operation::DeleteObjects
} else {
S3Operation::Unknown("POST bucket".into())
}
}
_ => S3Operation::Unknown(alloc::format!(
"{} /{}/{}",
req.method.as_str(),
bucket.unwrap_or(""),
key.unwrap_or("")
)),
}
}
pub fn extract_bucket_from_host(host: &str, endpoint_suffix: &str) -> Option<String> {
if let Some(prefix) = host.strip_suffix(endpoint_suffix) {
let bucket = prefix.trim_end_matches('.');
if !bucket.is_empty() {
return Some(bucket.to_string());
}
}
None
}
pub fn normalize_request(req: &mut S3Request, endpoint_suffix: &str) {
if req.bucket.is_none() {
if let Some(host) = req.header("host") {
if let Some(bucket) = extract_bucket_from_host(host, endpoint_suffix) {
req.bucket = Some(bucket);
if req.key.is_none() {
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::collections::BTreeMap;
fn make_request(method: HttpMethod, path: &str) -> HttpRequest {
HttpRequest {
method,
path: path.into(),
query: BTreeMap::new(),
headers: BTreeMap::new(),
body: alloc::vec![],
}
}
fn make_request_with_query(
method: HttpMethod,
path: &str,
query: &[(&str, &str)],
) -> HttpRequest {
let mut q = BTreeMap::new();
for (k, v) in query {
q.insert(k.to_string(), v.to_string());
}
HttpRequest {
method,
path: path.into(),
query: q,
headers: BTreeMap::new(),
body: alloc::vec![],
}
}
#[test]
fn test_parse_path_root() {
let (bucket, key) = parse_path("/");
assert!(bucket.is_none());
assert!(key.is_none());
}
#[test]
fn test_parse_path_bucket_only() {
let (bucket, key) = parse_path("/mybucket");
assert_eq!(bucket, Some("mybucket".into()));
assert!(key.is_none());
}
#[test]
fn test_parse_path_bucket_trailing_slash() {
let (bucket, key) = parse_path("/mybucket/");
assert_eq!(bucket, Some("mybucket".into()));
assert!(key.is_none());
}
#[test]
fn test_parse_path_bucket_and_key() {
let (bucket, key) = parse_path("/mybucket/mykey");
assert_eq!(bucket, Some("mybucket".into()));
assert_eq!(key, Some("mykey".into()));
}
#[test]
fn test_parse_path_nested_key() {
let (bucket, key) = parse_path("/mybucket/folder/subfolder/file.txt");
assert_eq!(bucket, Some("mybucket".into()));
assert_eq!(key, Some("folder/subfolder/file.txt".into()));
}
#[test]
fn test_list_buckets() {
let req = make_request(HttpMethod::Get, "/");
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::ListBuckets);
}
#[test]
fn test_create_bucket() {
let req = make_request(HttpMethod::Put, "/mybucket");
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::CreateBucket);
assert_eq!(s3_req.bucket, Some("mybucket".into()));
}
#[test]
fn test_delete_bucket() {
let req = make_request(HttpMethod::Delete, "/mybucket");
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::DeleteBucket);
}
#[test]
fn test_head_bucket() {
let req = make_request(HttpMethod::Head, "/mybucket");
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::HeadBucket);
}
#[test]
fn test_list_objects() {
let req = make_request(HttpMethod::Get, "/mybucket");
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::ListObjectsV2);
}
#[test]
fn test_list_objects_with_prefix() {
let req = make_request_with_query(HttpMethod::Get, "/mybucket", &[("prefix", "folder/")]);
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::ListObjectsV2);
}
#[test]
fn test_get_bucket_location() {
let req = make_request_with_query(HttpMethod::Get, "/mybucket", &[("location", "")]);
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::GetBucketLocation);
}
#[test]
fn test_get_bucket_versioning() {
let req = make_request_with_query(HttpMethod::Get, "/mybucket", &[("versioning", "")]);
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::GetBucketVersioning);
}
#[test]
fn test_put_object() {
let req = make_request(HttpMethod::Put, "/mybucket/mykey");
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::PutObject);
assert_eq!(s3_req.bucket, Some("mybucket".into()));
assert_eq!(s3_req.key, Some("mykey".into()));
}
#[test]
fn test_get_object() {
let req = make_request(HttpMethod::Get, "/mybucket/mykey");
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::GetObject);
}
#[test]
fn test_delete_object() {
let req = make_request(HttpMethod::Delete, "/mybucket/mykey");
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::DeleteObject);
}
#[test]
fn test_head_object() {
let req = make_request(HttpMethod::Head, "/mybucket/mykey");
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::HeadObject);
}
#[test]
fn test_copy_object() {
let mut req = make_request(HttpMethod::Put, "/mybucket/newkey");
req.headers
.insert("x-amz-copy-source".into(), "/otherbucket/oldkey".into());
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::CopyObject);
}
#[test]
fn test_create_multipart() {
let req = make_request_with_query(HttpMethod::Post, "/mybucket/mykey", &[("uploads", "")]);
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::CreateMultipartUpload);
}
#[test]
fn test_upload_part() {
let req = make_request_with_query(
HttpMethod::Put,
"/mybucket/mykey",
&[("uploadId", "abc123"), ("partNumber", "1")],
);
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::UploadPart);
}
#[test]
fn test_complete_multipart() {
let req = make_request_with_query(
HttpMethod::Post,
"/mybucket/mykey",
&[("uploadId", "abc123")],
);
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::CompleteMultipartUpload);
}
#[test]
fn test_abort_multipart() {
let req = make_request_with_query(
HttpMethod::Delete,
"/mybucket/mykey",
&[("uploadId", "abc123")],
);
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::AbortMultipartUpload);
}
#[test]
fn test_list_parts() {
let req = make_request_with_query(
HttpMethod::Get,
"/mybucket/mykey",
&[("uploadId", "abc123")],
);
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::ListParts);
}
#[test]
fn test_delete_objects() {
let req = make_request_with_query(HttpMethod::Post, "/mybucket", &[("delete", "")]);
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::DeleteObjects);
}
#[test]
fn test_list_object_versions() {
let req = make_request_with_query(HttpMethod::Get, "/mybucket", &[("versions", "")]);
let s3_req = parse_s3_request(&req);
assert_eq!(s3_req.operation, S3Operation::ListObjectVersions);
}
#[test]
fn test_extract_bucket_from_host() {
assert_eq!(
extract_bucket_from_host("mybucket.s3.amazonaws.com", "s3.amazonaws.com"),
Some("mybucket".into())
);
assert_eq!(
extract_bucket_from_host("s3.amazonaws.com", "s3.amazonaws.com"),
None
);
assert_eq!(
extract_bucket_from_host("mybucket.localhost", "localhost"),
Some("mybucket".into())
);
}
}