// SPDX-License-Identifier: AGPL-3.0-only use std::sync::LazyLock; use std::path::Path; use std::fs; use std::sync::Mutex; use mlua::{Lua, LuaSerdeExt, Compiler, StdLib, Value, LuaOptions, Table}; use http::StatusCode; use crate::CONFIG; static INTERNAL_THEMES: &[(&str, &str)] = &[]; static LUA: LazyLock> = LazyLock::new(|| { let stdlib = StdLib::TABLE | StdLib::STRING | StdLib::UTF8 | StdLib::BIT | StdLib::MATH; let lua = Lua::new_with(stdlib, LuaOptions::new().thread_pool_size(2)).expect("lua initialization"); lua.sandbox(true).expect("lua initialization"); lua.set_compiler( if CONFIG.theme_dev { Compiler::new().set_optimization_level(0).set_debug_level(2) } else { Compiler::new().set_optimization_level(2).set_debug_level(1) } ); let expect: Value = lua.load(include_str!("lua-lib/expect.lua")) .set_compiler(Compiler::new().set_optimization_level(2).set_debug_level(1)) .eval() .expect("expect.lua loading"); lua.globals().set("expect", expect).unwrap(); let html: Value = lua.load(include_str!("lua-lib/html.lua")) .set_compiler(Compiler::new().set_optimization_level(2).set_debug_level(1)) .eval() .expect("html.lua loading"); lua.globals().set("html", html).unwrap(); let themes = lua.create_table().expect("creating themes table"); for (k, v) in INTERNAL_THEMES { log::info!("Registering compiled lua theme `{k}`."); let _ = themes.set(*k, lua.load(*v).eval::().expect("loading internal theme")); } if let Some(theme_dir) = CONFIG.theme_dir.as_ref() { if !CONFIG.theme_dev { log::info!("Registering lua theme dir `{theme_dir}` with extension `{}`.", CONFIG.theme_ext_lua); for (k, v) in walk_dir(theme_dir.as_ref().as_ref()).expect("walking theme dir") { log::info!("Registering runtime lua theme `{k}`."); let _ = themes.set(k, lua.load(v).eval::().expect("loading external theme")); } } else { log::info!("Ready to dev-load lua themes from `{theme_dir}` with extension `{}`.", CONFIG.theme_ext_lua); } } lua.globals().set("__themes", themes).unwrap(); lua.globals().set_readonly(true); Mutex::new(lua) }); fn walk_dir(path: &Path) -> std::io::Result> { let ext = CONFIG.theme_ext_lua.as_ref(); let mut path_bits = vec![]; let mut dir_readers = vec![fs::read_dir(path)?]; let mut ret = vec![]; while let Some(r) = dir_readers.iter_mut().last() { if let Some(ent) = r.next() { let ent = ent?; let name = ent.file_name().into_string().expect("why do you have such FUCKED UP FILE PATHS"); let ty = ent.file_type()?; if ty.is_file() && name.ends_with(CONFIG.theme_ext_lua.as_ref()) { ret.push((path_bits.join("") + &name[..name.len() - ext.len()], fs::read_to_string(ent.path())?)); } else if ty.is_dir() { path_bits.push(name); dir_readers.push(fs::read_dir(ent.path())?); } } else { dir_readers.pop(); } } Ok(ret) } pub fn render_theme(name: &str, ctx: &crate::ctx::Ctx) -> Option> { if name == ".." || name.starts_with("../") || name.ends_with("/..") || name.contains("/../") { return Some(Err(StatusCode::BAD_REQUEST)) } LUA.clear_poison(); let lua = LUA.lock().expect("FIXME: Mutex poisoning race condition."); let themes: Table = lua.globals().get("__themes").unwrap(); let theme; match themes.get(name) { Ok(Value::Function(themefn)) => { theme = themefn; }, Ok(Value::Nil) if !CONFIG.theme_dev || CONFIG.theme_dir.is_none() => { return None; }, Ok(Value::Nil) => { let code = fs::read_to_string(CONFIG.theme_dir.as_ref().unwrap().to_string() + "/" + name + &CONFIG.theme_ext_lua).ok()?; let res = lua.load(code).eval::(); match res { Ok(Value::Function(themefn)) => { theme = themefn; }, Ok(v) => { log::error!("Got `{v:?}` instead of Function when dev-loading `{name}`."); return Some(Err(StatusCode::INTERNAL_SERVER_ERROR)); }, Err(e) => { log::error!("Error dev-loading `{name}`: {e}"); return Some(Err(StatusCode::INTERNAL_SERVER_ERROR)); }, } }, Ok(v) => { log::error!("Got `{v:?}` instead of Function when loading `{name}`."); return Some(Err(StatusCode::INTERNAL_SERVER_ERROR)); } Err(e) => { log::error!("Error loading `{name}`: {e}"); return Some(Err(StatusCode::INTERNAL_SERVER_ERROR)); }, //TODO: gate behind flag } let ctx = match lua.to_value(ctx) { Ok(ok) => ok, Err(e) => { log::error!("Lua context serialization error: {e}"); return Some(Err(StatusCode::INTERNAL_SERVER_ERROR)) } }; Some(theme.call((ctx,)).map_err(|e| { log::error!("Lua theme execution error: {e}"); StatusCode::INTERNAL_SERVER_ERROR })) } pub fn touch() { LazyLock::force(&LUA); }