/* * Copyright © 2023 Collabora Ltd. * Copyright © 2024 Valve Software * * SPDX-License-Identifier: MIT */ use anyhow::Result; use nix::errno::Errno; use nix::unistd::{access, AccessFlags}; use serde::Deserialize; use std::path::PathBuf; use tokio::fs::{metadata, read_to_string}; #[cfg(not(test))] use tokio::sync::OnceCell; use tokio::task::spawn_blocking; #[cfg(not(test))] use crate::hardware::is_deck; #[cfg(not(test))] static CONFIG: OnceCell> = OnceCell::const_new(); #[derive(Clone, Default, Deserialize, Debug)] #[serde(default)] pub(crate) struct PlatformConfig { pub factory_reset: Option, pub update_bios: Option, pub update_dock: Option, pub storage: Option, pub fan_control: Option, pub tdp_limit: Option>, pub gpu_clocks: Option>, pub battery_charge_limit: Option, } #[derive(Clone, Deserialize, Debug)] pub(crate) struct RangeConfig { pub min: T, pub max: T, } impl Copy for RangeConfig where T: Copy {} #[derive(Clone, Default, Deserialize, Debug)] pub(crate) struct ScriptConfig { pub script: PathBuf, #[serde(default)] pub script_args: Vec, } impl ScriptConfig { pub(crate) async fn is_valid(&self) -> Result { let script = self.script.clone(); if !spawn_blocking(move || match access(&script, AccessFlags::X_OK) { Ok(()) => Ok(true), Err(Errno::ENOENT | Errno::EACCES) => Ok(false), Err(e) => Err(e), }) .await?? { return Ok(false); } if !metadata(&self.script).await?.is_file() { return Ok(false); } Ok(true) } } #[derive(Clone, Default, Deserialize, Debug)] pub(crate) struct ResetConfig { pub all: ScriptConfig, pub os: ScriptConfig, pub user: ScriptConfig, } #[derive(Clone, Deserialize, Debug)] pub(crate) enum ServiceConfig { #[serde(rename = "systemd")] Systemd(String), #[serde(rename = "script")] Script { start: ScriptConfig, stop: ScriptConfig, status: ScriptConfig, }, } #[derive(Clone, Default, Deserialize, Debug)] pub(crate) struct StorageConfig { pub trim_devices: ScriptConfig, pub format_device: FormatDeviceConfig, } #[derive(Clone, Deserialize, Debug)] pub(crate) struct BatteryChargeLimitConfig { pub suggested_minimum_limit: Option, pub hwmon_name: String, pub attribute: String, } #[derive(Clone, Default, Deserialize, Debug)] pub(crate) struct FormatDeviceConfig { pub script: PathBuf, #[serde(default)] pub script_args: Vec, pub label_flag: String, #[serde(default)] pub device_flag: Option, #[serde(default)] pub validate_flag: Option, #[serde(default)] pub no_validate_flag: Option, } impl RangeConfig { #[allow(unused)] pub(crate) fn new(min: T, max: T) -> RangeConfig { RangeConfig { min, max } } } impl PlatformConfig { #[cfg(not(test))] async fn load() -> Result> { if !is_deck().await? { // Non-Steam Deck platforms are not yet supported return Ok(None); } let config = read_to_string("/usr/share/steamos-manager/platforms/jupiter.toml").await?; Ok(Some(toml::from_str(config.as_ref())?)) } #[cfg(test)] pub(crate) fn set_test_paths(&mut self) { if let Some(ref mut update_bios) = self.update_bios { if update_bios.script.as_os_str().is_empty() { update_bios.script = crate::path("exe"); } } if let Some(ref mut update_dock) = self.update_dock { if update_dock.script.as_os_str().is_empty() { update_dock.script = crate::path("exe"); } } } } #[cfg(not(test))] pub(crate) async fn platform_config() -> Result<&'static Option> { CONFIG.get_or_try_init(PlatformConfig::load).await } #[cfg(test)] pub(crate) async fn platform_config() -> Result> { let test = crate::testing::current(); let config = test.platform_config.borrow().clone(); Ok(config) } #[cfg(test)] mod test { use super::*; use crate::{path, testing}; use std::os::unix::fs::PermissionsExt; use tokio::fs::{set_permissions, write}; #[tokio::test] async fn script_config_valid_no_path() { assert!(!ScriptConfig::default().is_valid().await.unwrap()); } #[tokio::test] async fn script_config_valid_directory() { assert!(!ScriptConfig { script: PathBuf::from("/"), script_args: Vec::new(), } .is_valid() .await .unwrap()); } #[tokio::test] async fn script_config_valid_noexec() { let _handle = testing::start(); let exe_path = path("exe"); write(&exe_path, "").await.unwrap(); set_permissions(&exe_path, PermissionsExt::from_mode(0o600)).await.unwrap(); assert!(!ScriptConfig { script: exe_path, script_args: Vec::new(), } .is_valid() .await .unwrap()); } #[tokio::test] async fn script_config_valid() { let _handle = testing::start(); let exe_path = path("exe"); write(&exe_path, "").await.unwrap(); set_permissions(&exe_path, PermissionsExt::from_mode(0o700)).await.unwrap(); assert!(ScriptConfig { script: exe_path, script_args: Vec::new(), } .is_valid() .await .unwrap()); } #[tokio::test] async fn jupiter_valid() { let config = read_to_string("data/platforms/jupiter.toml") .await .expect("read_to_string"); let res = toml::from_str::(config.as_ref()); assert!(res.is_ok(), "{res:?}"); } }