use super::gc::arena::Arena;
use super::proto::Proto;
use super::string::LuaString;
use super::value::Val;
pub const LUA_SIGNATURE: &[u8] = b"\x1bLua";
const LUAC_VERSION: u8 = 0x51;
const LUAC_FORMAT: u8 = 0;
pub const LUAC_HEADERSIZE: usize = 12;
const ENDIANNESS: u8 = 1;
const SIZEOF_INT: u8 = 4;
const SIZEOF_SIZE_T: u8 = 8;
const SIZEOF_INSTRUCTION: u8 = 4;
const SIZEOF_LUA_NUMBER: u8 = 8;
const INTEGRAL_FLAG: u8 = 0;
const LUA_TNIL: u8 = 0;
const LUA_TBOOLEAN: u8 = 1;
const LUA_TNUMBER: u8 = 3;
const LUA_TSTRING: u8 = 4;
struct DumpState<'a> {
buf: Vec<u8>,
strip: bool,
string_arena: Option<&'a Arena<LuaString>>,
}
impl<'a> DumpState<'a> {
fn new(strip: bool, string_arena: Option<&'a Arena<LuaString>>) -> Self {
Self {
buf: Vec::with_capacity(256),
strip,
string_arena,
}
}
fn dump_byte(&mut self, b: u8) {
self.buf.push(b);
}
fn dump_int(&mut self, n: i32) {
self.buf.extend_from_slice(&n.to_le_bytes());
}
fn dump_size(&mut self, n: u64) {
self.buf.extend_from_slice(&n.to_le_bytes());
}
fn dump_number(&mut self, n: f64) {
self.buf.extend_from_slice(&n.to_le_bytes());
}
fn dump_block(&mut self, data: &[u8]) {
self.buf.extend_from_slice(data);
}
fn dump_string(&mut self, s: Option<&[u8]>) {
match s {
None => self.dump_size(0),
Some(data) => {
let len = data.len() as u64;
self.dump_size(len + 1);
self.dump_block(data);
self.dump_byte(0); }
}
}
fn dump_header(&mut self) {
self.dump_block(LUA_SIGNATURE);
self.dump_byte(LUAC_VERSION);
self.dump_byte(LUAC_FORMAT);
self.dump_byte(ENDIANNESS);
self.dump_byte(SIZEOF_INT);
self.dump_byte(SIZEOF_SIZE_T);
self.dump_byte(SIZEOF_INSTRUCTION);
self.dump_byte(SIZEOF_LUA_NUMBER);
self.dump_byte(INTEGRAL_FLAG);
}
fn dump_code(&mut self, proto: &Proto) {
self.dump_int(proto.code.len() as i32);
for &instr in &proto.code {
self.buf.extend_from_slice(&instr.to_le_bytes());
}
}
fn dump_constants(&mut self, proto: &Proto, parent_source: &str) {
self.dump_int(proto.constants.len() as i32);
for (i, val) in proto.constants.iter().enumerate() {
match val {
Val::Nil => {
if let Some(bytes) = proto
.string_pool
.iter()
.find(|(idx, _)| *idx == i as u32)
.map(|(_, b)| b.as_slice())
{
self.dump_byte(LUA_TSTRING);
self.dump_string(Some(bytes));
} else {
self.dump_byte(LUA_TNIL);
}
}
Val::Bool(b) => {
self.dump_byte(LUA_TBOOLEAN);
self.dump_byte(u8::from(*b));
}
Val::Num(n) => {
self.dump_byte(LUA_TNUMBER);
self.dump_number(*n);
}
Val::Str(r) => {
self.dump_byte(LUA_TSTRING);
if let Some(arena) = self.string_arena {
if let Some(s) = arena.get(*r) {
self.dump_string(Some(s.data()));
} else {
self.dump_string(Some(b""));
}
} else {
if let Some(bytes) = proto
.string_pool
.iter()
.find(|(idx, _)| *idx == i as u32)
.map(|(_, b)| b.as_slice())
{
self.dump_string(Some(bytes));
} else {
self.dump_string(Some(b""));
}
}
}
_ => self.dump_byte(LUA_TNIL),
}
}
self.dump_int(proto.protos.len() as i32);
for child in &proto.protos {
self.dump_function(child, parent_source);
}
}
fn dump_debug(&mut self, proto: &Proto) {
if self.strip {
self.dump_int(0); self.dump_int(0); self.dump_int(0); } else {
self.dump_int(proto.line_info.len() as i32);
for &line in &proto.line_info {
self.dump_int(line as i32);
}
self.dump_int(proto.local_vars.len() as i32);
for var in &proto.local_vars {
self.dump_string(Some(var.name.as_bytes()));
self.dump_int(var.start_pc as i32);
self.dump_int(var.end_pc as i32);
}
self.dump_int(proto.upvalue_names.len() as i32);
for name in &proto.upvalue_names {
self.dump_string(Some(name.as_bytes()));
}
}
}
fn dump_function(&mut self, proto: &Proto, parent_source: &str) {
if proto.source == parent_source {
self.dump_string(None);
} else {
self.dump_string(Some(proto.source.as_bytes()));
}
self.dump_int(proto.line_defined as i32);
self.dump_int(proto.last_line_defined as i32);
self.dump_byte(proto.num_upvalues);
self.dump_byte(proto.num_params);
self.dump_byte(proto.is_vararg);
self.dump_byte(proto.max_stack_size);
self.dump_code(proto);
self.dump_constants(proto, &proto.source);
self.dump_debug(proto);
}
}
pub fn dump(proto: &Proto, string_arena: Option<&Arena<LuaString>>, strip: bool) -> Vec<u8> {
let mut d = DumpState::new(strip, string_arena);
d.dump_header();
d.dump_function(proto, "");
d.buf
}
#[must_use]
pub fn make_header() -> [u8; LUAC_HEADERSIZE] {
let mut h = [0u8; LUAC_HEADERSIZE];
h[0..4].copy_from_slice(LUA_SIGNATURE);
h[4] = LUAC_VERSION;
h[5] = LUAC_FORMAT;
h[6] = ENDIANNESS;
h[7] = SIZEOF_INT;
h[8] = SIZEOF_SIZE_T;
h[9] = SIZEOF_INSTRUCTION;
h[10] = SIZEOF_LUA_NUMBER;
h[11] = INTEGRAL_FLAG;
h
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::approx_constant
)]
mod tests {
use super::*;
use crate::vm::instructions::{Instruction, OpCode};
#[test]
fn dump_header_format() {
let proto = Proto::new("test");
let bytes = dump(&proto, None, false);
assert!(bytes.len() >= LUAC_HEADERSIZE);
assert_eq!(&bytes[0..4], LUA_SIGNATURE);
assert_eq!(bytes[4], LUAC_VERSION);
assert_eq!(bytes[5], LUAC_FORMAT);
assert_eq!(bytes[6], ENDIANNESS);
assert_eq!(bytes[7], SIZEOF_INT);
assert_eq!(bytes[8], SIZEOF_SIZE_T);
assert_eq!(bytes[9], SIZEOF_INSTRUCTION);
assert_eq!(bytes[10], SIZEOF_LUA_NUMBER);
assert_eq!(bytes[11], INTEGRAL_FLAG);
}
#[test]
fn dump_empty_function() {
let mut proto = Proto::new("=test");
proto
.code
.push(Instruction::abc(OpCode::Return, 0, 1, 0).raw());
proto.line_info.push(0);
let bytes = dump(&proto, None, false);
assert!(bytes.len() > LUAC_HEADERSIZE);
}
#[test]
fn dump_with_constants() {
let mut proto = Proto::new("=test");
proto.constants.push(Val::Nil);
proto.constants.push(Val::Bool(true));
proto.constants.push(Val::Bool(false));
proto.constants.push(Val::Num(3.14));
proto.constants.push(Val::Nil); proto.string_pool.push((4, b"hello".to_vec()));
proto
.code
.push(Instruction::abc(OpCode::Return, 0, 1, 0).raw());
proto.line_info.push(0);
let bytes = dump(&proto, None, false);
assert!(bytes.windows(5).any(|w| w == b"hello"));
}
#[test]
fn dump_with_nested_protos() {
let mut inner = Proto::new("=test");
inner
.code
.push(Instruction::abc(OpCode::Return, 0, 1, 0).raw());
inner.line_info.push(0);
let mut outer = Proto::new("=test");
outer.protos.push(std::rc::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);
assert!(bytes.len() > LUAC_HEADERSIZE + 20);
}
#[test]
fn dump_stripped_no_debug() {
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(crate::vm::proto::LocalVar {
name: "x".into(),
start_pc: 0,
end_pc: 1,
});
proto.upvalue_names.push("_ENV".into());
let stripped = dump(&proto, None, true);
let full = dump(&proto, None, false);
assert!(stripped.len() < full.len());
}
#[test]
fn make_header_matches_dump() {
let h = make_header();
let proto = Proto::new("test");
let bytes = dump(&proto, None, false);
assert_eq!(&bytes[..LUAC_HEADERSIZE], &h[..]);
}
#[test]
fn dump_source_elision() {
let mut inner = Proto::new("=test");
inner
.code
.push(Instruction::abc(OpCode::Return, 0, 1, 0).raw());
inner.line_info.push(0);
let mut outer = Proto::new("=test");
outer.protos.push(std::rc::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 test_bytes = b"=test";
let count = bytes
.windows(test_bytes.len())
.filter(|w| *w == test_bytes)
.count();
assert_eq!(count, 1, "inner source should be elided");
}
}