use serde::{Deserialize, Serialize};
pub(crate) fn validate_storage_key(key: &str) -> Result<(), &'static str> {
if key.is_empty() {
return Ok(());
}
if key.starts_with('/') {
return Err("storage key must be relative (must not start with `/`)");
}
if key.contains('\\') {
return Err("storage key must not contain backslashes");
}
if key.chars().any(|c| c.is_control()) {
return Err("storage key must not contain control characters");
}
if key.split('/').any(|segment| segment == "..") {
return Err("storage key must not contain a `..` path segment");
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct FileField(String);
impl FileField {
pub fn key(&self) -> &str {
&self.0
}
pub fn url(&self) -> String {
crate::storage::storage_opt()
.map(|s| s.url(self.key()))
.unwrap_or_else(|| self.0.clone())
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<String> for FileField {
fn from(key: String) -> Self {
FileField(key)
}
}
impl From<&str> for FileField {
fn from(key: &str) -> Self {
FileField(key.to_string())
}
}
impl AsRef<str> for FileField {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for FileField {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct ImageField(FileField);
impl std::ops::Deref for ImageField {
type Target = FileField;
fn deref(&self) -> &FileField {
&self.0
}
}
impl From<String> for ImageField {
fn from(key: String) -> Self {
ImageField(FileField::from(key))
}
}
impl From<&str> for ImageField {
fn from(key: &str) -> Self {
ImageField(FileField::from(key))
}
}
impl AsRef<str> for ImageField {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl std::fmt::Display for ImageField {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
macro_rules! impl_string_newtype {
($ty:ty) => {
impl Serialize for $ty {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(self.key())
}
}
impl<'de> Deserialize<'de> for $ty {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let key = String::deserialize(d)?;
crate::orm::file_field::validate_storage_key(&key)
.map_err(serde::de::Error::custom)?;
Ok(<$ty>::from(key))
}
}
impl sqlx::Type<sqlx::Sqlite> for $ty {
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
<String as sqlx::Type<sqlx::Sqlite>>::type_info()
}
fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool {
<String as sqlx::Type<sqlx::Sqlite>>::compatible(ty)
}
}
impl sqlx::Type<sqlx::Postgres> for $ty {
fn type_info() -> sqlx::postgres::PgTypeInfo {
<String as sqlx::Type<sqlx::Postgres>>::type_info()
}
fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
<String as sqlx::Type<sqlx::Postgres>>::compatible(ty)
}
}
impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for $ty {
fn decode(
value: sqlx::sqlite::SqliteValueRef<'r>,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let s = <String as sqlx::Decode<sqlx::Sqlite>>::decode(value)?;
Ok(<$ty>::from(s))
}
}
impl<'r> sqlx::Decode<'r, sqlx::Postgres> for $ty {
fn decode(
value: sqlx::postgres::PgValueRef<'r>,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let s = <String as sqlx::Decode<sqlx::Postgres>>::decode(value)?;
Ok(<$ty>::from(s))
}
}
impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for $ty {
fn encode_by_ref(
&self,
buf: &mut <sqlx::Sqlite as sqlx::Database>::ArgumentBuffer<'q>,
) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync>> {
<String as sqlx::Encode<'q, sqlx::Sqlite>>::encode_by_ref(
&self.key().to_string(),
buf,
)
}
}
impl<'q> sqlx::Encode<'q, sqlx::Postgres> for $ty {
fn encode_by_ref(
&self,
buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'q>,
) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync>> {
<String as sqlx::Encode<'q, sqlx::Postgres>>::encode_by_ref(
&self.key().to_string(),
buf,
)
}
}
};
}
impl_string_newtype!(FileField);
impl_string_newtype!(ImageField);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normal_relative_keys_deserialize() {
for key in ["ab12-photo.jpg", "posts/ab12-photo.jpg", "photo..jpg", ""] {
let json = serde_json::to_string(key).unwrap();
let f: FileField = serde_json::from_str(&json)
.unwrap_or_else(|e| panic!("key {key:?} should deserialize: {e}"));
assert_eq!(f.key(), key);
let i: ImageField = serde_json::from_str(&json)
.unwrap_or_else(|e| panic!("key {key:?} should deserialize: {e}"));
assert_eq!(i.key(), key);
}
}
#[test]
fn malicious_keys_are_rejected() {
let bad = [
"/etc/passwd", "../secret.jpg", "posts/../../secret", "..", "a\\b.jpg", "a\nb.jpg", ];
for key in bad {
let json = serde_json::to_string(key).unwrap();
assert!(
serde_json::from_str::<FileField>(&json).is_err(),
"FileField accepted malicious key {key:?}"
);
assert!(
serde_json::from_str::<ImageField>(&json).is_err(),
"ImageField accepted malicious key {key:?}"
);
}
}
}