diff --git a/Cargo.lock b/Cargo.lock index ba688e1..bd49702 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,6 +855,7 @@ dependencies = [ "sdk", "serde", "serde_sjson", + "string_template", "strip-ansi-escapes", "time", "tokio", diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index 18ba7b1..86444be 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -32,3 +32,4 @@ colors-transform = "0.2.11" usvg = "0.25.0" druid-widget-nursery = "0.1" ansi-parser = "0.8.0" +string_template = "0.2.1" diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index 715397f..f811c5f 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -107,19 +107,35 @@ log("mod_main", "Initializing mods...") local require_store = {} +-- This token is treated as a string template and filled by DTMM during deployment. +-- This allows hiding unsafe I/O functions behind a setting. +-- It's also a valid table definition, thereby degrading gracefully when not replaced. +local is_io_enabled = {{is_io_enabled}} -- luacheck: ignore 113 +local lua_libs = { + debug = debug, + os = { + date = os.date, + time = os.time, + clock = os.clock, + getenv = os.getenv, + difftime = os.difftime, + }, + load = load, + loadfile = loadfile, + loadstring = loadstring, +} + +if is_io_enabled then + lua_libs.io = io + lua_libs.os = os + lua_libs.ffi = ffi +end + Mods = { -- Keep a backup of certain system libraries before -- Fatshark's code scrubs them. -- The loader can then decide to pass them on to mods, or ignore them - lua = setmetatable({}, { - io = io, - debug = debug, - ffi = ffi, - os = os, - load = load, - loadfile = loadfile, - loadstring = loadstring, - }), + lua = setmetatable({}, { __index = lua_libs }), require_store = require_store } diff --git a/crates/dtmm/src/controller/game.rs b/crates/dtmm/src/controller/game.rs index 4f5633e..a5f6fa8 100644 --- a/crates/dtmm/src/controller/game.rs +++ b/crates/dtmm/src/controller/game.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::io::{self, Cursor, ErrorKind}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -15,6 +16,7 @@ use sdk::{ Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary, }; use serde::{Deserialize, Serialize}; +use string_template::Template; use time::OffsetDateTime; use tokio::fs; use tokio::io::AsyncWriteExt; @@ -452,7 +454,13 @@ async fn patch_boot_bundle(state: Arc) -> Result> { let span = tracing::debug_span!("Importing mod main script"); let _enter = span.enter(); - let lua = include_str!("../../assets/mod_main.lua"); + let is_io_enabled = format!("{}", state.is_io_enabled); + let mut data = HashMap::new(); + data.insert("is_io_enabled", is_io_enabled.as_str()); + + let tmpl = include_str!("../../assets/mod_main.lua"); + let lua = Template::new(tmpl).render(&data); + tracing::trace!("Main script rendered:\n===========\n{}\n=============", lua); let file = lua::compile(MOD_BOOT_SCRIPT, lua).wrap_err("Failed to compile mod main Lua file")?; diff --git a/crates/dtmm/src/state/data.rs b/crates/dtmm/src/state/data.rs index b1adcb6..55774aa 100644 --- a/crates/dtmm/src/state/data.rs +++ b/crates/dtmm/src/state/data.rs @@ -153,6 +153,7 @@ pub(crate) struct State { pub is_save_in_progress: bool, pub is_next_save_pending: bool, pub is_update_in_progress: bool, + pub is_io_enabled: bool, pub game_dir: Arc, pub data_dir: Arc, pub nexus_api_key: Arc, @@ -189,6 +190,7 @@ impl State { is_save_in_progress: false, is_next_save_pending: false, is_update_in_progress: false, + is_io_enabled: false, config_path: Arc::new(PathBuf::new()), game_dir: Arc::new(PathBuf::new()), data_dir: Arc::new(PathBuf::new()), diff --git a/crates/dtmm/src/state/delegate.rs b/crates/dtmm/src/state/delegate.rs index 2e12bd0..97c6e6b 100644 --- a/crates/dtmm/src/state/delegate.rs +++ b/crates/dtmm/src/state/delegate.rs @@ -1,4 +1,5 @@ -use std::{path::PathBuf, sync::Arc}; +use std::path::PathBuf; +use std::sync::Arc; use color_eyre::Report; use druid::im::Vector; @@ -8,8 +9,9 @@ use druid::{ }; use tokio::sync::mpsc::UnboundedSender; +use crate::ui::window; use crate::util::ansi::ansi_to_rich_text; -use crate::{ui::window, util::config::Config}; +use crate::util::config::Config; use super::{ModInfo, State}; @@ -68,6 +70,7 @@ pub(crate) struct ActionState { pub config_path: Arc, pub ctx: Arc, pub nexus_api_key: Arc, + pub is_io_enabled: bool, } impl From for ActionState { @@ -80,6 +83,7 @@ impl From for ActionState { config_path: state.config_path, ctx: state.ctx, nexus_api_key: state.nexus_api_key, + is_io_enabled: state.is_io_enabled, } } } @@ -407,6 +411,7 @@ impl AppDelegate for Delegate { state.config_path = Arc::new(config.path); state.data_dir = Arc::new(config.data_dir); state.game_dir = Arc::new(config.game_dir.unwrap_or_default()); + state.is_io_enabled = config.unsafe_io; } state.loading = false; diff --git a/crates/dtmm/src/ui/widget/controller.rs b/crates/dtmm/src/ui/widget/controller.rs index b6d3806..f789b5a 100644 --- a/crates/dtmm/src/ui/widget/controller.rs +++ b/crates/dtmm/src/ui/widget/controller.rs @@ -80,11 +80,19 @@ impl> Controller for DirtyStateController { ) { // Only start tracking changes after the initial load has finished if old_data.loading == data.loading { - if compare_state_fields!(old_data, data, mods, game_dir, data_dir, nexus_api_key) { + if compare_state_fields!( + old_data, + data, + mods, + game_dir, + data_dir, + nexus_api_key, + is_io_enabled + ) { ctx.submit_command(ACTION_START_SAVE_SETTINGS); } - if compare_state_fields!(old_data, data, mods, game_dir) { + if compare_state_fields!(old_data, data, mods, game_dir, is_io_enabled) { ctx.submit_command(ACTION_SET_DIRTY); } } diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index fbedb8f..7aa2819 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -425,6 +425,29 @@ fn build_view_settings() -> impl Widget { .with_flex_child(TextBox::new().expand_width().lens(State::nexus_api_key), 1.) .expand_width(); + let io_setting = Flex::row() + .must_fill_main_axis(true) + .main_axis_alignment(MainAxisAlignment::Start) + .with_child(Label::new("Enable unsafe I/O:")) + .with_default_spacer() + .with_child(Checkbox::from_label(Label::dynamic( + |enabled: &bool, _: &Env| { + if *enabled { + "Enabled".into() + } else { + "Disabled".into() + } + }, + ))) + .lens(State::is_io_enabled) + .tooltip(|_: &State, _: &Env| { + "Enabling this gives ANY mod full access to your files \ + and the ability to load arbitrary software libraries.\n\ + Only enable this if it is crucial for a mod's functionality, \ + and you are sure none of the ones you have installed are malicious." + }) + .expand_width(); + let content = Flex::column() .must_fill_main_axis(true) .cross_axis_alignment(CrossAxisAlignment::Start) @@ -432,6 +455,8 @@ fn build_view_settings() -> impl Widget { .with_default_spacer() .with_child(game_dir_setting) .with_default_spacer() + .with_child(io_setting) + .with_default_spacer() .with_child(nexus_apy_key_setting); SizedBox::new(content) diff --git a/crates/dtmm/src/util/config.rs b/crates/dtmm/src/util/config.rs index 1cafeae..3a0d0b2 100644 --- a/crates/dtmm/src/util/config.rs +++ b/crates/dtmm/src/util/config.rs @@ -30,6 +30,7 @@ pub(crate) struct ConfigSerialize<'a> { data_dir: &'a Path, nexus_api_key: &'a String, mod_order: Vec>, + unsafe_io: bool, } impl<'a> From<&'a ActionState> for ConfigSerialize<'a> { @@ -38,6 +39,7 @@ impl<'a> From<&'a ActionState> for ConfigSerialize<'a> { game_dir: &state.game_dir, data_dir: &state.data_dir, nexus_api_key: &state.nexus_api_key, + unsafe_io: state.is_io_enabled, mod_order: state .mods .iter() @@ -61,6 +63,8 @@ pub(crate) struct Config { #[serde(default = "get_default_data_dir")] pub data_dir: PathBuf, pub game_dir: Option, + #[serde(default)] + pub unsafe_io: bool, pub nexus_api_key: Option, #[serde(default)] pub mod_order: Vec, @@ -144,6 +148,7 @@ where game_dir: None, nexus_api_key: None, mod_order: Vec::new(), + unsafe_io: false, }; {