1use std::rc::Rc;
15
16use mlua::{AnyUserData, Lua, Value};
17use retex_engine::input::LuaItem;
18use retex_engine::{Engine, LuaHost};
19
20mod callback;
21mod compat;
22mod extras;
23mod fontlib;
24mod fontloaderlib;
25mod fonttable;
26mod intarray;
27mod kpselib;
28mod langlib;
29mod lfslib;
30mod lualib;
31pub mod luaotfload;
32mod nestlib;
33pub mod nodelib;
34mod otfaux;
35mod otfnodes;
36mod pdflib;
37mod siolib;
38mod statuslib;
39mod texiolib;
40mod texlib;
41mod tokenlib;
42mod vflib;
43
44#[derive(Default)]
48pub(crate) struct EngineSlot(std::cell::RefCell<Vec<AnyUserData>>);
49
50pub(crate) struct PrintStack(pub Vec<Vec<LuaItem>>);
57
58impl Default for PrintStack {
59 fn default() -> Self {
60 Self(vec![Vec::new()])
61 }
62}
63
64pub(crate) fn with_engine<R>(lua: &Lua, f: impl FnOnce(&mut Engine) -> R) -> mlua::Result<R> {
66 let ud = lua
68 .app_data_ref::<EngineSlot>()
69 .and_then(|s| s.0.borrow().last().cloned())
70 .ok_or_else(|| mlua::Error::runtime("no engine"))?;
71 ud.borrow_mut_scoped::<Engine, R>(f)
72 .map_err(|e| mlua::Error::runtime(format!("engine borrow: {e}")))
73}
74
75const PLAIN_ERRORS_KEY: &str = "retex.plainerrors";
77
78const SETNAME_KEY: &str = "retex.plainerrors.setname";
81
82thread_local! {
83 static LAST_WRAPPED_ERROR: std::cell::RefCell<Option<String>> =
86 const { std::cell::RefCell::new(None) };
87}
88
89pub(crate) fn create_function_plain_errors<A, R, F>(lua: &Lua, f: F) -> mlua::Result<mlua::Function>
94where
95 A: mlua::FromLuaMulti,
96 R: mlua::IntoLuaMulti,
97 F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
98{
99 plain_errors(lua, f, 0)
100}
101
102pub(crate) fn create_function_caller_errors<A, R, F>(
107 lua: &Lua,
108 f: F,
109) -> mlua::Result<mlua::Function>
110where
111 A: mlua::FromLuaMulti,
112 R: mlua::IntoLuaMulti,
113 F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
114{
115 plain_errors(lua, f, 2)
116}
117
118fn plain_errors<A, R, F>(lua: &Lua, f: F, level: u8) -> mlua::Result<mlua::Function>
120where
121 A: mlua::FromLuaMulti,
122 R: mlua::IntoLuaMulti,
123 F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
124{
125 let inner = lua.create_function(move |lua, args: A| {
126 let mut out = match f(lua, args) {
127 Ok(r) => r.into_lua_multi(lua)?,
128 Err(e) => {
129 let msg = lua.create_string(lua_error_message(&e))?;
130 return Ok(mlua::MultiValue::from_vec(vec![
131 Value::Boolean(false),
132 Value::String(msg),
133 ]));
134 }
135 };
136 out.push_front(Value::Boolean(true));
137 Ok(out)
138 })?;
139 let key = format!("{PLAIN_ERRORS_KEY}{level}");
141 let factory: mlua::Function = match lua.named_registry_value::<Value>(&key)? {
142 Value::Function(f) => f,
143 _ => {
144 let f = lua
145 .load(format!(
146 "local f, setname = ...
147 local wrapper
148 local function finish(ok, ...)
149 if ok then return ... end
150 setname(wrapper)
151 error((...), {level})
152 end
153 wrapper = function(...) return finish(f(...)) end
154 return wrapper"
155 ))
156 .set_name("=[retex]")
157 .into_function()?;
158 lua.set_named_registry_value(&key, &f)?;
159 f
160 }
161 };
162 let setname: mlua::Function = match lua.named_registry_value::<Value>(SETNAME_KEY)? {
163 Value::Function(f) => f,
164 _ => {
165 let f = lua
166 .load(
167 "local set = ...
168 return function(fn)
169 for k, v in pairs(package.loaded) do
170 if type(v) == 'table' then
171 for k2, v2 in pairs(v) do
172 if v2 == fn then
173 local n = k .. '.' .. tostring(k2)
174 if k == '_G' then n = tostring(k2) end
175 set(n)
176 return
177 end
178 end
179 end
180 end
181 set(nil)
182 end",
183 )
184 .set_name("=[retex]")
185 .into_function()?
186 .call::<mlua::Function>(lua.create_function(|_, n: Option<String>| {
187 LAST_WRAPPED_ERROR.with(|c| *c.borrow_mut() = n);
188 Ok(())
189 })?)?;
190 lua.set_named_registry_value(SETNAME_KEY, &f)?;
191 f
192 }
193 };
194 factory.call((inner, setname))
195}
196
197pub(crate) fn take_printed(lua: &Lua) -> Vec<LuaItem> {
199 lua.app_data_mut::<PrintStack>()
200 .map(|mut ps| ps.0.iter_mut().flat_map(std::mem::take).collect::<Vec<_>>())
201 .unwrap_or_default()
202}
203
204pub(crate) fn push_print(lua: &Lua, item: LuaItem) {
206 if let Some(mut ps) = lua.app_data_mut::<PrintStack>()
207 && let Some(top) = ps.0.last_mut()
208 {
209 top.push(item);
210 }
211}
212
213pub struct LuaTexHost {
215 lua: Lua,
216 functions: std::cell::OnceCell<Option<mlua::Table>>,
218 intarrays: intarray::Intarrays,
220}
221
222const PROFILER_START: &str = r#"
226local samples, getinfo = {}, debug.getinfo
227debug.sethook(function()
228 local i = getinfo(2, "S")
229 if i then
230 local k = i.short_src .. ":" .. i.linedefined
231 samples[k] = (samples[k] or 0) + 1
232 end
233end, "", 1000)
234__retex_profile = samples
235"#;
236
237const PROFILER_REPORT: &str = r#"
238local s = __retex_profile
239if not s then return end
240debug.sethook()
241local t, total = {}, 0
242for k, n in pairs(s) do t[#t + 1] = {k, n}; total = total + n end
243table.sort(t, function(a, b) return a[2] > b[2] end)
244io.stderr:write(string.format("[lua profile] %d samples (1 per 1000 instructions)\n", total))
245for i = 1, math.min(25, #t) do
246 io.stderr:write(string.format("[lua profile] %6d %5.1f%% %s\n", t[i][2], 100 * t[i][2] / total, t[i][1]))
247end
248"#;
249
250impl Drop for LuaTexHost {
251 fn drop(&mut self) {
252 if std::env::var_os("RETEX_LUA_PROFILE").is_some() {
253 let _ = self.lua.load(PROFILER_REPORT).exec();
254 }
255 }
256}
257
258impl LuaTexHost {
259 pub fn new() -> mlua::Result<Rc<Self>> {
261 #[allow(unsafe_code)]
266 let lua = unsafe {
267 Lua::unsafe_new_with(
268 mlua::StdLib::ALL,
269 mlua::LuaOptions::new().catch_rust_panics(false),
271 )
272 };
273 lua.set_app_data(PrintStack::default());
274 lua.set_app_data(EngineSlot::default());
275 retex_engine::timing::mark("create: Lua state");
276 compat::install(&lua)?;
277 texlib::install(&lua)?;
278 texiolib::install(&lua)?;
279 lualib::install(&lua)?;
280 callback::install(&lua)?;
281 kpselib::install(&lua)?;
282 statuslib::install(&lua)?;
283 lfslib::install(&lua)?;
284 nodelib::install(&lua)?;
285 tokenlib::install(&lua)?;
286 extras::install(&lua)?;
287 luaotfload::install(&lua)?;
288 langlib::install(&lua)?;
289 fontlib::install(&lua)?;
290 fontloaderlib::install(&lua)?;
291 pdflib::install(&lua)?;
292 vflib::install(&lua)?;
293 retex_engine::timing::mark("create: Lua libraries");
294 retex_lpeg::install(&lua)?;
295 retex_engine::timing::mark("create: LPeg");
296 siolib::install(&lua)?;
297 let arg = lua.create_table()?;
300 for (i, a) in std::env::args_os().enumerate() {
301 arg.raw_set(i as i64, lua.create_string(a.as_encoded_bytes())?)?;
302 }
303 lua.globals().set("arg", arg)?;
304 let output_directory = lua.create_function(|lua, ()| {
306 Ok(crate::with_engine(lua, |e| {
307 e.files
308 .output_dir
309 .as_ref()
310 .map(|d| d.to_string_lossy().into_owned())
311 })
312 .ok()
313 .flatten())
314 })?;
315 let private_output_directory = lua.create_function(|lua, ()| {
318 Ok(crate::with_engine(lua, |e| e.files.private_output_dir).unwrap_or(false))
319 })?;
320 lua.load(include_str!("shell.lua"))
321 .set_name("=shell")
322 .call::<()>((output_directory, private_output_directory))?;
323 lua.load(
325 "for _, n in ipairs({'callback', 'fio', 'font', 'fontloader', 'gzip', 'img',
326 'kpse', 'lang', 'lua', 'md5', 'mplib', 'node', 'pdf', 'pdfe', 'pdfscanner',
327 'sha2', 'sio', 'status', 'tex', 'texio', 'token', 'unicode', 'vf', 'zip',
328 'zlib'}) do
329 if type(_G[n]) == 'table' and package.loaded[n] == nil then
330 package.loaded[n] = _G[n]
331 end
332 end",
333 )
334 .set_name("=[retex]")
335 .exec()?;
336 if std::env::var_os("RETEX_LUA_PROFILE").is_some() {
337 lua.load(PROFILER_START).set_name("=profiler").exec()?;
338 }
339 let intarrays = intarray::Intarrays::new(&lua);
340 Ok(Rc::new(LuaTexHost {
341 lua,
342 functions: std::cell::OnceCell::new(),
343 intarrays,
344 }))
345 }
346
347 pub fn lua(&self) -> &Lua {
349 &self.lua
350 }
351
352 pub fn attach(engine: &mut Engine) -> mlua::Result<Rc<Self>> {
354 let host = LuaTexHost::new()?;
355 engine.lua = Some(host.clone() as Rc<dyn LuaHost>);
356 Ok(host)
357 }
358
359 pub fn check_io_names(&self) -> mlua::Result<()> {
363 kpselib::check_io_names(&self.lua)
364 }
365
366 pub fn make_safer(&self) -> mlua::Result<()> {
370 self.lua.load(SAFER).set_name("=[retex]").exec()
371 }
372
373 pub fn set_script_args(&self, script: &str, args: &[String]) -> mlua::Result<()> {
376 let t = self.lua.create_table()?;
377 t.set(0, script)?;
378 for (i, a) in args.iter().enumerate() {
379 t.set(i as i64 + 1, a.as_str())?;
380 }
381 self.lua.globals().set("arg", t)
382 }
383}
384
385const SAFER: &str = "local function none() return nil end
386for _, f in ipairs({'execute', 'remove', 'rename'}) do os[f] = none end
387os.tempdir = none
388io.popen = none
389io.open = none
390io.tmpfile = none
391if lfs then
392 for _, f in ipairs({'mkdir', 'chdir', 'rmdir', 'touch'}) do lfs[f] = none end
393end
394if debug then debug.traceback = nil end";
395
396pub fn luac(args: &[String]) -> Result<(), String> {
401 let mut out = "texluac.out".to_owned();
402 let (mut strip, mut parse_only) = (false, false);
403 let mut files = Vec::new();
404 let mut it = args.iter();
405 while let Some(a) = it.next() {
406 match a.as_str() {
407 "-o" => out = it.next().ok_or("'-o' needs argument")?.clone(),
408 "-s" => strip = true,
409 "-p" => parse_only = true,
410 "-v" => println!("Lua 5.3.6 Copyright (C) 1994-2020 Lua.org, PUC-Rio"),
411 "-l" => return Err("listings (-l) are not supported".to_owned()),
412 "--" => files.extend(it.by_ref().cloned()),
413 s if s.starts_with('-') && s != "-" => {
414 return Err(format!("unrecognized option '{s}'"));
415 }
416 _ => files.push(a.clone()),
417 }
418 }
419 if files.is_empty() {
420 return Err("no input files given".to_owned());
421 }
422 if files.len() > 1 && !parse_only {
423 return Err("combining several files into one chunk is not supported".to_owned());
424 }
425 let lua = Lua::new();
426 let mut chunks = Vec::new();
427 for f in &files {
428 let code = if f == "-" {
429 let mut v = Vec::new();
430 std::io::Read::read_to_end(&mut std::io::stdin(), &mut v).map_err(|e| e.to_string())?;
431 v
432 } else {
433 std::fs::read(f).map_err(|e| format!("cannot open {f}: {e}"))?
434 };
435 let func = lua
436 .load(code)
437 .set_name(format!("@{f}"))
438 .into_function()
439 .map_err(|e| e.to_string())?;
440 chunks.push(func);
441 }
442 if parse_only {
443 return Ok(());
444 }
445 let Some(main) = chunks.pop() else {
446 return Ok(());
447 };
448 let bytes = main.dump(strip);
449 std::fs::write(&out, bytes).map_err(|e| format!("cannot write {out}: {e}"))
450}
451
452impl LuaTexHost {
453 fn run_chunk(
456 &self,
457 engine: &mut Engine,
458 body: impl FnOnce(&Lua) -> mlua::Result<()>,
459 ) -> Result<(), String> {
460 let lua = &self.lua;
461 if let Some(mut ps) = lua.app_data_mut::<PrintStack>() {
462 ps.0.push(Vec::new());
463 }
464 let result = self.lend(engine, body);
465 let items = lua
466 .app_data_mut::<PrintStack>()
467 .and_then(|mut ps| if ps.0.len() > 1 { ps.0.pop() } else { None })
468 .unwrap_or_default();
469 engine.push_lua_output(items);
470 result.map_err(|e| lua_error_message(&e))
471 }
472
473 fn lend<R>(
475 &self,
476 engine: &mut Engine,
477 f: impl FnOnce(&Lua) -> mlua::Result<R>,
478 ) -> mlua::Result<R> {
479 let lua = &self.lua;
480 engine.work_begin(retex_engine::profile::Work::Lua);
481 let r = lua.scope(|scope| {
482 let ud = scope.create_any_userdata_ref_mut(&mut *engine)?;
483 let slot = lua.app_data_ref::<EngineSlot>();
484 if let Some(s) = &slot {
485 s.0.borrow_mut().push(ud);
486 }
487 drop(slot);
488 let r = f(lua);
489 if let Some(s) = lua.app_data_ref::<EngineSlot>() {
490 s.0.borrow_mut().pop();
491 }
492 r
493 });
494 engine.work_end();
495 r
496 }
497}
498
499fn not_a_node(engine: &mut Engine, v: &Value) -> ! {
503 let ty = match v {
504 Value::Integer(_) => "number",
505 _ => v.type_name(),
506 };
507 engine.luatex_fatal(
508 "node lib",
509 &format!("lua <node> expected, not an object with type {ty}"),
510 )
511}
512
513fn live(engine: &Engine, p: retex_engine::nodes::NodeRef) -> retex_engine::nodes::NodeRef {
517 if engine.nodes.is_free(p) {
518 retex_engine::nodes::NodeRef::NULL
519 } else {
520 p
521 }
522}
523
524impl LuaHost for LuaTexHost {
525 fn run(&self, engine: &mut Engine, name: &str, code: &[u8]) -> Result<(), String> {
526 let chunk_name = format!("=[{name}]");
527 self.run_chunk(engine, |lua| {
528 lua.load(code).set_name(chunk_name.as_str()).exec()
529 })
530 }
531
532 fn call_function(&self, engine: &mut Engine, n: i32) -> Option<Result<(), String>> {
533 let functions = self
534 .functions
535 .get_or_init(|| self.lua.named_registry_value("retex.functions").ok())
536 .as_ref()?;
537 let Ok(Value::Function(f)) = functions.raw_get::<Value>(n) else {
538 return None;
539 };
540 if let Some(r) = self.intarrays.call(self, engine, n, &f) {
541 return Some(r);
542 }
543 Some(self.run_chunk(engine, |_| f.call::<()>(n)))
544 }
545
546 fn intarray_set(&self, gset: i32, writes: &[(i64, i64, &[i64])]) -> bool {
547 let f = self
548 .functions
549 .get_or_init(|| self.lua.named_registry_value("retex.functions").ok())
550 .as_ref()
551 .and_then(|t| t.raw_get::<mlua::Function>(gset).ok());
552 self.intarrays.set(&self.lua, gset, f.as_ref(), writes)
553 }
554
555 fn call_simple(&self, engine: &mut Engine, name: &str) -> Result<(), String> {
556 let Some(f) = callback::get(&self.lua, name) else {
557 return Ok(());
558 };
559 self.lend(engine, |_lua| f.call::<()>(()))
560 .map_err(|e| lua_error_message(&e))
561 }
562
563 fn call_with_string(&self, engine: &mut Engine, name: &str, arg: &str) -> Result<(), String> {
564 let Some(f) = callback::get(&self.lua, name) else {
565 return Ok(());
566 };
567 self.lend(engine, |_lua| f.call::<()>(arg))
568 .map_err(|e| lua_error_message(&e))
569 }
570
571 fn has_callback(&self, name: &str) -> bool {
572 if let Some((f, _)) = callback::hot(&self.lua, name) {
573 return f;
574 }
575 callback::get(&self.lua, name).is_some()
576 }
577
578 fn define_font(&self, engine: &mut Engine, name: &[u8], size: i32) -> Option<u32> {
579 let f = callback::get(&self.lua, "define_font")?;
580 let next_id = engine.fonts.fonts.len() as i64;
581 let name = self.lua.create_string(name).ok()?;
582 let r = self.lend(engine, |_lua| {
583 f.call::<Value>((name, i64::from(size), next_id))
584 });
585 match r {
586 Ok(Value::Integer(n)) if n > 0 => Some(n as u32),
587 Ok(Value::Number(n)) if n > 0.0 => Some(n as u32),
588 Ok(Value::Table(t)) => {
589 if matches!(t.raw_get::<Value>("name"), Ok(Value::Nil)) {
590 engine
591 .luatex_fatal("font", &format!("lua-loaded font '{next_id}' has no name!"));
592 }
593 let info = fonttable::font_from_table(&t).ok()?;
594 let f = engine.register_font(info);
595 engine.check_lua_loaded_font(f);
596 if let Err(e) = self.lend(engine, |lua| fonttable::attach_commands(lua, &t, f)) {
597 engine.lua_error(&lua_error_message(&e));
598 }
599 Some(f)
600 }
601 Ok(_) => None,
602 Err(e) => {
603 engine.lua_error(&lua_error_message(&e));
604 None
605 }
606 }
607 }
608
609 fn list_filter(
610 &self,
611 engine: &mut Engine,
612 callback_name: &str,
613 head: retex_engine::nodes::NodeRef,
614 group: &str,
615 ) -> retex_engine::ListFilter {
616 use retex_engine::ListFilter;
617 let Some(f) = callback::get(&self.lua, callback_name) else {
618 return ListFilter::Keep(head);
619 };
620 let r = self.lend(engine, |lua| {
621 let h = if head.is_null() {
622 Value::Nil
623 } else {
624 Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
625 };
626 f.call::<Value>((h, group))
627 });
628 let r = match r {
629 Ok(Value::Boolean(false)) => return ListFilter::Flush,
630 Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
631 Ok(n) => retex_engine::nodes::NodeRef(n.0),
632 Err(_) => not_a_node(engine, &Value::UserData(u.clone())),
634 },
635 Ok(Value::Nil) => return ListFilter::Flush,
637 Ok(Value::Boolean(true)) => head,
638 Ok(other) => not_a_node(engine, &other),
639 Err(e) => {
640 engine.lua_error(&lua_error_message(&e));
641 head
642 }
643 };
644 ListFilter::Keep(live(engine, r))
645 }
646
647 fn pack_quality(
648 &self,
649 engine: &mut Engine,
650 cb: &str,
651 incident: &str,
652 detail: i32,
653 b: retex_engine::nodes::NodeRef,
654 first: i32,
655 last: i32,
656 ) -> retex_engine::nodes::NodeRef {
657 use retex_engine::nodes::NodeRef;
658 let Some(f) = callback::get(&self.lua, cb) else {
659 return NodeRef::NULL;
660 };
661 let r = self.lend(engine, |lua| {
662 let h = Value::UserData(lua.create_userdata(nodelib::LNode(b.0))?);
663 f.call::<Value>((incident, detail, h, first, last))
664 });
665 match r {
666 Ok(Value::UserData(u)) if cb == "hpack_quality" => match u.borrow::<nodelib::LNode>() {
667 Ok(n) => live(engine, NodeRef(n.0)),
668 Err(_) => NodeRef::NULL,
669 },
670 Ok(Value::Nil) => NodeRef::NULL,
671 Ok(other) if cb == "hpack_quality" => not_a_node(engine, &other),
673 Ok(_) => NodeRef::NULL,
674 Err(e) => {
675 engine.lua_error(&lua_error_message(&e));
676 NodeRef::NULL
677 }
678 }
679 }
680
681 fn node_pass(&self, engine: &mut Engine, cb: &str, head: retex_engine::nodes::NodeRef) -> bool {
682 if callback::hot(&self.lua, cb).is_some_and(|(f, d)| !f && !d) {
683 return false;
684 }
685 let Ok(reg) = self
686 .lua
687 .named_registry_value::<mlua::Table>("retex.callbacks")
688 else {
689 return false;
690 };
691 match reg.get::<Value>(cb) {
692 Ok(Value::Boolean(false)) => true,
693 Ok(Value::Function(f)) => {
694 let first = engine.nodes.next(head);
695 if first.is_null() {
696 return true;
697 }
698 let tail = engine.nodes.tail_of(first);
699 let r = self.lend(engine, |lua| {
700 let h = Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?);
701 let t = Value::UserData(lua.create_userdata(nodelib::LNode(tail.0))?);
702 f.call::<()>((h, t))
703 });
704 if let Err(e) = r {
705 engine.lua_error(&lua_error_message(&e));
706 }
707 true
708 }
709 _ => false,
710 }
711 }
712
713 fn linebreak_filter(
714 &self,
715 engine: &mut Engine,
716 head: retex_engine::nodes::NodeRef,
717 display: bool,
718 ) -> Option<retex_engine::nodes::NodeRef> {
719 let f = callback::get(&self.lua, "linebreak_filter")?;
720 let r = self.lend(engine, |lua| {
721 let h = if head.is_null() {
722 Value::Nil
723 } else {
724 Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
725 };
726 f.call::<Value>((h, display))
727 });
728 let r = match r {
730 Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
731 Ok(n) => Some(retex_engine::nodes::NodeRef(n.0)),
732 Err(_) => None,
733 },
734 Ok(_) => None,
735 Err(e) => {
736 engine.lua_error(&lua_error_message(&e));
737 None
738 }
739 };
740 r.map(|p| live(engine, p)).filter(|p| !p.is_null())
741 }
742
743 fn mlist_to_hlist(
744 &self,
745 engine: &mut Engine,
746 head: retex_engine::nodes::NodeRef,
747 style: i32,
748 penalties: bool,
749 ) -> Option<retex_engine::nodes::NodeRef> {
750 let f = callback::get(&self.lua, "mlist_to_hlist")?;
751 let name = texlib::MATH_STYLE_NAMES
752 .get(style as usize)
753 .copied()
754 .unwrap_or("text");
755 let r = self.lend(engine, |lua| {
756 let h = if head.is_null() {
757 Value::Nil
758 } else {
759 Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
760 };
761 f.call::<Value>((h, name, penalties))
762 });
763 let r = match r {
764 Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
765 Ok(n) => Some(retex_engine::nodes::NodeRef(n.0)),
766 Err(_) => None,
767 },
768 Ok(v @ (Value::Integer(_) | Value::Number(_) | Value::String(_) | Value::Table(_))) => {
769 not_a_node(engine, &v)
770 }
771 Ok(_) => None,
772 Err(e) => {
773 engine.lua_error(&lua_error_message(&e));
774 None
775 }
776 };
777 r.map(|p| live(engine, p))
778 }
779
780 fn pipe_read(&self, engine: &mut Engine, cmd: &[u8]) -> Option<Vec<u8>> {
781 let cmd = self.lua.create_string(cmd).ok()?;
782 let f: mlua::Function = self
783 .lua
784 .load(
785 "local cmd = ...
786 local f = io.popen(cmd, 'r')
787 if not f then return nil end
788 local s = f:read('a')
789 f:close()
790 return s",
791 )
792 .set_name("=[retex]")
793 .into_function()
794 .ok()?;
795 match self.lend(engine, |_| f.call::<Value>(cmd)) {
796 Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
797 _ => None,
798 }
799 }
800
801 fn input_level_string(&self, engine: &mut Engine, n: i64) -> Option<Vec<u8>> {
802 let f = callback::get(&self.lua, "input_level_string")?;
803 let r = self.lend(engine, |_| f.call::<Value>(n));
804 match r {
805 Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
806 Ok(_) => None,
807 Err(e) => {
808 engine.lua_error(&lua_error_message(&e));
809 None
810 }
811 }
812 }
813
814 fn glyph_info(&self, engine: &mut Engine, p: retex_engine::nodes::NodeRef) -> Option<Vec<u8>> {
815 let f = callback::get(&self.lua, "glyph_info")?;
816 let r = self.lend(engine, |lua| {
817 let n = lua.create_userdata(nodelib::LNode(p.0))?;
818 f.call::<Value>(n)
819 });
820 match r {
821 Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
822 Ok(_) => None,
823 Err(e) => {
824 engine.lua_error(&lua_error_message(&e));
825 None
826 }
827 }
828 }
829
830 fn call_reference(
831 &self,
832 engine: &mut Engine,
833 reference: i32,
834 args: retex_engine::ReferenceArgs,
835 ) -> Result<(), String> {
836 use retex_engine::ReferenceArgs;
837 self.lend(engine, |lua| {
838 let v: Value = nodelib::lua_refs_synced(lua)?.raw_get(reference)?;
839 match (v, args) {
840 (Value::Function(f), ReferenceArgs::Glyph(font, chr)) => f.call::<()>((font, chr)),
841 (Value::Function(f), ReferenceArgs::Node(p)) => {
842 f.call::<()>(lua.create_userdata(nodelib::LNode(p.0))?)
843 }
844 (Value::String(code), _) => lua
845 .load(code.as_bytes().as_ref())
846 .set_name("=[vf command]")
847 .exec(),
848 _ => Ok(()),
849 }
850 })
851 .map_err(|e| lua_error_message(&e))
852 }
853
854 fn dump(&self, _engine: &mut Engine) -> Vec<u8> {
855 let mut out = retex_engine::format::Writer::default();
857 let mut entries: Vec<(i64, Vec<u8>)> = Vec::new();
858 if let Ok(lt) = self.lua.globals().get::<mlua::Table>("lua")
859 && let Ok(bc) = lt.get::<mlua::Table>("bytecode")
860 {
861 for (k, v) in bc.pairs::<i64, mlua::Function>().flatten() {
862 entries.push((k, v.dump(false)));
863 }
864 }
865 entries.sort_by_key(|(k, _)| *k);
866 out.u(entries.len() as u64);
867 for (k, b) in entries {
868 out.i(k);
869 out.bytes(&b);
870 }
871 out.buf
872 }
873
874 fn undump(&self, _engine: &mut Engine, data: &[u8]) -> Result<(), String> {
875 if data.is_empty() {
876 return Ok(());
877 }
878 let mut r = retex_engine::format::Reader::new(data);
879 let n = r.u().map_err(|e| e.to_string())?;
880 let lt: mlua::Table = self.lua.globals().get("lua").map_err(|e| e.to_string())?;
881 let bc: mlua::Table = lt.get("bytecode").map_err(|e| e.to_string())?;
882 for _ in 0..n {
883 let k = r.i().map_err(|e| e.to_string())?;
884 let b = r.bytes().map_err(|e| e.to_string())?;
885 let f = self
886 .lua
887 .load(b)
888 .set_mode(mlua::ChunkMode::Binary)
889 .into_function()
890 .map_err(|e| e.to_string())?;
891 bc.set(k, f).map_err(|e| e.to_string())?;
892 }
893 Ok(())
894 }
895}
896
897pub fn lua_error_message(e: &mlua::Error) -> String {
900 match e {
901 mlua::Error::RuntimeError(s) => luatex_traceback(s),
902 mlua::Error::SyntaxError { message: s, .. } => s.clone(),
903 mlua::Error::CallbackError { cause, .. } => lua_error_message(cause),
904 other => other.to_string(),
905 }
906}
907
908fn luatex_traceback(s: &str) -> String {
914 let Some(at) = s.find("\nstack traceback:\n") else {
915 return s.to_string();
916 };
917 let (head, tb) = s.split_at(at + "\nstack traceback:\n".len());
918 let lines: Vec<&str> = tb.split('\n').collect();
919 let mut out: Vec<String> = Vec::with_capacity(lines.len());
920 let mut i = 0;
921 if lines
924 .first()
925 .is_some_and(|l| *l == "\t[C]: in ?" || l.starts_with("\t[C]: in metamethod "))
926 {
927 i = 1;
928 }
929 while i < lines.len() {
930 let l = lines[i];
931 let next_is_wrapper = lines
932 .get(i + 1)
933 .is_some_and(|n| n.starts_with("\t[retex]:"));
934 if l == "\t[C]: in function 'error'" && next_is_wrapper {
935 let name = LAST_WRAPPED_ERROR.with(|c| c.borrow().clone());
936 out.push(match name {
937 Some(n) => format!("\t[C]: in function '{n}'"),
938 None => "\t[C]: in ?".to_string(),
939 });
940 i += 1;
941 while i < lines.len()
942 && (lines[i].starts_with("\t[retex]:") || lines[i] == "\t(...tail calls...)")
943 {
944 i += 1;
945 }
946 continue;
947 }
948 out.push(l.to_string());
949 i += 1;
950 }
951 format!("{head}{}", out.join("\n"))
952}
953
954#[cfg(test)]
955mod tests {
956 use super::*;
957
958 #[test]
959 fn arg_holds_the_command_line() {
960 let host = LuaTexHost::new().expect("host");
961 let (n, first): (i64, String) = host.lua().load("return #arg, arg[0]").eval().expect("arg");
962 assert_eq!(n as usize, std::env::args().count() - 1);
963 assert_eq!(first, std::env::args().next().unwrap());
964 }
965
966 #[test]
967 fn lpeg_is_installed() {
968 let host = LuaTexHost::new().expect("host");
969 let r: (i64, bool, String, bool) = host
970 .lua()
971 .load(
972 r#"return lpeg.match(lpeg.R"az"^1, "abc1"), package.loaded.lpeg == lpeg,
973 lpeg.version(), package.loaded.re == nil"#,
974 )
975 .eval()
976 .expect("lpeg");
977 assert_eq!(r, (4, true, "1.0.1".to_string(), true));
978 }
979}