use crate::error::{AppError, AppResult};
use std::path::PathBuf;
use thunderstore_engine::profile::layout::BadProfile;
pub const GAME: &str = "valheim";
pub type Target = thunderstore_engine::profile::target::InstallTarget;
pub fn describe(target: &Target) -> String {
match &target.profile {
Some(name) => format!("profile {name:?}"),
None => "the game directory".to_string(),
}
}
pub fn resolve(base: PathBuf, game_dir: PathBuf, profile: Option<&str>) -> AppResult<Target> {
Target::resolve(base, game_dir, GAME, profile).map_err(profile_error)
}
pub fn profile_error(error: thunderstore_engine::Error) -> AppError {
match error {
thunderstore_engine::Error::BadProfile(BadProfile::Missing { name, .. }) => {
missing_profile_error(&name)
}
other => other.into(),
}
}
fn is_bare_shell_word(name: &str) -> bool {
!name.is_empty()
&& !name.starts_with('-')
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
}
pub fn missing_profile_error(name: &str) -> AppError {
let create = match is_bare_shell_word(name) {
true => format!("vmm profile create {name}"),
false => "vmm profile create <name>".to_string(),
};
AppError::advice(
format!("there is no profile named {name:?}."),
"Nothing was changed. Create it first, or list the profiles you have.",
&[create.as_str(), "vmm profile list"],
)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn describe_names_the_profile_or_the_game_directory() {
let base = tempdir().unwrap();
let game_dir = tempdir().unwrap();
let game = Target::resolve(
base.path().to_path_buf(),
game_dir.path().to_path_buf(),
GAME,
None,
)
.unwrap();
assert_eq!(describe(&game), "the game directory");
thunderstore_engine::profile::layout::create(base.path(), GAME, "experiment").unwrap();
let profile = Target::resolve(
base.path().to_path_buf(),
game_dir.path().to_path_buf(),
GAME,
Some("experiment"),
)
.unwrap();
assert_eq!(describe(&profile), "profile \"experiment\"");
}
#[test]
fn resolving_an_unsafe_profile_name_still_refuses() {
let base = tempdir().unwrap();
let game_dir = tempdir().unwrap();
let error = resolve(
base.path().to_path_buf(),
game_dir.path().to_path_buf(),
Some("../escape"),
)
.unwrap_err();
assert_eq!(
error.to_string(),
"Profile error: invalid profile name \"../escape\""
);
}
#[test]
fn resolve_returns_a_target_for_a_profile_that_exists() {
let base = tempdir().unwrap();
let game_dir = tempdir().unwrap();
thunderstore_engine::profile::layout::create(base.path(), GAME, "experiment").unwrap();
let target = resolve(
base.path().to_path_buf(),
game_dir.path().to_path_buf(),
Some("experiment"),
)
.unwrap();
assert_eq!(target.profile.as_deref(), Some("experiment"));
assert_eq!(
target.dir,
thunderstore_engine::profile::layout::profile_dir(base.path(), GAME, "experiment")
);
}
#[test]
fn an_ordinary_profile_name_is_offered_as_a_command_to_copy() {
let message = missing_profile_error("experiment").to_string();
assert!(
message.contains("vmm profile create experiment"),
"a name that needs no quoting must stay concrete; got: {message}"
);
assert!(message.contains("vmm profile list"), "got: {message}");
}
#[test]
fn a_name_needing_quoting_is_never_offered_as_a_command_to_copy() {
for name in [
"work profile",
"main; rm -rf ~",
"$(whoami)",
"a`id`b",
"-all",
] {
let message = missing_profile_error(name).to_string();
assert!(
message.contains("vmm profile create <name>"),
"{name:?} must degrade to the placeholder; got: {message}"
);
assert!(
!message.contains(&format!("vmm profile create {name}")),
"{name:?} must not appear in a copyable command; got: {message}"
);
assert!(
message.contains("there is no profile named"),
"got: {message}"
);
assert!(message.contains("vmm profile list"), "got: {message}");
}
}
}