about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml2
-rw-r--r--src/media/mod.rs32
-rw-r--r--src/micropub/mod.rs173
-rw-r--r--templates-neo/Cargo.toml2
-rw-r--r--templates/Cargo.toml2
-rw-r--r--util/Cargo.toml2
-rw-r--r--util/src/micropub.rs33
8 files changed, 131 insertions, 117 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c9f8ac2..f45a2bb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1860,7 +1860,7 @@ dependencies = [
 
 [[package]]
 name = "kittybox-util"
-version = "0.2.0"
+version = "0.3.0"
 dependencies = [
  "axum-core",
  "futures-util",
diff --git a/Cargo.toml b/Cargo.toml
index 001f2b1..fd6311f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -57,7 +57,7 @@ required-features = ["sqlparser"]
 members = [".", "./util", "./templates", "./indieauth", "./templates-neo", "./tower-watchdog"]
 default-members = [".", "./util", "./templates", "./indieauth"]
 [dependencies.kittybox-util]
-version = "0.2.0"
+version = "0.3.0"
 path = "./util"
 features = ["fs", "axum"]
 [dependencies.kittybox-frontend-renderer]
diff --git a/src/media/mod.rs b/src/media/mod.rs
index 32b8405..3ed6810 100644
--- a/src/media/mod.rs
+++ b/src/media/mod.rs
@@ -13,10 +13,10 @@ pub use storage::file::FileStore;
 
 impl From<MediaStoreError> for MicropubError {
     fn from(err: MediaStoreError) -> Self {
-        Self {
-            error: ErrorType::InternalServerError,
-            error_description: format!("{}", err)
-        }
+        Self::new(
+            ErrorType::InternalServerError,
+            format!("media store error: {}", err)
+        )
     }
 }
 
@@ -27,25 +27,25 @@ pub(crate) async fn upload<S: MediaStore, A: AuthBackend>(
     mut upload: Multipart
 ) -> Response {
     if !user.check_scope(&Scope::Media) {
-        return MicropubError {
-            error: ErrorType::NotAuthorized,
-            error_description: "Interacting with the media storage requires the \"media\" scope.".to_owned()
-        }.into_response();
+        return MicropubError::from_static(
+            ErrorType::NotAuthorized,
+            "Interacting with the media storage requires the \"media\" scope."
+        ).into_response();
     }
     let host = user.me.authority();
     let field = match upload.next_field().await {
         Ok(Some(field)) => field,
         Ok(None) => {
-            return MicropubError {
-                error: ErrorType::InvalidRequest,
-                error_description: "Send multipart/form-data with one field named file".to_owned()
-            }.into_response();
+            return MicropubError::from_static(
+                ErrorType::InvalidRequest,
+                "Send multipart/form-data with one field named file"
+            ).into_response();
         },
         Err(err) => {
-            return MicropubError {
-                error: ErrorType::InternalServerError,
-                error_description: format!("Error while parsing multipart/form-data: {}", err)
-            }.into_response();
+            return MicropubError::new(
+                ErrorType::InternalServerError,
+                format!("Error while parsing multipart/form-data: {}", err)
+            ).into_response();
         },
     };
     let metadata: Metadata = (&field).into();
diff --git a/src/micropub/mod.rs b/src/micropub/mod.rs
index 9838e5a..663702f 100644
--- a/src/micropub/mod.rs
+++ b/src/micropub/mod.rs
@@ -28,13 +28,13 @@ pub struct MicropubQuery {
 
 impl From<StorageError> for MicropubError {
     fn from(err: StorageError) -> Self {
-        Self {
-            error: match err.kind() {
+        Self::new(
+            match err.kind() {
                 crate::database::ErrorKind::NotFound => ErrorKind::NotFound,
                 _ => ErrorKind::InternalServerError,
             },
-            error_description: format!("Backend error: {}", err),
-        }
+            format!("backend error: {}", err)
+        )
     }
 }
 
@@ -246,11 +246,10 @@ pub(crate) async fn _post<D: 'static + Storage>(
 
     // Security check! Do we have an OAuth2 scope to proceed?
     if !user.check_scope(&Scope::Create) {
-        return Err(MicropubError {
-            error: ErrorKind::InvalidScope,
-            error_description: "Not enough privileges - try acquiring the \"create\" scope."
-                .to_owned(),
-        });
+        return Err(MicropubError::from_static(
+            ErrorKind::InvalidScope,
+            "Not enough privileges - try acquiring the \"create\" scope."
+        ));
     }
 
     // Security check #2! Are we posting to our own website?
@@ -261,18 +260,18 @@ pub(crate) async fn _post<D: 'static + Storage>(
             .iter()
             .any(|url| !url.as_str().unwrap().starts_with(user.me.as_str()))
     {
-        return Err(MicropubError {
-            error: ErrorKind::Forbidden,
-            error_description: "You're posting to a website that's not yours.".to_owned(),
-        });
+        return Err(MicropubError::from_static(
+            ErrorKind::Forbidden,
+            "You're posting to a website that's not yours."
+        ));
     }
 
     // Security check #3! Are we overwriting an existing document?
     if db.post_exists(&uid).await? {
-        return Err(MicropubError {
-            error: ErrorKind::AlreadyExists,
-            error_description: "UID clash was detected, operation aborted.".to_owned(),
-        });
+        return Err(MicropubError::from_static(
+            ErrorKind::AlreadyExists,
+            "UID clash was detected, operation aborted."
+        ));
     }
     // Save the post
     tracing::debug!("Saving post to database...");
@@ -365,20 +364,20 @@ impl MicropubUpdate {
             if add.iter().map(|(k, _)| k.as_str()).any(|k| {
                 k.to_lowercase().as_str() == "uid"
             }) {
-                return Err(MicropubError {
-                    error: ErrorKind::InvalidRequest,
-                    error_description: "Update cannot modify the post UID".to_owned()
-                });
+                return Err(MicropubError::from_static(
+                    ErrorKind::InvalidRequest,
+                    "Update cannot modify the post UID"
+                ));
             }
         }
         if let Some(replace) = &self.replace {
             if replace.iter().map(|(k, v)| k.as_str()).any(|k| {
                 k.to_lowercase().as_str() == "uid"
             }) {
-                return Err(MicropubError {
-                    error: ErrorKind::InvalidRequest,
-                    error_description: "Update cannot modify the post UID".to_owned()
-                });
+                return Err(MicropubError::from_static(
+                    ErrorKind::InvalidRequest,
+                    "Update cannot modify the post UID"
+                ));
             }
         }
         let iter = match &self.delete {
@@ -392,10 +391,10 @@ impl MicropubUpdate {
         };
         if let Some(mut iter) = iter {
             if iter.any(|k| k.to_lowercase().as_str() == "uid") {
-                return Err(MicropubError {
-                    error: ErrorKind::InvalidRequest,
-                    error_description: "Update cannot modify the post UID".to_owned()
-                });
+                return Err(MicropubError::from_static(
+                    ErrorKind::InvalidRequest,
+                    "Update cannot modify the post UID"
+                ));
             }
         }
         Ok(())
@@ -456,13 +455,12 @@ async fn post_action<D: Storage, A: AuthBackend>(
     db: D,
     user: User<A>,
 ) -> Result<(), MicropubError> {
-    let uri = if let Ok(uri) = action.url.parse::<hyper::Uri>() {
-        uri
-    } else {
-        return Err(MicropubError {
-            error: ErrorKind::InvalidRequest,
-            error_description: "Your URL doesn't parse properly.".to_owned(),
-        });
+    let uri = match action.url.parse::<hyper::Uri>() {
+        Ok(uri) => uri,
+        Err(err) => return Err(MicropubError::new(
+            ErrorKind::InvalidRequest,
+            format!("url parsing error: {}", err)
+        ))
     };
 
     if uri.authority().unwrap()
@@ -474,38 +472,38 @@ async fn post_action<D: Storage, A: AuthBackend>(
             .authority()
             .unwrap()
     {
-        return Err(MicropubError {
-            error: ErrorKind::Forbidden,
-            error_description: "Don't tamper with others' posts!".to_owned(),
-        });
+        return Err(MicropubError::from_static(
+            ErrorKind::Forbidden,
+            "Don't tamper with others' posts!"
+        ));
     }
 
     match action.action {
         ActionType::Delete => {
             if !user.check_scope(&Scope::Delete) {
-                return Err(MicropubError {
-                    error: ErrorKind::InvalidScope,
-                    error_description: "You need a \"delete\" scope for this.".to_owned(),
-                });
+                return Err(MicropubError::from_static(
+                    ErrorKind::InvalidScope,
+                    "You need a \"delete\" scope for this."
+                ));
             }
 
             db.delete_post(&action.url).await?
         }
         ActionType::Update => {
             if !user.check_scope(&Scope::Update) {
-                return Err(MicropubError {
-                    error: ErrorKind::InvalidScope,
-                    error_description: "You need an \"update\" scope for this.".to_owned(),
-                });
+                return Err(MicropubError::from_static(
+                    ErrorKind::InvalidScope,
+                    "You need an \"update\" scope for this."
+                ));
             }
 
             let update = if let Some(update) = action.update {
                 update
             } else {
-                return Err(MicropubError {
-                    error: ErrorKind::InvalidRequest,
-                    error_description: "Update request is not set.".to_owned(),
-                })
+                return Err(MicropubError::from_static(
+                    ErrorKind::InvalidRequest,
+                    "Update request is not set."
+                ));
             };
 
             update.check_validity()?;
@@ -546,18 +544,18 @@ async fn dispatch_body(
         } else if let Ok(body) = serde_json::from_slice::<serde_json::Value>(&body) {
             // quick sanity check
             if !body.is_object() || !body["type"].is_array() {
-                return Err(MicropubError {
-                    error: ErrorKind::InvalidRequest,
-                    error_description: "Invalid MF2-JSON detected: `.` should be an object, `.type` should be an array of MF2 types".to_owned()
-                });
+                return Err(MicropubError::from_static(
+                    ErrorKind::InvalidRequest,
+                    "Invalid MF2-JSON detected: `.` should be an object, `.type` should be an array of MF2 types"
+                ));
             }
 
             Ok(PostBody::MF2(body))
         } else {
-            Err(MicropubError {
-                error: ErrorKind::InvalidRequest,
-                error_description: "Invalid JSON object passed.".to_owned(),
-            })
+            Err(MicropubError::from_static(
+                ErrorKind::InvalidRequest,
+                "Invalid JSON object passed."
+            ))
         }
     } else if content_type == ContentType::form_url_encoded() {
         if let Ok(body) = serde_urlencoded::from_bytes::<MicropubFormAction>(&body) {
@@ -565,14 +563,13 @@ async fn dispatch_body(
         } else if let Ok(body) = serde_urlencoded::from_bytes::<Vec<(String, String)>>(&body) {
             Ok(PostBody::MF2(form_to_mf2_json(body)))
         } else {
-            Err(MicropubError {
-                error: ErrorKind::InvalidRequest,
-                error_description: "Invalid form-encoded data. Try h=entry&content=Hello!"
-                    .to_owned(),
-            })
+            Err(MicropubError::from_static(
+                ErrorKind::InvalidRequest,
+                "Invalid form-encoded data. Try h=entry&content=Hello!"
+            ))
         }
     } else {
-        Err(MicropubError::new(
+        Err(MicropubError::from_static(
             ErrorKind::UnsupportedMediaType,
             "This Content-Type is not recognized. Try application/json instead?",
         ))
@@ -616,7 +613,7 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
     let query = if let Some(Query(query)) = query {
         query
     } else {
-        return MicropubError::new(
+        return MicropubError::from_static(
             ErrorKind::InvalidRequest,
             "Invalid query provided. Try ?q=config to see what you can do."
         ).into_response();
@@ -628,7 +625,7 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
         .unwrap()
         != &host
     {
-        return MicropubError::new(
+        return MicropubError::from_static(
             ErrorKind::NotAuthorized,
             "This website doesn't belong to you.",
         )
@@ -644,9 +641,9 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
                 Err(err) => {
                     return MicropubError::new(
                         ErrorKind::InternalServerError,
-                        &format!("Error fetching channels: {}", err),
+                        format!("Error fetching channels: {}", err),
                     )
-                    .into_response()
+                        .into_response()
                 }
             };
 
@@ -668,36 +665,36 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
                     map
                 }
             })
-            .into_response()
+                .into_response()
         }
         QueryType::Source => {
             match query.url {
                 Some(url) => {
                     match db.get_post(&url).await {
                         Ok(some) => match some {
-                            Some(post) => axum::response::Json(&post).into_response(),
-                            None => MicropubError::new(
+                            Some(post) => {
+                                let mut response = axum::response::Json(&post).into_response();
+
+                                response
+                            },
+                            None => MicropubError::from_static(
                                 ErrorKind::NotFound,
                                 "The specified MF2 object was not found in database.",
                             )
-                            .into_response(),
+                                .into_response(),
                         },
-                        Err(err) => MicropubError::new(
-                            ErrorKind::InternalServerError,
-                            &format!("Backend error: {}", err),
-                        )
-                        .into_response(),
+                        Err(err) => MicropubError::from(err).into_response(),
                     }
                 }
                 None => {
                     // Here, one should probably attempt to query at least the main feed and collect posts
                     // Using a pre-made query function can't be done because it does unneeded filtering
                     // Don't implement for now, this is optional
-                    MicropubError::new(
+                    MicropubError::from_static(
                         ErrorKind::InvalidRequest,
                         "Querying for post list is not implemented yet.",
                     )
-                    .into_response()
+                        .into_response()
                 }
             }
         }
@@ -705,9 +702,9 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
             Ok(chans) => axum::response::Json(json!({ "channels": chans })).into_response(),
             Err(err) => MicropubError::new(
                 ErrorKind::InternalServerError,
-                &format!("Error fetching channels: {}", err),
+                format!("error fetching channels: backend error: {}", err),
             )
-            .into_response(),
+                .into_response(),
         },
         QueryType::SyndicateTo => {
             axum::response::Json(json!({ "syndicate-to": [] })).into_response()
@@ -718,16 +715,16 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
                 Err(err) => {
                     return MicropubError::new(
                         ErrorKind::InternalServerError,
-                        &format!("Error fetching categories: {}", err)
+                        format!("error fetching categories: backend error: {}", err)
                     ).into_response()
                 }
             };
             axum::response::Json(json!({ "categories": categories })).into_response()
         },
-        QueryType::Unknown(q) => return MicropubError {
-            error: ErrorKind::InvalidRequest,
-            error_description: format!("Invalid query: {}", q)
-        }.into_response(),
+        QueryType::Unknown(q) => return MicropubError::new(
+            ErrorKind::InvalidRequest,
+            format!("Invalid query: {}", q)
+        ).into_response(),
     }
 }
 
diff --git a/templates-neo/Cargo.toml b/templates-neo/Cargo.toml
index 3f58b42..22865f0 100644
--- a/templates-neo/Cargo.toml
+++ b/templates-neo/Cargo.toml
@@ -32,7 +32,7 @@ features = ["formatting"]
 version = "^0.4.19"
 features = ["serde"]
 [dependencies.kittybox-util]
-version = "0.2.0"
+version = "0.3.0"
 path = "../util"
 [dependencies.kittybox-indieauth]
 version = "0.2.0"
diff --git a/templates/Cargo.toml b/templates/Cargo.toml
index 5281418..607f29e 100644
--- a/templates/Cargo.toml
+++ b/templates/Cargo.toml
@@ -29,7 +29,7 @@ axum = "^0.7.5"
 version = "^0.4.19"
 features = ["serde"]
 [dependencies.kittybox-util]
-version = "0.2.0"
+version = "0.3.0"
 path = "../util"
 [dependencies.kittybox-indieauth]
 version = "0.2.0"
diff --git a/util/Cargo.toml b/util/Cargo.toml
index 5169cbe..8eb2b4f 100644
--- a/util/Cargo.toml
+++ b/util/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "kittybox-util"
-version = "0.2.0"
+version = "0.3.0"
 edition = "2021"
 rust-version = "1.75.0"
 
diff --git a/util/src/micropub.rs b/util/src/micropub.rs
index 6127079..9d2c525 100644
--- a/util/src/micropub.rs
+++ b/util/src/micropub.rs
@@ -98,8 +98,8 @@ pub struct Error {
     /// General kind of an error that occured.
     pub error: ErrorKind,
     /// A human-readable error description intended for application developers.
-    // TODO use Cow<'static, str> to save on heap allocations
-    pub error_description: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub error_description: Option<std::borrow::Cow<'static, str>>,
 }
 
 impl std::error::Error for Error {}
@@ -113,9 +113,13 @@ impl std::fmt::Display for Error {
         };
 
         f.write_str(&s)?;
-        f.write_str(" (")?;
-        f.write_str(&self.error_description)?;
-        f.write_str(")")
+        if let Some(desc) = self.error_description.as_deref() {
+            f.write_str(" (")?;
+            f.write_str(desc)?;
+            f.write_str(")")?;
+        };
+
+        Ok(())
     }
 }
 
@@ -124,21 +128,34 @@ impl From<serde_json::Error> for Error {
         use ErrorKind::*;
         Self {
             error: InvalidRequest,
-            error_description: err.to_string(),
+            error_description: Some(err.to_string().into()),
         }
     }
 }
 
 impl Error {
     /// Create a new Micropub error.
-    pub fn new(error: ErrorKind, error_description: &str) -> Self {
+    pub fn new(error: ErrorKind, error_description: String) -> Self {
+        Self {
+            error,
+            error_description: Some(error_description.into()),
+        }
+    }
+    /// Create a new Micropub error from a static string.
+    pub const fn from_static(error: ErrorKind, error_description: &'static str) -> Self {
         Self {
             error,
-            error_description: error_description.to_owned(),
+            error_description: Some(std::borrow::Cow::Borrowed(error_description))
         }
     }
 }
 
+impl From<ErrorKind> for Error {
+    fn from(error: ErrorKind) -> Self {
+        Self { error, error_description: None }
+    }
+}
+
 #[cfg(feature = "http")]
 impl From<&Error> for http::StatusCode {
     fn from(err: &Error) -> Self {