diff --git a/src/lib.rs b/src/lib.rs index ebff740..15d3ec4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,7 +66,7 @@ use utils::*; use directories_next::ProjectDirs; use serde::{de::DeserializeOwned, Serialize}; -use std::fs::{self, File, OpenOptions}; +use std::fs::{self, File, OpenOptions, Permissions}; use std::io::Write; use std::path::{Path, PathBuf}; use thiserror::Error; @@ -128,6 +128,9 @@ pub enum ConfyError { #[error("Failed to open configuration file")] OpenConfigurationFileError(#[source] std::io::Error), + + #[error("Failed to set configuration file permissions")] + SetPermissionsFileError(#[source] std::io::Error), } /// Load an application configuration from disk @@ -233,6 +236,23 @@ pub fn store<'a, T: Serialize>( store_path(path, cfg) } +/// Save changes made to a configuration object at a specified path +/// +/// This is an alternate version of [`store`] that allows the specification of +/// file permissions that must be set. For more information on errors and +/// behavior, see [`store`]'s documentation. +/// +/// [`store`]: fn.store.html +pub fn store_perms<'a, T: Serialize>( + app_name: &str, + config_name: impl Into<Option<&'a str>>, + cfg: T, + perms: Permissions, +) -> Result<(), ConfyError> { + let path = get_configuration_file_path(app_name, config_name)?; + store_path_perms(path, cfg, perms) +} + /// Save changes made to a configuration object at a specified path /// /// This is an alternate version of [`store`] that allows the specification of @@ -241,7 +261,29 @@ pub fn store<'a, T: Serialize>( /// /// [`store`]: fn.store.html pub fn store_path<T: Serialize>(path: impl AsRef<Path>, cfg: T) -> Result<(), ConfyError> { - let path = path.as_ref(); + do_store(path.as_ref(), cfg, None) +} + +/// Save changes made to a configuration object at a specified path +/// +/// This is an alternate version of [`store_path`] that allows the +/// specification of file permissions that must be set. For more information on +/// errors and behavior, see [`store`]'s documentation. +/// +/// [`store_path`]: fn.store_path.html +pub fn store_path_perms<T: Serialize>( + path: impl AsRef<Path>, + cfg: T, + perms: Permissions, +) -> Result<(), ConfyError> { + do_store(path.as_ref(), cfg, Some(perms)) +} + +fn do_store<T: Serialize>( + path: &Path, + cfg: T, + perms: Option<Permissions>, +) -> Result<(), ConfyError> { let config_dir = path .parent() .ok_or_else(|| ConfyError::BadConfigDirectory(format!("{:?} is a root or prefix", path)))?; @@ -282,6 +324,11 @@ pub fn store_path<T: Serialize>(path: impl AsRef<Path>, cfg: T) -> Result<(), Co .open(&path_tmp) .map_err(ConfyError::OpenConfigurationFileError)?; + if let Some(p) = perms { + f.set_permissions(p) + .map_err(ConfyError::SetPermissionsFileError)?; + } + f.write_all(s.as_bytes()) .map_err(ConfyError::WriteConfigurationFileError)?; f.flush().map_err(ConfyError::WriteConfigurationFileError)?; @@ -326,6 +373,9 @@ mod tests { use serde::Serializer; use serde_derive::{Deserialize, Serialize}; + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; + #[derive(PartialEq, Default, Debug, Serialize, Deserialize)] struct ExampleConfig { name: String, @@ -368,6 +418,45 @@ mod tests { }) } + /// [`store_path_perms`] stores [`ExampleConfig`], with only read permission for owner (UNIX). + #[test] + #[cfg(unix)] + fn test_store_path_perms() { + with_config_path(|path| { + let config: ExampleConfig = ExampleConfig { + name: "Secret".to_string(), + count: 16549, + }; + store_path_perms(path, &config, Permissions::from_mode(0o600)) + .expect("store_path_perms failed"); + let loaded = load_path(path).expect("load_path failed"); + assert_eq!(config, loaded); + }) + } + + /// [`store_path_perms`] stores [`ExampleConfig`], as read-only. + #[test] + fn test_store_path_perms_readonly() { + with_config_path(|path| { + let config: ExampleConfig = ExampleConfig { + name: "Soon read-only".to_string(), + count: 27115, + }; + store_path(path, &config).expect("store_path failed"); + + let metadata = fs::metadata(path).expect("reading metadata failed"); + let mut permissions = metadata.permissions(); + permissions.set_readonly(true); + + store_path_perms(path, &config, permissions).expect("store_path_perms failed"); + + assert!(fs::metadata(path) + .expect("reading metadata failed") + .permissions() + .readonly()); + }) + } + /// [`store_path`] fails when given a root path. #[test] fn test_store_path_root_error() {