Files
shacraft-launcher/src-tauri/src/settings.rs
T

199 lines
5.9 KiB
Rust

use crate::download;
use serde::{Deserialize, Serialize};
use std::{fmt, fs, io, path::Path};
const SETTINGS_FILE: &str = "settings.json";
const MIN_MEMORY_MB: u16 = 3 * 1024;
const MAX_MEMORY_MB: u16 = 12 * 1024;
const DEFAULT_MEMORY_MB: u16 = 6 * 1024;
const DEFAULT_NICKNAME: &str = "Emil";
/// Which account the player launches as. `Microsoft` requires a real signed-in
/// session; `Offline` uses the local nickname (no Microsoft account needed).
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AccountMode {
Microsoft,
Offline,
}
impl Default for AccountMode {
fn default() -> Self {
Self::Offline
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LauncherSettings {
pub memory_mb: u16,
#[serde(default = "default_nickname")]
pub nickname: String,
#[serde(default)]
pub account_mode: AccountMode,
}
fn default_nickname() -> String {
DEFAULT_NICKNAME.into()
}
impl Default for LauncherSettings {
fn default() -> Self {
Self {
memory_mb: DEFAULT_MEMORY_MB,
nickname: DEFAULT_NICKNAME.into(),
account_mode: AccountMode::Offline,
}
}
}
#[derive(Debug)]
pub enum SettingsError {
Io(io::Error),
InvalidJson(serde_json::Error),
InvalidMemory,
InvalidNickname,
}
impl fmt::Display for SettingsError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(formatter, "Cannot access launcher settings: {error}"),
Self::InvalidJson(error) => write!(formatter, "Cannot read launcher settings: {error}"),
Self::InvalidMemory => {
write!(formatter, "Memory allocation must be between 3 and 12 GiB")
}
Self::InvalidNickname => write!(
formatter,
"Nickname must be 3-16 ASCII letters, numbers, or underscores"
),
}
}
}
pub fn load(data_dir: &Path) -> Result<LauncherSettings, SettingsError> {
let path = data_dir.join(SETTINGS_FILE);
let source = match fs::read_to_string(path) {
Ok(source) => source,
Err(error) if error.kind() == io::ErrorKind::NotFound => {
return Ok(LauncherSettings::default())
}
Err(error) => return Err(SettingsError::Io(error)),
};
let settings = serde_json::from_str(&source).map_err(SettingsError::InvalidJson)?;
validate(&settings)?;
Ok(settings)
}
pub fn save(
data_dir: &Path,
settings: LauncherSettings,
) -> Result<LauncherSettings, SettingsError> {
validate(&settings)?;
fs::create_dir_all(data_dir).map_err(SettingsError::Io)?;
let target = data_dir.join(SETTINGS_FILE);
let temporary = data_dir.join(".settings.json.shacraft.part");
let contents = serde_json::to_vec_pretty(&settings).expect("LauncherSettings is serializable");
fs::write(&temporary, contents).map_err(SettingsError::Io)?;
download::replace_file(&temporary, &target).map_err(SettingsError::Io)?;
Ok(settings)
}
fn validate(settings: &LauncherSettings) -> Result<(), SettingsError> {
if !(MIN_MEMORY_MB..=MAX_MEMORY_MB).contains(&settings.memory_mb)
|| settings.memory_mb % 1024 != 0
{
return Err(SettingsError::InvalidMemory);
}
if !(3..=16).contains(&settings.nickname.len())
|| !settings
.nickname
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
{
return Err(SettingsError::InvalidNickname);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{load, save, AccountMode, LauncherSettings};
use std::{
fs, process,
time::{SystemTime, UNIX_EPOCH},
};
fn temporary_directory() -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"shacraft-settings-test-{}-{}",
process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
))
}
#[test]
fn defaults_then_persists_memory() {
let directory = temporary_directory();
let default = load(&directory).unwrap();
assert_eq!(default.memory_mb, 6 * 1024);
assert_eq!(default.nickname, "Emil");
assert_eq!(default.account_mode, AccountMode::Offline);
let saved = save(
&directory,
LauncherSettings {
memory_mb: 8 * 1024,
nickname: "Emil".into(),
account_mode: AccountMode::Microsoft,
},
)
.unwrap();
assert_eq!(saved.memory_mb, 8 * 1024);
assert_eq!(
load(&directory).unwrap().account_mode,
AccountMode::Microsoft
);
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn rejects_unsafe_memory_values() {
let directory = temporary_directory();
assert!(save(
&directory,
LauncherSettings {
memory_mb: 512,
nickname: "Emil".into(),
account_mode: AccountMode::Offline
}
)
.is_err());
assert!(save(
&directory,
LauncherSettings {
memory_mb: 6 * 1024,
nickname: "невалидный".into(),
account_mode: AccountMode::Offline
}
)
.is_err());
}
#[test]
fn old_settings_without_nickname_defaults_gracefully() {
let directory = temporary_directory();
fs::create_dir_all(&directory).unwrap();
fs::write(directory.join("settings.json"), r#"{"memoryMb": 6144}"#).unwrap();
let settings = load(&directory).unwrap();
assert_eq!(settings.memory_mb, 6 * 1024);
assert_eq!(settings.nickname, "Emil");
fs::remove_dir_all(directory).unwrap();
}
}