aboutsummaryrefslogtreecommitdiffstats
path: root/src/theming/lua.rs
blob: 0699f09fafef732cc5f5b8cd9ee8c2c3434ffc85 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// 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<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 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);
}