use std::rc::Rc;
use crate::error::{LuaError, LuaResult, SyntaxError};
use super::dump::{LUA_SIGNATURE, LUAC_HEADERSIZE, make_header};
use super::proto::{LocalVar, Proto};
use super::value::Val;
const LUA_TNIL: u8 = 0;
const LUA_TBOOLEAN: u8 = 1;
const LUA_TNUMBER: u8 = 3;
const LUA_TSTRING: u8 = 4;
struct LoadState<'a> {
data: &'a [u8],
pos: usize,
name: String,
}
impl<'a> LoadState<'a> {
fn new(data: &'a [u8], name: &str) -> Self {
Self {
data,
pos: 0,
name: name.to_string(),
}
}
fn error(&self, reason: &str) -> LuaError {
LuaError::Syntax(SyntaxError {
message: format!("{}: {} in precompiled chunk", self.name, reason),
source: self.name.clone(),
line: 0,
raw_message: None,
})
}
fn load_byte(&mut self) -> LuaResult<u8> {
if self.pos >= self.data.len() {
return Err(self.error("truncated"));
}
let b = self.data[self.pos];
self.pos += 1;
Ok(b)
}
fn load_block(&mut self, n: usize) -> LuaResult<&'a [u8]> {
if self.pos + n > self.data.len() {
return Err(self.error("truncated"));
}
let slice = &self.data[self.pos..self.pos + n];
self.pos += n;
Ok(slice)
}
fn load_int(&mut self) -> LuaResult<i32> {
let bytes = self.load_block(4)?;
Ok(i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn load_size(&mut self) -> LuaResult<u64> {
let bytes = self.load_block(8)?;
Ok(u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]))
}
fn load_number(&mut self) -> LuaResult<f64> {
let bytes = self.load_block(8)?;
Ok(f64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]))
}
fn load_string(&mut self) -> LuaResult<Option<Vec<u8>>> {
let size = self.load_size()?;
if size == 0 {
return Ok(None);
}
let data_len = (size - 1) as usize;
let data = self.load_block(data_len)?.to_vec();
let _null = self.load_byte()?;
Ok(Some(data))
}
fn load_header(&mut self) -> LuaResult<()> {
let sig = self.load_block(LUA_SIGNATURE.len())?;
if sig != LUA_SIGNATURE {
return Err(self.error("not a precompiled chunk"));
}
let expected = make_header();
let rest = self.load_block(LUAC_HEADERSIZE - LUA_SIGNATURE.len())?;
if rest != &expected[LUA_SIGNATURE.len()..] {
return Err(self.error("incompatible precompiled chunk"));
}
Ok(())
}
fn load_code(&mut self, proto: &mut Proto) -> LuaResult<()> {
let n = self.load_int()? as usize;
proto.code.reserve(n);
for _ in 0..n {
let bytes = self.load_block(4)?;
proto
.code
.push(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]));
}
Ok(())
}
fn load_constants(&mut self, proto: &mut Proto, parent_source: &str) -> LuaResult<()> {
let n = self.load_int()? as usize;
proto.constants.reserve(n);
for i in 0..n {
let tag = self.load_byte()?;
match tag {
LUA_TNIL => {
proto.constants.push(Val::Nil);
}
LUA_TBOOLEAN => {
let b = self.load_byte()?;
proto.constants.push(Val::Bool(b != 0));
}
LUA_TNUMBER => {
let num = self.load_number()?;
proto.constants.push(Val::Num(num));
}
LUA_TSTRING => {
let bytes = self.load_string()?.unwrap_or_default();
proto.constants.push(Val::Nil);
proto.string_pool.push((i as u32, bytes));
}
_ => {
return Err(self.error("bad constant type"));
}
}
}
let np = self.load_int()? as usize;
proto.protos.reserve(np);
for _ in 0..np {
let child = self.load_function(parent_source)?;
proto.protos.push(child);
}
Ok(())
}
fn load_debug(&mut self, proto: &mut Proto) -> LuaResult<()> {
let n = self.load_int()? as usize;
proto.line_info.reserve(n);
for _ in 0..n {
proto.line_info.push(self.load_int()? as u32);
}
let n = self.load_int()? as usize;
proto.local_vars.reserve(n);
for _ in 0..n {
let name = self.load_string()?.unwrap_or_default();
let name = String::from_utf8_lossy(&name).into_owned();
let start_pc = self.load_int()? as u32;
let end_pc = self.load_int()? as u32;
proto.local_vars.push(LocalVar {
name,
start_pc,
end_pc,
});
}
let n = self.load_int()? as usize;
proto.upvalue_names.reserve(n);
for _ in 0..n {
let name = self.load_string()?.unwrap_or_default();
proto
.upvalue_names
.push(String::from_utf8_lossy(&name).into_owned());
}
Ok(())
}
fn load_function(&mut self, parent_source: &str) -> LuaResult<Rc<Proto>> {
let source = match self.load_string()? {
Some(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
None => parent_source.to_string(),
};
let mut proto = Proto::new(&source);
proto.line_defined = self.load_int()? as u32;
proto.last_line_defined = self.load_int()? as u32;
proto.num_upvalues = self.load_byte()?;
proto.num_params = self.load_byte()?;
proto.is_vararg = self.load_byte()?;
proto.max_stack_size = self.load_byte()?;
self.load_code(&mut proto)?;
self.load_constants(&mut proto, &source)?;
self.load_debug(&mut proto)?;
Ok(Rc::new(proto))
}
}
pub fn undump(data: &[u8], name: &str) -> LuaResult<Rc<Proto>> {
let mut s = LoadState::new(data, name);
s.load_header()?;
s.load_function(name)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::vm::dump::dump;
use crate::vm::instructions::{Instruction, OpCode};
#[test]
fn undump_valid_header() {
let proto = Proto::new("=test");
let bytes = dump(&proto, None, false);
let result = undump(&bytes, "=test");
assert!(result.is_ok());
}
#[test]
fn undump_bad_signature() {
let data = b"not a lua chunk at all";
let result = undump(data, "=test");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("not a precompiled chunk"),
"unexpected error: {err}"
);
}
#[test]
fn undump_bad_version() {
let mut data = vec![0x1b, b'L', b'u', b'a'];
data.push(0x52); data.extend_from_slice(&[0; 20]); let result = undump(&data, "=test");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("incompatible"), "unexpected error: {err}");
}
#[test]
fn undump_truncated() {
let data = b"\x1bLua"; let result = undump(data, "=test");
assert!(result.is_err());
}
#[test]
fn round_trip_simple() {
let mut proto = Proto::new("=test");
proto.line_defined = 0;
proto.last_line_defined = 0;
proto.num_upvalues = 0;
proto.num_params = 0;
proto.is_vararg = 2;
proto.max_stack_size = 2;
proto
.code
.push(Instruction::abc(OpCode::Return, 0, 1, 0).raw());
proto.line_info.push(1);
let bytes = dump(&proto, None, false);
let loaded = undump(&bytes, "=test").expect("undump should succeed");
assert_eq!(loaded.source, proto.source);
assert_eq!(loaded.line_defined, proto.line_defined);
assert_eq!(loaded.last_line_defined, proto.last_line_defined);
assert_eq!(loaded.num_upvalues, proto.num_upvalues);
assert_eq!(loaded.num_params, proto.num_params);
assert_eq!(loaded.is_vararg, proto.is_vararg);
assert_eq!(loaded.max_stack_size, proto.max_stack_size);
assert_eq!(loaded.code, proto.code);
assert_eq!(loaded.line_info, proto.line_info);
}
#[test]
fn round_trip_with_strings() {
let mut proto = Proto::new("=test");
proto.constants.push(Val::Num(42.0));
proto.constants.push(Val::Nil); proto.string_pool.push((1, b"hello".to_vec()));
proto.constants.push(Val::Bool(true));
proto
.code
.push(Instruction::abc(OpCode::Return, 0, 1, 0).raw());
proto.line_info.push(0);
let bytes = dump(&proto, None, false);
let loaded = undump(&bytes, "=test").expect("undump should succeed");
assert_eq!(loaded.constants[0], Val::Num(42.0));
assert_eq!(loaded.string_pool.len(), 1);
assert_eq!(loaded.string_pool[0].0, 1);
assert_eq!(loaded.string_pool[0].1, b"hello");
assert_eq!(loaded.constants[2], Val::Bool(true));
}
#[test]
fn round_trip_with_nested() {
let mut inner = Proto::new("=test");
inner
.code
.push(Instruction::abc(OpCode::Return, 0, 1, 0).raw());
inner.line_info.push(0);
inner.line_defined = 5;
inner.last_line_defined = 10;
let mut outer = Proto::new("=test");
outer.protos.push(Rc::new(inner));
outer
.code
.push(Instruction::abc(OpCode::Return, 0, 1, 0).raw());
outer.line_info.push(0);
let bytes = dump(&outer, None, false);
let loaded = undump(&bytes, "=test").expect("undump should succeed");
assert_eq!(loaded.protos.len(), 1);
assert_eq!(loaded.protos[0].line_defined, 5);
assert_eq!(loaded.protos[0].last_line_defined, 10);
assert_eq!(loaded.protos[0].source, "=test");
}
#[test]
fn round_trip_stripped() {
let mut proto = Proto::new("=test");
proto
.code
.push(Instruction::abc(OpCode::Return, 0, 1, 0).raw());
proto.line_info.push(1);
proto.local_vars.push(LocalVar {
name: "x".into(),
start_pc: 0,
end_pc: 1,
});
proto.upvalue_names.push("_ENV".into());
let bytes = dump(&proto, None, true);
let loaded = undump(&bytes, "=test").expect("undump should succeed");
assert_eq!(loaded.code, proto.code);
assert!(loaded.line_info.is_empty());
assert!(loaded.local_vars.is_empty());
assert!(loaded.upvalue_names.is_empty());
}
#[test]
fn round_trip_debug_info() {
let mut proto = Proto::new("=test");
proto
.code
.push(Instruction::abc(OpCode::Return, 0, 1, 0).raw());
proto.line_info.push(42);
proto.local_vars.push(LocalVar {
name: "myvar".into(),
start_pc: 0,
end_pc: 5,
});
proto.upvalue_names.push("upval1".into());
let bytes = dump(&proto, None, false);
let loaded = undump(&bytes, "=test").expect("undump should succeed");
assert_eq!(loaded.line_info, vec![42]);
assert_eq!(loaded.local_vars.len(), 1);
assert_eq!(loaded.local_vars[0].name, "myvar");
assert_eq!(loaded.local_vars[0].start_pc, 0);
assert_eq!(loaded.local_vars[0].end_pc, 5);
assert_eq!(loaded.upvalue_names, vec!["upval1"]);
}
}