diff --git a/Cargo.lock b/Cargo.lock index 6accfaff..d3be3142 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2500,6 +2500,7 @@ dependencies = [ "argon2", "atty", "axum 0.6.0-rc.4", + "camino", "clap", "dotenv", "futures-util", @@ -2549,6 +2550,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "camino", "chrono", "figment", "indoc", @@ -2719,6 +2721,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "camino", "convert_case", "csv", "futures-util", @@ -2862,6 +2865,7 @@ name = "mas-static-files" version = "0.1.0" dependencies = [ "axum 0.6.0-rc.4", + "camino", "headers", "http", "http-body", @@ -2915,6 +2919,7 @@ name = "mas-templates" version = "0.1.0" dependencies = [ "anyhow", + "camino", "chrono", "mas-data-model", "mas-router", diff --git a/clippy.toml b/clippy.toml index da61a081..61c5c04f 100644 --- a/clippy.toml +++ b/clippy.toml @@ -9,4 +9,6 @@ disallowed-methods = [ disallowed-types = [ "rand::OsRng", + { path = "std::path::PathBuf", reason = "use camino::Utf8PathBuf instead" }, + { path = "std::path::Path", reason = "use camino::Utf8Path instead" }, ] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 64cbc389..dc891d3a 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -10,6 +10,7 @@ anyhow = "1.0.66" argon2 = { version = "0.4.1", features = ["password-hash"] } atty = "0.2.14" axum = "0.6.0-rc.4" +camino = "1.1.1" clap = { version = "4.0.26", features = ["derive"] } dotenv = "0.15.0" futures-util = "0.3.25" diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 604463b2..9ea132d8 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -12,9 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::path::PathBuf; - use anyhow::Context; +use camino::Utf8PathBuf; use clap::Parser; use mas_config::ConfigurationSection; @@ -50,7 +49,7 @@ enum Subcommand { pub struct Options { /// Path to the configuration file #[arg(short, long, global = true, action = clap::ArgAction::Append)] - config: Vec, + config: Vec, #[command(subcommand)] subcommand: Option, @@ -78,7 +77,7 @@ impl Options { .unwrap_or_else(|_| "config.yaml".to_owned()) // Split the file list on `:` .split(':') - .map(PathBuf::from) + .map(Utf8PathBuf::from) .collect() } else { self.config.clone() diff --git a/crates/cli/src/commands/templates.rs b/crates/cli/src/commands/templates.rs index 74689306..796dd5ec 100644 --- a/crates/cli/src/commands/templates.rs +++ b/crates/cli/src/commands/templates.rs @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::path::PathBuf; - +use camino::Utf8PathBuf; use clap::Parser; use mas_storage::Clock; use mas_templates::Templates; @@ -29,7 +28,7 @@ enum Subcommand { /// Save the builtin templates to a folder Save { /// Where the templates should be saved - path: PathBuf, + path: Utf8PathBuf, /// Overwrite existing template files #[arg(long)] diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 926f2de8..25077016 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -17,8 +17,6 @@ #![warn(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] -use std::path::PathBuf; - use anyhow::Context; use clap::Parser; use mas_config::TelemetryConfig; @@ -44,7 +42,7 @@ async fn main() -> anyhow::Result<()> { async fn try_main() -> anyhow::Result<()> { // Load environment variables from .env files // We keep the path to log it afterwards - let dotenv_path: Result, _> = dotenv::dotenv() + let dotenv_path: Result, _> = dotenv::dotenv() .map(Some) // Display the error if it is something other than the .env file not existing .or_else(|e| if e.not_found() { Ok(None) } else { Err(e) }); diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index b3001396..22f5fb72 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -60,7 +60,7 @@ where router.merge(mas_handlers::graphql_router::(*playground)) } mas_config::HttpResource::Static { web_root } => { - let handler = mas_static_files::service(web_root); + let handler = mas_static_files::service(web_root.as_deref()); router.nest_service(mas_router::StaticAsset::route(), handler) } mas_config::HttpResource::OAuth => { @@ -91,11 +91,8 @@ where "root": app_base, }); - let index_service = ViteManifestService::new( - manifest.clone().try_into().unwrap(), - assets_base.into(), - config, - ); + let index_service = + ViteManifestService::new(manifest.clone(), assets_base.into(), config); let static_service = ServeDir::new(assets).append_index_html_on_directories(false); diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 77d71401..eea707a1 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -13,11 +13,12 @@ async-trait = "0.1.58" thiserror = "1.0.37" anyhow = "1.0.66" -schemars = { version = "0.8.11", features = ["url", "chrono"] } -figment = { version = "0.10.8", features = ["env", "yaml", "test"] } +camino = { version = "1.1.1", features = ["serde1"] } chrono = { version = "0.4.23", features = ["serde"] } -url = { version = "2.3.1", features = ["serde"] } +figment = { version = "0.10.8", features = ["env", "yaml", "test"] } +schemars = { version = "0.8.11", features = ["url", "chrono"] } ulid = { version = "1.0.0", features = ["serde"] } +url = { version = "2.3.1", features = ["serde"] } serde = { version = "1.0.147", features = ["derive"] } serde_with = { version = "2.1.0", features = ["hex", "chrono"] } diff --git a/crates/config/src/sections/database.rs b/crates/config/src/sections/database.rs index 9a2046a2..772ee885 100644 --- a/crates/config/src/sections/database.rs +++ b/crates/config/src/sections/database.rs @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{num::NonZeroU32, path::PathBuf, time::Duration}; +use std::{num::NonZeroU32, time::Duration}; use anyhow::Context; use async_trait::async_trait; +use camino::Utf8PathBuf; use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -85,7 +86,8 @@ enum ConnectConfig { /// Directory containing the UNIX socket to connect to #[serde(default)] - socket: Option, + #[schemars(with = "Option")] + socket: Option, /// PostgreSQL user name to connect as #[serde(default)] diff --git a/crates/config/src/sections/http.rs b/crates/config/src/sections/http.rs index f9a938dc..3988401c 100644 --- a/crates/config/src/sections/http.rs +++ b/crates/config/src/sections/http.rs @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{borrow::Cow, io::Cursor, ops::Deref, path::PathBuf}; +use std::{borrow::Cow, io::Cursor, ops::Deref}; use anyhow::bail; use async_trait::async_trait; +use camino::Utf8PathBuf; use mas_keystore::PrivateKey; use rand::Rng; use schemars::JsonSchema; @@ -99,7 +100,8 @@ pub enum BindConfig { /// Listen on a UNIX domain socket Unix { /// Path to the socket - socket: PathBuf, + #[schemars(with = "String")] + socket: Utf8PathBuf, }, /// Accept connections on file descriptors passed by the parent process. @@ -125,14 +127,16 @@ pub enum BindConfig { #[serde(rename_all = "snake_case")] pub enum KeyOrFile { Key(String), - KeyFile(PathBuf), + #[schemars(with = "String")] + KeyFile(Utf8PathBuf), } #[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "snake_case")] pub enum CertificateOrFile { Certificate(String), - CertificateFile(PathBuf), + #[schemars(with = "String")] + CertificateFile(Utf8PathBuf), } /// Configuration related to TLS on a listener @@ -252,7 +256,8 @@ pub enum Resource { /// Path from which to serve static files. If not specified, it will /// serve the static files embedded in the server binary #[serde(default)] - web_root: Option, + #[schemars(with = "Option")] + web_root: Option, }, /// Mount a "/connection-info" handler which helps debugging informations on @@ -263,10 +268,12 @@ pub enum Resource { /// Mount the single page app Spa { /// Path to the vite manifest - manifest: PathBuf, + #[schemars(with = "String")] + manifest: Utf8PathBuf, /// Path to the assets to server - assets: PathBuf, + #[schemars(with = "String")] + assets: Utf8PathBuf, }, } diff --git a/crates/config/src/sections/policy.rs b/crates/config/src/sections/policy.rs index 7146701a..446c5b80 100644 --- a/crates/config/src/sections/policy.rs +++ b/crates/config/src/sections/policy.rs @@ -12,9 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::path::PathBuf; - use async_trait::async_trait; +use camino::Utf8PathBuf; use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -40,7 +39,8 @@ fn default_authorization_grant_endpoint() -> String { pub struct PolicyConfig { /// Path to the WASM module #[serde(default)] - pub wasm_module: Option, + #[schemars(with = "Option")] + pub wasm_module: Option, /// Entrypoint to use when evaluating client registrations #[serde(default = "default_client_registration_endpoint")] diff --git a/crates/config/src/sections/secrets.rs b/crates/config/src/sections/secrets.rs index 770c49b4..25d40bcc 100644 --- a/crates/config/src/sections/secrets.rs +++ b/crates/config/src/sections/secrets.rs @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{borrow::Cow, path::PathBuf}; +use std::borrow::Cow; use anyhow::Context; use async_trait::async_trait; +use camino::Utf8PathBuf; use mas_jose::jwk::{JsonWebKey, JsonWebKeySet}; use mas_keystore::{Encrypter, Keystore, PrivateKey}; use rand::{ @@ -38,14 +39,16 @@ fn example_secret() -> &'static str { #[serde(rename_all = "snake_case")] pub enum KeyOrFile { Key(String), - KeyFile(PathBuf), + #[schemars(with = "String")] + KeyFile(Utf8PathBuf), } #[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "snake_case")] pub enum PasswordOrFile { Password(String), - PasswordFile(PathBuf), + #[schemars(with = "String")] + PasswordFile(Utf8PathBuf), } #[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] diff --git a/crates/config/src/sections/templates.rs b/crates/config/src/sections/templates.rs index f51f9a97..4cba0549 100644 --- a/crates/config/src/sections/templates.rs +++ b/crates/config/src/sections/templates.rs @@ -13,6 +13,7 @@ // limitations under the License. use async_trait::async_trait; +use camino::Utf8PathBuf; use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -28,7 +29,8 @@ fn default_builtin() -> bool { pub struct TemplatesConfig { /// Path to the folder that holds the custom templates #[serde(default)] - pub path: Option, + #[schemars(with = "Option")] + pub path: Option, /// Load the templates embedded in the binary #[serde(default = "default_builtin")] diff --git a/crates/config/src/util.rs b/crates/config/src/util.rs index 5cd3ca92..f3bbb23f 100644 --- a/crates/config/src/util.rs +++ b/crates/config/src/util.rs @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::path::Path; - use anyhow::Context; use async_trait::async_trait; +use camino::Utf8Path; use figment::{ error::Error as FigmentError, providers::{Env, Format, Serialized, Yaml}, @@ -65,20 +64,20 @@ pub trait ConfigurationSection<'a>: Sized + Deserialize<'a> + Serialize { /// Load configuration from a list of files and environment variables. fn load_from_files

(paths: &[P]) -> Result where - P: AsRef, + P: AsRef, { let base = Figment::new().merge(Env::prefixed("MAS_").split("_")); paths .iter() - .fold(base, |f, path| f.merge(Yaml::file(path))) + .fold(base, |f, path| f.merge(Yaml::file(path.as_ref()))) .extract_inner(Self::path()) } /// Load configuration from a file and environment variables. fn load_from_file

(path: P) -> Result where - P: AsRef, + P: AsRef, { Self::load_from_files(&[path]) } diff --git a/crates/iana-codegen/Cargo.toml b/crates/iana-codegen/Cargo.toml index 9fb7d55e..2d4b947a 100644 --- a/crates/iana-codegen/Cargo.toml +++ b/crates/iana-codegen/Cargo.toml @@ -8,6 +8,7 @@ license = "Apache-2.0" [dependencies] anyhow = "1.0.66" async-trait = "0.1.58" +camino = "1.1.1" convert_case = "0.6.0" csv = "1.1.6" futures-util = "0.3.25" diff --git a/crates/iana-codegen/src/main.rs b/crates/iana-codegen/src/main.rs index 71a5230b..36c8bdc6 100644 --- a/crates/iana-codegen/src/main.rs +++ b/crates/iana-codegen/src/main.rs @@ -16,8 +16,9 @@ #![deny(clippy::all, clippy::str_to_string, rustdoc::broken_intra_doc_links)] #![warn(clippy::pedantic)] -use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc}; +use std::{collections::HashMap, fmt::Display, sync::Arc}; +use camino::{Utf8Path, Utf8PathBuf}; use reqwest::Client; use tokio::io::AsyncWriteExt; use tracing::Level; @@ -35,8 +36,8 @@ struct File { items: HashMap<&'static str, Vec>, } -fn resolve_path(relative: PathBuf) -> PathBuf { - let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); +fn resolve_path(relative: impl AsRef) -> Utf8PathBuf { + let crate_root = Utf8Path::new(env!("CARGO_MANIFEST_DIR")); let workspace_root = crate_root.parent().unwrap().parent().unwrap(); workspace_root.join(relative) } @@ -65,12 +66,12 @@ impl File { } #[tracing::instrument(skip_all)] - async fn write(&self, path: PathBuf) -> anyhow::Result<()> { + async fn write(&self, path: impl AsRef) -> anyhow::Result<()> { let mut file = tokio::fs::OpenOptions::new() .create(true) .truncate(true) .write(true) - .open(path) + .open(path.as_ref()) .await?; tracing::info!("Writing file"); @@ -180,8 +181,11 @@ pub enum {} {{"#, use self::traits::{EnumEntry, EnumMember, Section}; -#[tracing::instrument(skip(client))] -async fn generate_jose(client: &Arc, path: PathBuf) -> anyhow::Result<()> { +#[tracing::instrument(skip_all, fields(%path))] +async fn generate_jose( + client: &Arc, + path: impl AsRef + std::fmt::Display, +) -> anyhow::Result<()> { let path = resolve_path(path); let client = client.clone(); @@ -208,8 +212,11 @@ async fn generate_jose(client: &Arc, path: PathBuf) -> anyhow::Result<() Ok(()) } -#[tracing::instrument(skip(client))] -async fn generate_oauth(client: &Arc, path: PathBuf) -> anyhow::Result<()> { +#[tracing::instrument(skip_all, fields(%path))] +async fn generate_oauth( + client: &Arc, + path: impl AsRef + std::fmt::Display, +) -> anyhow::Result<()> { let path = resolve_path(path); let client = client.clone(); @@ -244,7 +251,7 @@ async fn main() -> anyhow::Result<()> { let client = Client::builder().user_agent("iana-parser/0.0.1").build()?; let client = Arc::new(client); - let iana_crate_root = PathBuf::from("crates/iana/"); + let iana_crate_root = Utf8Path::new("crates/iana/"); generate_jose(&client, iana_crate_root.join("src/jose.rs")).await?; generate_oauth(&client, iana_crate_root.join("src/oauth.rs")).await?; diff --git a/crates/static-files/Cargo.toml b/crates/static-files/Cargo.toml index 30525633..20265a2c 100644 --- a/crates/static-files/Cargo.toml +++ b/crates/static-files/Cargo.toml @@ -10,6 +10,7 @@ dev = [] [dependencies] axum = { version = "0.6.0-rc.4", features = ["headers"] } +camino = "1.1.1" headers = "0.3.8" http = "0.2.8" http-body = "0.4.5" diff --git a/crates/static-files/src/lib.rs b/crates/static-files/src/lib.rs index 4f798da0..f408455d 100644 --- a/crates/static-files/src/lib.rs +++ b/crates/static-files/src/lib.rs @@ -25,6 +25,9 @@ #[cfg(not(feature = "dev"))] mod builtin { + // the RustEmbed derive uses std::path::Path + #![allow(clippy::disallowed_types)] + use std::{ fmt::Write, future::{ready, Ready}, @@ -127,25 +130,25 @@ mod builtin { #[cfg(feature = "dev")] mod builtin { - use std::path::PathBuf; - + use camnio::Utf8Path; use tower_http::services::ServeDir; /// Serve static files in dev mode #[must_use] pub fn service() -> ServeDir { - let path = PathBuf::from(format!("{}/public", env!("CARGO_MANIFEST_DIR"))); + let path = Utf8Path::new(format!("{}/public", env!("CARGO_MANIFEST_DIR"))); ServeDir::new(path).append_index_html_on_directories(false) } } -use std::{convert::Infallible, future::ready, path::PathBuf}; +use std::{convert::Infallible, future::ready}; use axum::{ body::HttpBody, response::Response, routing::{on_service, MethodFilter}, }; +use camino::Utf8Path; use http::{Request, StatusCode}; use tower::{util::BoxCloneService, ServiceExt}; use tower_http::services::ServeDir; @@ -153,7 +156,7 @@ use tower_http::services::ServeDir; /// Serve static files #[must_use] pub fn service( - path: &Option, + path: Option<&Utf8Path>, ) -> BoxCloneService, Response, Infallible> { let svc = if let Some(path) = path { let handler = ServeDir::new(path).append_index_html_on_directories(false); diff --git a/crates/templates/Cargo.toml b/crates/templates/Cargo.toml index b7b19f31..96e85f54 100644 --- a/crates/templates/Cargo.toml +++ b/crates/templates/Cargo.toml @@ -20,6 +20,7 @@ serde = { version = "1.0.147", features = ["derive"] } serde_json = "1.0.88" serde_urlencoded = "0.7.1" +camino = "1.1.1" chrono = "0.4.23" url = "2.3.1" ulid = { version = "1.0.0", features = ["serde"] } diff --git a/crates/templates/src/forms.rs b/crates/templates/src/forms.rs index 0f95a922..596d5f59 100644 --- a/crates/templates/src/forms.rs +++ b/crates/templates/src/forms.rs @@ -227,8 +227,8 @@ mod tests { ); let form = TestForm { - foo: "".to_owned(), - bar: "".to_owned(), + foo: String::new(), + bar: String::new(), }; let state = form .to_form_state() diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 793067e7..06eb9cfa 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -24,15 +24,10 @@ //! Templates rendering -use std::{ - collections::HashSet, - io::Cursor, - path::{Path, PathBuf}, - string::ToString, - sync::Arc, -}; +use std::{collections::HashSet, io::Cursor, string::ToString, sync::Arc}; use anyhow::{bail, Context as _}; +use camino::{Utf8Path, Utf8PathBuf}; use mas_data_model::StorageBackend; use mas_router::UrlBuilder; use serde::Serialize; @@ -64,7 +59,7 @@ pub use self::{ pub struct Templates { tera: Arc>, url_builder: UrlBuilder, - path: Option, + path: Option, builtin: bool, } @@ -91,7 +86,7 @@ pub enum TemplateLoadingError { impl Templates { /// List directories to watch - pub async fn watch_roots(&self) -> Vec { + pub async fn watch_roots(&self) -> Vec { Self::roots(self.path.as_deref(), self.builtin) .await .into_iter() @@ -99,18 +94,21 @@ impl Templates { .collect() } - async fn roots(path: Option<&str>, builtin: bool) -> Vec> { + async fn roots( + path: Option<&Utf8Path>, + builtin: bool, + ) -> Vec> { let mut paths = Vec::new(); if builtin && cfg!(feature = "dev") { paths.push( - Path::new(env!("CARGO_MANIFEST_DIR")) + Utf8Path::new(env!("CARGO_MANIFEST_DIR")) .join("src") .join("res"), ); } if let Some(path) = path { - paths.push(PathBuf::from(path)); + paths.push(Utf8PathBuf::from(path)); } let mut ret = Vec::new(); @@ -137,7 +135,7 @@ impl Templates { /// Load the templates from the given config pub async fn load( - path: Option, + path: Option, builtin: bool, url_builder: UrlBuilder, ) -> Result { @@ -151,7 +149,7 @@ impl Templates { } async fn load_( - path: Option<&str>, + path: Option<&Utf8Path>, builtin: bool, url_builder: UrlBuilder, ) -> Result { @@ -169,8 +167,7 @@ impl Templates { // This uses blocking I/Os, do that in a blocking task let tera = tokio::task::spawn_blocking(move || { - // Using `to_string_lossy` here is probably fine - let path = format!("{}/**/*.{{html,txt,subject}}", root.to_string_lossy()); + let path = format!("{}/**/*.{{html,txt,subject}}", root); info!(%path, "Loading templates from filesystem"); Tera::parse(&path) }) @@ -223,7 +220,7 @@ impl Templates { } /// Save the builtin templates to a folder - pub async fn save(path: &Path, overwrite: bool) -> anyhow::Result<()> { + pub async fn save(path: &Utf8Path, overwrite: bool) -> anyhow::Result<()> { if cfg!(feature = "dev") { bail!("Builtin templates are not included in dev binaries") }