use crate::config::strings;
use std::{fmt, path::PathBuf};
#[derive(Debug, Clone)]
pub struct DirectorySetting {
pub meta: crate::GameSettingMeta,
original: String,
parsed: PathBuf,
}
impl std::fmt::Display for DirectorySetting {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}", self.original)
}
}
impl crate::GameSetting for DirectorySetting {
fn meta(&self) -> &crate::GameSettingMeta {
&self.meta
}
}
impl DirectorySetting {
pub fn new<S: Into<String>>(value: S, source_config: PathBuf, comment: &mut String) -> Self {
let original = value.into();
let parse_base = if source_config.file_name().is_some_and(|f| f == "openmw.cfg") {
source_config.parent().unwrap_or(source_config.as_path())
} else {
source_config.as_path()
};
let parsed = strings::parse_data_directory(&parse_base, &original);
let meta = crate::GameSettingMeta {
source_config,
comment: std::mem::take(comment),
};
Self {
meta,
original,
parsed,
}
}
#[must_use]
pub fn original(&self) -> &String {
&self.original
}
#[must_use]
pub fn original_str(&self) -> &str {
&self.original
}
#[must_use]
pub fn parsed(&self) -> &std::path::Path {
&self.parsed
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_directory_setting_basic_construction() {
let config_path = PathBuf::from("/my/config");
let mut comment = "some comment".to_string();
let setting = DirectorySetting::new("data", config_path.clone(), &mut comment);
assert_eq!(setting.original, "data");
assert_eq!(setting.parsed, config_path.join("data"));
assert_eq!(setting.meta.source_config, config_path);
assert_eq!(setting.meta.comment, "some comment");
assert!(comment.is_empty()); }
#[test]
fn test_directory_setting_with_user_data_token() {
let config_path = PathBuf::from("/irrelevant");
let mut comment = String::new();
let setting = DirectorySetting::new("?userdata?/foo", config_path, &mut comment);
let expected_prefix = crate::default_userdata_path();
assert!(setting.parsed.starts_with(expected_prefix));
assert!(setting.parsed.ends_with("foo/"));
}
#[test]
fn test_directory_setting_with_user_config_token() {
let config_path = PathBuf::from("/config/dir");
let mut comment = String::new();
let setting = DirectorySetting::new("?userconfig?/bar", config_path, &mut comment);
dbg!(setting.parsed());
let expected_prefix = crate::default_config_path();
assert!(setting.parsed.starts_with(expected_prefix));
assert!(setting.parsed.ends_with("bar"));
}
#[test]
fn test_directory_setting_quoted_path() {
let config_path = PathBuf::from("/my/config");
let mut comment = String::new();
let setting =
DirectorySetting::new("\"path/with spaces\"", config_path.clone(), &mut comment);
assert_eq!(setting.original, "\"path/with spaces\"");
assert_eq!(setting.parsed, config_path.join("path").join("with spaces"));
}
#[test]
fn test_directory_setting_relative_path_normalization() {
let config_path = PathBuf::from("/my/config");
let mut comment = String::new();
let setting = DirectorySetting::new("subdir\\nested", config_path.clone(), &mut comment);
let expected = config_path.join("subdir").join("nested");
assert_eq!(setting.parsed, expected);
}
fn mock_path(path: &str) -> PathBuf {
PathBuf::from(path)
}
#[test]
fn test_dot_component_is_removed() {
let config = mock_path("/etc/openmw");
let mut comment = String::from("comment");
let setting = DirectorySetting::new("./data", config.clone(), &mut comment);
assert_eq!(setting.parsed(), &config.join("data"));
}
#[test]
fn test_double_dot_not_normalized() {
let config = mock_path("/home/user/.config/openmw");
let mut comment = String::from("comment");
let setting = DirectorySetting::new("../common", config.clone(), &mut comment);
let expected = config.join("../common");
assert_eq!(setting.parsed(), &expected);
}
#[test]
fn test_dot_components_not_normalized() {
let config = mock_path("/opt/game/config");
let mut comment = String::new();
let setting = DirectorySetting::new("foo/./bar/../baz", config.clone(), &mut comment);
let expected = config.join("foo/./bar/../baz");
assert_eq!(setting.parsed(), &expected);
}
#[test]
fn test_absolute_path_not_joined_to_config() {
let config = mock_path("/etc/openmw");
let mut comment = String::new();
let setting = DirectorySetting::new("/absolute/path/to/data", config, &mut comment);
assert_eq!(setting.parsed(), &PathBuf::from("/absolute/path/to/data"));
}
#[test]
fn test_absolute_path_original_preserved() {
let config = mock_path("/etc/openmw");
let mut comment = String::new();
let setting = DirectorySetting::new("/absolute/data", config, &mut comment);
assert_eq!(setting.original(), "/absolute/data");
assert_eq!(setting.original_str(), "/absolute/data");
}
#[test]
fn test_backslash_normalised_to_separator() {
let config = mock_path("/my/config");
let mut comment = String::new();
let setting = DirectorySetting::new("subdir\\nested\\leaf", config.clone(), &mut comment);
let expected = config.join("subdir").join("nested").join("leaf");
assert_eq!(setting.parsed(), &expected);
}
#[test]
fn test_mixed_separators_normalised() {
let config = mock_path("/my/config");
let mut comment = String::new();
let setting = DirectorySetting::new("a\\b/c", config.clone(), &mut comment);
let expected = config.join("a").join("b").join("c");
assert_eq!(setting.parsed(), &expected);
}
#[test]
fn test_quoted_path_stripped_of_quotes() {
let config = mock_path("/cfg");
let mut comment = String::new();
let setting = DirectorySetting::new("\"simple\"", config.clone(), &mut comment);
assert_eq!(setting.parsed(), &config.join("simple"));
}
#[test]
fn test_quoted_path_ampersand_escapes_next_char() {
let config = mock_path("/cfg");
let mut comment = String::new();
let setting = DirectorySetting::new("\"foo&\"bar\"", config.clone(), &mut comment);
assert_eq!(setting.parsed(), &config.join("foo\"bar"));
}
#[test]
fn test_quoted_path_ampersand_escapes_ampersand() {
let config = mock_path("/cfg");
let mut comment = String::new();
let setting = DirectorySetting::new("\"foo&&bar\"", config.clone(), &mut comment);
assert_eq!(setting.parsed(), &config.join("foo&bar"));
}
#[test]
fn test_original_preserves_quotes() {
let config = mock_path("/cfg");
let mut comment = String::new();
let setting = DirectorySetting::new("\"path with spaces\"", config, &mut comment);
assert_eq!(setting.original(), "\"path with spaces\"");
}
#[test]
fn test_userdata_token_only() {
let config = mock_path("/irrelevant");
let mut comment = String::new();
let setting = DirectorySetting::new("?userdata?", config, &mut comment);
assert_eq!(setting.parsed(), &crate::default_userdata_path());
}
#[test]
fn test_userconfig_token_only() {
let config = mock_path("/irrelevant");
let mut comment = String::new();
let setting = DirectorySetting::new("?userconfig?", config, &mut comment);
assert_eq!(setting.parsed(), &crate::default_config_path());
}
#[test]
fn test_userdata_token_with_nested_path() {
let config = mock_path("/irrelevant");
let mut comment = String::new();
let setting = DirectorySetting::new("?userdata?/saves/slot1", config, &mut comment);
let expected = crate::default_userdata_path().join("saves").join("slot1");
assert_eq!(setting.parsed(), &expected);
}
#[test]
fn test_local_token_only() {
let config = mock_path("/irrelevant");
let mut comment = String::new();
let setting = DirectorySetting::new("?local?", config, &mut comment);
assert_eq!(setting.parsed(), &crate::default_local_path());
}
#[test]
fn test_local_token_with_nested_path() {
let config = mock_path("/irrelevant");
let mut comment = String::new();
let setting = DirectorySetting::new("?local?/mods/common", config, &mut comment);
let expected = crate::default_local_path().join("mods").join("common");
assert_eq!(setting.parsed(), &expected);
}
#[test]
#[cfg(not(windows))]
fn test_global_token_only_on_supported_platforms() {
let config = mock_path("/irrelevant");
let mut comment = String::new();
let setting = DirectorySetting::new("?global?", config, &mut comment);
assert_eq!(setting.parsed(), &crate::default_global_path());
}
#[test]
#[cfg(not(windows))]
fn test_global_token_with_nested_path_on_supported_platforms() {
let config = mock_path("/irrelevant");
let mut comment = String::new();
let setting = DirectorySetting::new("?global?/openmw", config, &mut comment);
let expected = crate::default_global_path().join("openmw");
assert_eq!(setting.parsed(), &expected);
}
#[test]
#[cfg(windows)]
fn test_global_token_is_left_unexpanded_on_windows() {
let config = mock_path(r"C:\OpenMW");
let mut comment = String::new();
let setting = DirectorySetting::new("?global?/data", config.clone(), &mut comment);
let expected = config.join("?global?").join("data");
assert_eq!(setting.parsed(), &expected);
}
#[test]
fn test_source_config_stored_verbatim() {
let config = mock_path("/etc/openmw/openmw.cfg");
let mut comment = String::new();
let setting = DirectorySetting::new("data", config.clone(), &mut comment);
assert_eq!(setting.meta.source_config, config);
}
#[test]
fn test_comment_cleared_after_new() {
let config = mock_path("/etc/openmw");
let mut comment = String::from("# a comment\n");
let setting = DirectorySetting::new("data", config, &mut comment);
assert_eq!(setting.meta.comment, "# a comment\n");
assert!(
comment.is_empty(),
"comment should be cleared after construction"
);
}
#[test]
fn test_empty_comment_stays_empty() {
let config = mock_path("/etc/openmw");
let mut comment = String::new();
let setting = DirectorySetting::new("data", config, &mut comment);
assert!(setting.meta.comment.is_empty());
}
}