aboutsummaryrefslogblamecommitdiffstats
path: root/src/theming/lua.rs
blob: 118762b2fa8a3de10dd2069dc6eaa9ea9d1ec7bd (plain) (tree)
1
2
3
4
5
6
7
8

                                         
                    
            
 
                     
                                                                                           
                     


                  

                                              






                                                                                                      
                                                                                        






                                                                               
                                               




                                                                               




                                                                  
                                                        
                                                                                          


                                                      








                                                                                                               


     
                                                 

                                   


                 

                                                                    
                                          











                                                                                                   
                                                                                                          













                                                    
                                                                                              



                                                                                                


                                                                        















                                                                                                                                                     
 





                                                







                                                                                                                            
 



                        
// 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, SerializeOptions};
use http::StatusCode;

use crate::CONFIG;

static INTERNAL_THEMES: &[(&str, &str)] = &[];

static LUA: LazyLock<Mutex<Lua>> = 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::<Value>().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::<Value>().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<Vec<(String, String)>> {
  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<Result<String, StatusCode>> {
  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::<Value>();
      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 opts = SerializeOptions::new()
    .serialize_none_to_null(false)
    .serialize_unit_to_null(false)
    .set_array_metatable(false);

  let ctx = match lua.to_value_with(ctx, opts) {
    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);
}