use crate::error::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UploadId(String);
impl UploadId {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl std::str::FromStr for UploadId {
type Err = Error;
fn from_str(id: &str) -> Result<Self, Self::Err> {
if id.is_empty() {
return Err(Error::InvalidUploadId("id is empty".to_string()));
}
if id.len() > MAX_UPLOAD_ID_LEN {
return Err(Error::InvalidUploadId(format!(
"id is {} bytes; max {}",
id.len(),
MAX_UPLOAD_ID_LEN,
)));
}
for c in id.chars() {
if c == '/' || c == '\\' {
return Err(Error::InvalidUploadId(
"id contains a path separator".to_string(),
));
}
if c.is_control() {
return Err(Error::InvalidUploadId(
"id contains a control character".to_string(),
));
}
}
Ok(Self(id.to_string()))
}
}
impl TryFrom<&str> for UploadId {
type Error = Error;
fn try_from(id: &str) -> Result<Self, Self::Error> {
id.parse()
}
}
impl TryFrom<String> for UploadId {
type Error = Error;
fn try_from(id: String) -> Result<Self, Self::Error> {
id.parse::<Self>()?;
Ok(Self(id))
}
}
impl AsRef<str> for UploadId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::fmt::Display for UploadId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
const MAX_UPLOAD_ID_LEN: usize = 256;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_typical_ids() {
for id in [
"550e8400-e29b-41d4-a716-446655440000", "01H8XGJWBWBAQ4SHN3JPHQM6JZ", "2YHHb9hVTQR1Etu6P3kY83bp1Wd", "my-custom-slug_42.with.dots",
"a", "Любой-юникод", ] {
assert!(
id.parse::<UploadId>().is_ok(),
"expected {id:?} to be accepted"
);
}
}
#[test]
fn rejects_empty() {
let err = "".parse::<UploadId>().unwrap_err();
assert!(matches!(err, Error::InvalidUploadId(_)));
assert_eq!(err.status_code(), 400);
}
#[test]
fn rejects_oversized() {
let id = "A".repeat(MAX_UPLOAD_ID_LEN + 1);
let err = id.parse::<UploadId>().unwrap_err();
assert!(matches!(err, Error::InvalidUploadId(_)));
let just_fits = "A".repeat(MAX_UPLOAD_ID_LEN);
assert!(just_fits.parse::<UploadId>().is_ok());
}
#[test]
fn rejects_path_separators() {
for id in ["foo/bar", "../etc/passwd", "foo\\bar", "..\\windows"] {
let err = id.parse::<UploadId>().unwrap_err();
assert!(
matches!(err, Error::InvalidUploadId(_)),
"expected {id:?} to be rejected, got {err:?}"
);
}
}
#[test]
fn rejects_control_characters() {
for id in ["foo\0bar", "foo\nbar", "foo\rbar", "foo\tbar", "\x7Fid"] {
let err = id.parse::<UploadId>().unwrap_err();
assert!(
matches!(err, Error::InvalidUploadId(_)),
"expected {id:?} to be rejected, got {err:?}"
);
}
}
#[test]
fn dotdot_alone_is_allowed() {
assert!("..".parse::<UploadId>().is_ok());
assert!("...json".parse::<UploadId>().is_ok());
}
#[test]
fn upload_id_parses_valid_id() {
let id: UploadId = "my-custom-slug_42.with.dots".parse().unwrap();
assert_eq!(id.as_str(), "my-custom-slug_42.with.dots");
}
#[test]
fn upload_id_rejects_invalid_id() {
let err = "foo/bar".parse::<UploadId>().unwrap_err();
assert!(matches!(err, Error::InvalidUploadId(_)));
}
}