use std::fmt::Write;
pub(super) fn encode_key_path(key: &str) -> String {
let mut encoded = String::with_capacity(key.len());
for byte in key.bytes() {
if byte == b'/' || byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~')
{
encoded.push(char::from(byte));
} else {
let _ = write!(encoded, "%{byte:02X}");
}
}
encoded
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::encode_key_path;
use crate::storage::{LocalDriver, Storage};
#[test]
fn key_segments_are_percent_encoded_but_separators_are_kept() {
assert_eq!(
encode_key_path("courses/1/lesson.txt"),
"courses/1/lesson.txt"
);
assert_eq!(encode_key_path("a-b_c.d~e"), "a-b_c.d~e");
assert_eq!(
encode_key_path("reports/Q1 #2?.pdf"),
"reports/Q1%20%232%3F.pdf"
);
assert_eq!(encode_key_path("100%/ação"), "100%25/a%C3%A7%C3%A3o");
assert_eq!(encode_key_path("a+b=c&d:e"), "a%2Bb%3Dc%26d%3Ae");
}
#[test]
fn cloud_urls_keep_reserved_characters_inside_the_object_key() {
let s3 = Storage::s3("assets", "us-east-1");
let r2 = Storage::r2("assets", "account");
assert_eq!(
s3.url("reports/Q1 #2.pdf").unwrap(),
"https://assets.s3.us-east-1.amazonaws.com/reports/Q1%20%232.pdf"
);
assert_eq!(
r2.url("reports/ação?.pdf").unwrap(),
"https://account.r2.cloudflarestorage.com/assets/reports/a%C3%A7%C3%A3o%3F.pdf"
);
}
#[tokio::test]
async fn local_urls_are_public_paths_not_filesystem_paths() {
let absolute = Storage::local("/srv/app/storage");
let relative = Storage::local("storage");
let driver = LocalDriver::new("/srv/app/storage");
for storage in [&absolute, &relative] {
assert_eq!(storage.url("a.png").unwrap(), "/storage/a.png");
assert_eq!(
storage.url("avatars/my photo#1.png").unwrap(),
"/storage/avatars/my%20photo%231.png"
);
}
assert_eq!(
driver.url("avatars/my photo#1.png").await.unwrap(),
absolute.url("avatars/my photo#1.png").unwrap()
);
assert!(!absolute.url("a.png").unwrap().contains("/srv/app"));
}
}