use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use crate::dirs;
use crate::{Config, Error, Result};
pub fn setup(force: bool) -> Result<PathBuf> {
setup_in(&dirs::config_dir()?, force)
}
fn setup_in(dir: &Path, force: bool) -> Result<PathBuf> {
let path = dir.join(dirs::CONFIG_FILE_NAME);
if !force {
ensure_missing(&path)?;
}
let contents = Config::recommended()?.to_toml()?;
dirs::create_private_directory(dir)?;
write_configuration(&path, &contents, force)?;
Ok(path)
}
fn ensure_missing(path: &Path) -> Result<()> {
match fs::symlink_metadata(path) {
Ok(_) => Err(Error::ConfigAlreadyExists(path.to_path_buf())),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(source) => Err(Error::io(path, source)),
}
}
fn write_configuration(path: &Path, contents: &str, force: bool) -> Result<()> {
let mut file = OpenOptions::new()
.write(true)
.create_new(!force)
.create(force)
.truncate(force)
.open(path)
.map_err(|source| {
if !force && source.kind() == io::ErrorKind::AlreadyExists {
Error::ConfigAlreadyExists(path.to_path_buf())
} else {
Error::io(path, source)
}
})?;
file.write_all(contents.as_bytes())
.map_err(|source| Error::io(path, source))
}
#[cfg(test)]
mod tests {
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use super::{ensure_missing, setup_in, write_configuration};
use crate::{Config, Error};
const CUSTOM_CONFIG: &str = "# Custom configuration\n";
fn assert_recommended_config(path: &Path) {
let contents = fs::read_to_string(path).unwrap();
let config: Config = toml::from_str(&contents).unwrap();
assert_eq!(config, Config::recommended().unwrap());
}
#[test]
fn creates_configuration_and_parent_directories() {
let home = tempfile::tempdir().unwrap();
let config_dir = home.path().join(".config/moonmchn");
let path = setup_in(&config_dir, false).unwrap();
assert_eq!(path, config_dir.join("config.toml"));
assert_recommended_config(&path);
}
#[test]
fn refuses_existing_configuration_without_modifying_it() {
let home = tempfile::tempdir().unwrap();
let path = setup_in(home.path(), false).unwrap();
fs::write(&path, CUSTOM_CONFIG).unwrap();
let error = setup_in(home.path(), false).unwrap_err();
assert!(matches!(error, Error::ConfigAlreadyExists(existing) if existing == path));
assert_eq!(fs::read_to_string(path).unwrap(), CUSTOM_CONFIG);
}
#[test]
fn force_replaces_entire_configuration() {
let home = tempfile::tempdir().unwrap();
let path = setup_in(home.path(), false).unwrap();
let contents = fs::read_to_string(&path).unwrap();
fs::write(&path, format!("{contents}{CUSTOM_CONFIG}")).unwrap();
assert_eq!(setup_in(home.path(), true).unwrap(), path);
assert_eq!(fs::read_to_string(&path).unwrap(), contents);
assert_recommended_config(&path);
}
#[test]
fn force_also_creates_missing_configuration() {
let home = tempfile::tempdir().unwrap();
let path = setup_in(home.path(), true).unwrap();
assert_recommended_config(&path);
}
#[test]
fn refuses_a_destination_created_after_validation() {
let home = tempfile::tempdir().unwrap();
let path = home.path().join("config.toml");
ensure_missing(&path).unwrap();
fs::write(&path, CUSTOM_CONFIG).unwrap();
let error = write_configuration(&path, "", false).unwrap_err();
assert!(matches!(error, Error::ConfigAlreadyExists(existing) if existing == path));
assert_eq!(fs::read_to_string(path).unwrap(), CUSTOM_CONFIG);
}
#[test]
fn force_does_not_replace_a_directory() {
let home = tempfile::tempdir().unwrap();
let config_dir = home.path().join(".config/moonmchn");
let path = config_dir.join("config.toml");
fs::create_dir_all(&path).unwrap();
let sentinel = path.join("keep");
fs::write(&sentinel, CUSTOM_CONFIG).unwrap();
let error = setup_in(&config_dir, true).unwrap_err();
assert!(matches!(error, Error::Io { path: failed_path, .. } if failed_path == path));
assert_eq!(fs::read_to_string(sentinel).unwrap(), CUSTOM_CONFIG);
}
#[test]
fn reports_filesystem_failure() {
let home = tempfile::tempdir().unwrap();
let config_dir = home.path().join(".config/moonmchn");
fs::write(home.path().join(".config"), "not a directory").unwrap();
let error = setup_in(&config_dir, false).unwrap_err();
assert!(matches!(error, Error::Io { .. }));
}
#[test]
fn creates_the_configuration_directory_private() {
let home = tempfile::tempdir().unwrap();
let config_dir = home.path().join(".config/moonmchn");
let parent = home.path().join(".config");
let control = home.path().join("control");
fs::create_dir(&control).unwrap();
setup_in(&config_dir, false).unwrap();
let mode = |path: &Path| fs::metadata(path).unwrap().permissions().mode() & 0o7777;
assert_eq!(mode(&config_dir), 0o700, "{config_dir:?} must be private");
assert_eq!(
mode(&parent),
mode(&control),
"{parent:?} must keep default permissions"
);
}
}