feat(dtmm): Guard certain Lua libraries behind a setting #114
9 changed files with 85 additions and 14 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -855,6 +855,7 @@ dependencies = [
|
||||||
"sdk",
|
"sdk",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_sjson",
|
"serde_sjson",
|
||||||
|
"string_template",
|
||||||
"strip-ansi-escapes",
|
"strip-ansi-escapes",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
@ -32,3 +32,4 @@ colors-transform = "0.2.11"
|
||||||
usvg = "0.25.0"
|
usvg = "0.25.0"
|
||||||
druid-widget-nursery = "0.1"
|
druid-widget-nursery = "0.1"
|
||||||
ansi-parser = "0.8.0"
|
ansi-parser = "0.8.0"
|
||||||
|
string_template = "0.2.1"
|
||||||
|
|
|
@ -107,19 +107,35 @@ log("mod_main", "Initializing mods...")
|
||||||
|
|
||||||
local require_store = {}
|
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 = {
|
Mods = {
|
||||||
-- Keep a backup of certain system libraries before
|
-- Keep a backup of certain system libraries before
|
||||||
-- Fatshark's code scrubs them.
|
-- Fatshark's code scrubs them.
|
||||||
-- The loader can then decide to pass them on to mods, or ignore them
|
-- The loader can then decide to pass them on to mods, or ignore them
|
||||||
lua = setmetatable({}, {
|
lua = setmetatable({}, { __index = lua_libs }),
|
||||||
io = io,
|
|
||||||
debug = debug,
|
|
||||||
ffi = ffi,
|
|
||||||
os = os,
|
|
||||||
load = load,
|
|
||||||
loadfile = loadfile,
|
|
||||||
loadstring = loadstring,
|
|
||||||
}),
|
|
||||||
require_store = require_store
|
require_store = require_store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io::{self, Cursor, ErrorKind};
|
use std::io::{self, Cursor, ErrorKind};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -15,6 +16,7 @@ use sdk::{
|
||||||
Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary,
|
Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use string_template::Template;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
|
@ -452,7 +454,13 @@ async fn patch_boot_bundle(state: Arc<ActionState>) -> Result<Vec<Bundle>> {
|
||||||
let span = tracing::debug_span!("Importing mod main script");
|
let span = tracing::debug_span!("Importing mod main script");
|
||||||
let _enter = span.enter();
|
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 =
|
let file =
|
||||||
lua::compile(MOD_BOOT_SCRIPT, lua).wrap_err("Failed to compile mod main Lua file")?;
|
lua::compile(MOD_BOOT_SCRIPT, lua).wrap_err("Failed to compile mod main Lua file")?;
|
||||||
|
|
||||||
|
|
|
@ -153,6 +153,7 @@ pub(crate) struct State {
|
||||||
pub is_save_in_progress: bool,
|
pub is_save_in_progress: bool,
|
||||||
pub is_next_save_pending: bool,
|
pub is_next_save_pending: bool,
|
||||||
pub is_update_in_progress: bool,
|
pub is_update_in_progress: bool,
|
||||||
|
pub is_io_enabled: bool,
|
||||||
pub game_dir: Arc<PathBuf>,
|
pub game_dir: Arc<PathBuf>,
|
||||||
pub data_dir: Arc<PathBuf>,
|
pub data_dir: Arc<PathBuf>,
|
||||||
pub nexus_api_key: Arc<String>,
|
pub nexus_api_key: Arc<String>,
|
||||||
|
@ -189,6 +190,7 @@ impl State {
|
||||||
is_save_in_progress: false,
|
is_save_in_progress: false,
|
||||||
is_next_save_pending: false,
|
is_next_save_pending: false,
|
||||||
is_update_in_progress: false,
|
is_update_in_progress: false,
|
||||||
|
is_io_enabled: false,
|
||||||
config_path: Arc::new(PathBuf::new()),
|
config_path: Arc::new(PathBuf::new()),
|
||||||
game_dir: Arc::new(PathBuf::new()),
|
game_dir: Arc::new(PathBuf::new()),
|
||||||
data_dir: Arc::new(PathBuf::new()),
|
data_dir: Arc::new(PathBuf::new()),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use druid::im::Vector;
|
use druid::im::Vector;
|
||||||
|
@ -8,8 +9,9 @@ use druid::{
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
use crate::ui::window;
|
||||||
use crate::util::ansi::ansi_to_rich_text;
|
use crate::util::ansi::ansi_to_rich_text;
|
||||||
use crate::{ui::window, util::config::Config};
|
use crate::util::config::Config;
|
||||||
|
|
||||||
use super::{ModInfo, State};
|
use super::{ModInfo, State};
|
||||||
|
|
||||||
|
@ -68,6 +70,7 @@ pub(crate) struct ActionState {
|
||||||
pub config_path: Arc<PathBuf>,
|
pub config_path: Arc<PathBuf>,
|
||||||
pub ctx: Arc<sdk::Context>,
|
pub ctx: Arc<sdk::Context>,
|
||||||
pub nexus_api_key: Arc<String>,
|
pub nexus_api_key: Arc<String>,
|
||||||
|
pub is_io_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<State> for ActionState {
|
impl From<State> for ActionState {
|
||||||
|
@ -80,6 +83,7 @@ impl From<State> for ActionState {
|
||||||
config_path: state.config_path,
|
config_path: state.config_path,
|
||||||
ctx: state.ctx,
|
ctx: state.ctx,
|
||||||
nexus_api_key: state.nexus_api_key,
|
nexus_api_key: state.nexus_api_key,
|
||||||
|
is_io_enabled: state.is_io_enabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -407,6 +411,7 @@ impl AppDelegate<State> for Delegate {
|
||||||
state.config_path = Arc::new(config.path);
|
state.config_path = Arc::new(config.path);
|
||||||
state.data_dir = Arc::new(config.data_dir);
|
state.data_dir = Arc::new(config.data_dir);
|
||||||
state.game_dir = Arc::new(config.game_dir.unwrap_or_default());
|
state.game_dir = Arc::new(config.game_dir.unwrap_or_default());
|
||||||
|
state.is_io_enabled = config.unsafe_io;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
|
|
@ -80,11 +80,19 @@ impl<W: Widget<State>> Controller<State, W> for DirtyStateController {
|
||||||
) {
|
) {
|
||||||
// Only start tracking changes after the initial load has finished
|
// Only start tracking changes after the initial load has finished
|
||||||
if old_data.loading == data.loading {
|
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);
|
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);
|
ctx.submit_command(ACTION_SET_DIRTY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -425,6 +425,29 @@ fn build_view_settings() -> impl Widget<State> {
|
||||||
.with_flex_child(TextBox::new().expand_width().lens(State::nexus_api_key), 1.)
|
.with_flex_child(TextBox::new().expand_width().lens(State::nexus_api_key), 1.)
|
||||||
.expand_width();
|
.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()
|
let content = Flex::column()
|
||||||
.must_fill_main_axis(true)
|
.must_fill_main_axis(true)
|
||||||
.cross_axis_alignment(CrossAxisAlignment::Start)
|
.cross_axis_alignment(CrossAxisAlignment::Start)
|
||||||
|
@ -432,6 +455,8 @@ fn build_view_settings() -> impl Widget<State> {
|
||||||
.with_default_spacer()
|
.with_default_spacer()
|
||||||
.with_child(game_dir_setting)
|
.with_child(game_dir_setting)
|
||||||
.with_default_spacer()
|
.with_default_spacer()
|
||||||
|
.with_child(io_setting)
|
||||||
|
.with_default_spacer()
|
||||||
.with_child(nexus_apy_key_setting);
|
.with_child(nexus_apy_key_setting);
|
||||||
|
|
||||||
SizedBox::new(content)
|
SizedBox::new(content)
|
||||||
|
|
|
@ -30,6 +30,7 @@ pub(crate) struct ConfigSerialize<'a> {
|
||||||
data_dir: &'a Path,
|
data_dir: &'a Path,
|
||||||
nexus_api_key: &'a String,
|
nexus_api_key: &'a String,
|
||||||
mod_order: Vec<LoadOrderEntrySerialize<'a>>,
|
mod_order: Vec<LoadOrderEntrySerialize<'a>>,
|
||||||
|
unsafe_io: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a ActionState> for ConfigSerialize<'a> {
|
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,
|
game_dir: &state.game_dir,
|
||||||
data_dir: &state.data_dir,
|
data_dir: &state.data_dir,
|
||||||
nexus_api_key: &state.nexus_api_key,
|
nexus_api_key: &state.nexus_api_key,
|
||||||
|
unsafe_io: state.is_io_enabled,
|
||||||
mod_order: state
|
mod_order: state
|
||||||
.mods
|
.mods
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -61,6 +63,8 @@ pub(crate) struct Config {
|
||||||
#[serde(default = "get_default_data_dir")]
|
#[serde(default = "get_default_data_dir")]
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
pub game_dir: Option<PathBuf>,
|
pub game_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub unsafe_io: bool,
|
||||||
pub nexus_api_key: Option<String>,
|
pub nexus_api_key: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mod_order: Vec<LoadOrderEntry>,
|
pub mod_order: Vec<LoadOrderEntry>,
|
||||||
|
@ -144,6 +148,7 @@ where
|
||||||
game_dir: None,
|
game_dir: None,
|
||||||
nexus_api_key: None,
|
nexus_api_key: None,
|
||||||
mod_order: Vec::new(),
|
mod_order: Vec::new(),
|
||||||
|
unsafe_io: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Reference in a new issue