use crate::VmResult;
use crate::native::{NativeCallContext, NativeCallResult};
use crate::string::MAX_STRING_SIZE;
use crate::thread::{LuaStringBuilder, LuaStringBuilderStorage, Thread};
use super::{digit, pos_relat};
const LUAL_PACK_PAD_BYTE: u8 = 0x00;
const MAX_INT_SIZE: usize = 16;
const NATIVE_ENDIAN_LITTLE: bool = cfg!(target_endian = "little");
const MAX_ALIGN: usize = 8;
#[derive(Clone, Copy, PartialEq, Eq)]
enum PackOption {
Int,
Uint,
Float,
Char,
String,
ZString,
Padding,
PaddingAlign,
Nop,
}
struct PackHeader<'thread> {
thread: &'thread Thread,
is_little: bool,
max_align: usize,
}
fn get_num(thread: &Thread, format: &[u8], index: &mut usize, default: i32) -> VmResult<i32> {
if !format.get(*index).is_some_and(|byte| digit(*byte)) {
return Ok(default);
}
let mut value = 0i32;
while format.get(*index).is_some_and(|byte| digit(*byte)) && value <= (i32::MAX - 9) / 10 {
value = value * 10 + (format[*index] - b'0') as i32;
*index += 1;
}
if value > MAX_STRING_SIZE as i32 || format.get(*index).is_some_and(|byte| digit(*byte)) {
return unsafe { crate::error!(thread, "size specifier is too large") }.map_err(Into::into);
}
Ok(value)
}
fn get_num_limit(thread: &Thread, format: &[u8], index: &mut usize, default: i32) -> VmResult<i32> {
let size = get_num(thread, format, index, default)?;
if size as usize > MAX_INT_SIZE || size <= 0 {
return unsafe {
crate::error!(
thread,
"integral size (%d) out of limits [1,%d]",
size,
MAX_INT_SIZE as i32
)
}
.map_err(Into::into);
}
Ok(size)
}
fn init_pack_header(thread: &Thread) -> PackHeader<'_> {
PackHeader {
thread,
is_little: NATIVE_ENDIAN_LITTLE,
max_align: 1,
}
}
fn get_pack_option(
header: &mut PackHeader<'_>,
format: &[u8],
index: &mut usize,
size: &mut usize,
) -> VmResult<PackOption> {
let option = format[*index];
*index += 1;
*size = 0;
let option = match option {
b'b' => {
*size = 1;
PackOption::Int
}
b'B' => {
*size = 1;
PackOption::Uint
}
b'h' => {
*size = 2;
PackOption::Int
}
b'H' => {
*size = 2;
PackOption::Uint
}
b'l' => {
*size = 8;
PackOption::Int
}
b'L' => {
*size = 8;
PackOption::Uint
}
b'j' => {
*size = 4;
PackOption::Int
}
b'J' => {
*size = 4;
PackOption::Uint
}
b'T' => {
*size = 4;
PackOption::Uint
}
b'f' => {
*size = 4;
PackOption::Float
}
b'd' | b'n' => {
*size = 8;
PackOption::Float
}
b'i' => {
*size = get_num_limit(header.thread, format, index, 4)? as usize;
PackOption::Int
}
b'I' => {
*size = get_num_limit(header.thread, format, index, 4)? as usize;
PackOption::Uint
}
b's' => {
*size = get_num_limit(header.thread, format, index, 4)? as usize;
PackOption::String
}
b'c' => {
let value = get_num(header.thread, format, index, -1)?;
if value == -1 {
return unsafe {
crate::error!(header.thread, "missing size for format option 'c'")
}
.map_err(Into::into);
}
*size = value as usize;
PackOption::Char
}
b'z' => PackOption::ZString,
b'x' => {
*size = 1;
PackOption::Padding
}
b'X' => PackOption::PaddingAlign,
b' ' => PackOption::Nop,
b'<' => {
header.is_little = true;
PackOption::Nop
}
b'>' => {
header.is_little = false;
PackOption::Nop
}
b'=' => {
header.is_little = NATIVE_ENDIAN_LITTLE;
PackOption::Nop
}
b'!' => {
header.max_align =
get_num_limit(header.thread, format, index, MAX_ALIGN as i32)? as usize;
PackOption::Nop
}
_ => {
return unsafe {
crate::error!(header.thread, "invalid format option '%c'", option as i32)
}
.map_err(Into::into);
}
};
Ok(option)
}
fn get_pack_details(
header: &mut PackHeader<'_>,
total_size: usize,
format: &[u8],
index: &mut usize,
size: &mut usize,
not_to_align: &mut usize,
) -> VmResult<PackOption> {
let option = get_pack_option(header, format, index, size)?;
let mut align = *size;
if option == PackOption::PaddingAlign {
if *index == format.len() {
return unsafe {
header
.thread
.lua_arg_error(1, "invalid next option for option 'X'")
}
.map_err(Into::into);
}
let mut next_size = 0usize;
if get_pack_option(header, format, index, &mut next_size)? == PackOption::Char
|| next_size == 0
{
return unsafe {
header
.thread
.lua_arg_error(1, "invalid next option for option 'X'")
}
.map_err(Into::into);
}
align = next_size;
}
if align <= 1 || option == PackOption::Char {
*not_to_align = 0;
} else {
if align > header.max_align {
align = header.max_align;
}
if (align & (align - 1)) != 0 {
return unsafe {
header
.thread
.lua_arg_error(1, "format asks for alignment not power of 2")
}
.map_err(Into::into);
}
*not_to_align = (align - (total_size & (align - 1))) & (align - 1);
}
Ok(option)
}
fn pack_int(
buffer: &mut LuaStringBuilder<'_, '_>,
mut value: u64,
is_little: bool,
size: usize,
negative: bool,
) -> VmResult {
let mut bytes = [0u8; MAX_INT_SIZE];
let first = if is_little { 0 } else { size - 1 };
bytes[first] = (value & 0xff) as u8;
for index in 1..size {
value >>= 8;
let byte_index = if is_little { index } else { size - 1 - index };
bytes[byte_index] = (value & 0xff) as u8;
}
if negative && size > core::mem::size_of::<i64>() {
for index in core::mem::size_of::<i64>()..size {
let byte_index = if is_little { index } else { size - 1 - index };
bytes[byte_index] = 0xff;
}
}
unsafe { buffer.push_bytes(&bytes[..size])? };
Ok(())
}
fn copy_with_endian(dest: &mut [u8], src: &[u8], is_little: bool) {
if is_little == NATIVE_ENDIAN_LITTLE {
dest.copy_from_slice(src);
} else {
for (dst, src) in dest.iter_mut().zip(src.iter().rev()) {
*dst = *src;
}
}
}
pub(super) fn string_pack(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
let format = thread.check_string(1)?;
let mut header = init_pack_header(thread);
let mut argument = 1;
let mut total_size = 0usize;
let mut index = 0usize;
let mut buffer_storage = LuaStringBuilderStorage::uninit();
let mut buffer = LuaStringBuilder::new(thread, &mut buffer_storage);
while index < format.len() {
let mut size = 0usize;
let mut not_to_align = 0usize;
let option = get_pack_details(
&mut header,
total_size,
format,
&mut index,
&mut size,
&mut not_to_align,
)?;
total_size += not_to_align + size;
for _ in 0..not_to_align {
buffer.push_byte(LUAL_PACK_PAD_BYTE)?;
}
argument += 1;
match option {
PackOption::Int => {
let value = thread.check_number(argument)? as i64;
if size < core::mem::size_of::<i64>() {
let limit = 1i64 << (size * 8 - 1);
if !(-limit <= value && value < limit) {
return thread
.lua_arg_error(argument, "integer overflow")
.map_err(Into::into);
}
}
pack_int(&mut buffer, value as u64, header.is_little, size, value < 0)?;
}
PackOption::Uint => {
let value = thread.check_number(argument)? as i64;
if size < core::mem::size_of::<i64>() && (value as u64) >= (1u64 << (size * 8))
{
return thread
.lua_arg_error(argument, "unsigned overflow")
.map_err(Into::into);
}
pack_int(&mut buffer, value as u64, header.is_little, size, false)?;
}
PackOption::Float => {
let value = thread.check_number(argument)?;
let mut bytes = [0u8; MAX_INT_SIZE];
if size == core::mem::size_of::<f32>() {
copy_with_endian(
&mut bytes[..size],
&f32::to_ne_bytes(value as f32),
header.is_little,
);
} else {
copy_with_endian(
&mut bytes[..size],
&f64::to_ne_bytes(value),
header.is_little,
);
}
buffer.push_bytes(&bytes[..size])?;
}
PackOption::Char => {
let string = thread.check_string(argument)?;
if string.len() > size {
return thread
.lua_arg_error(argument, "string longer than given size")
.map_err(Into::into);
}
buffer.push_bytes(string)?;
for _ in string.len()..size {
buffer.push_byte(LUAL_PACK_PAD_BYTE)?;
}
}
PackOption::String => {
let string = thread.check_string(argument)?;
if size < core::mem::size_of::<usize>()
&& string.len() >= (1usize << (size * 8))
{
return thread
.lua_arg_error(argument, "string length does not fit in given size")
.map_err(Into::into);
}
pack_int(
&mut buffer,
string.len() as u64,
header.is_little,
size,
false,
)?;
buffer.push_bytes(string)?;
total_size += string.len();
}
PackOption::ZString => {
let string = thread.check_string(argument)?;
if string.contains(&0) {
return thread
.lua_arg_error(argument, "string contains zeros")
.map_err(Into::into);
}
buffer.push_bytes(string)?;
buffer.push_byte(0)?;
total_size += string.len() + 1;
}
PackOption::Padding => buffer.push_byte(LUAL_PACK_PAD_BYTE)?,
PackOption::PaddingAlign | PackOption::Nop => argument -= 1,
}
}
buffer.finish()?;
Ok(1)
}
}
pub(super) fn string_pack_size(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
let format = thread.check_string(1)?;
let mut header = init_pack_header(thread);
let mut total_size = 0usize;
let mut index = 0usize;
while index < format.len() {
let mut size = 0usize;
let mut not_to_align = 0usize;
let option = get_pack_details(
&mut header,
total_size,
format,
&mut index,
&mut size,
&mut not_to_align,
)?;
if matches!(option, PackOption::String | PackOption::ZString) {
return thread
.lua_arg_error(1, "variable-length format")
.map_err(Into::into);
}
size += not_to_align;
if total_size > MAX_STRING_SIZE - size {
return thread
.lua_arg_error(1, "format result too large")
.map_err(Into::into);
}
total_size += size;
}
thread.push_integer(total_size as i32)?;
Ok(1)
}
}
fn unpack_int(
thread: &Thread,
data: &[u8],
is_little: bool,
size: usize,
signed: bool,
) -> VmResult<i64> {
let mut result = 0u64;
let limit = size.min(core::mem::size_of::<i64>());
for index in (0..limit).rev() {
result <<= 8;
let byte_index = if is_little { index } else { size - 1 - index };
result |= data[byte_index] as u64;
}
if size < core::mem::size_of::<i64>() {
if signed {
let mask = 1u64 << (size * 8 - 1);
result = (result ^ mask).wrapping_sub(mask);
}
} else if size > core::mem::size_of::<i64>() {
let mask = if !signed || result as i64 >= 0 {
0
} else {
0xff
};
for index in limit..size {
let byte_index = if is_little { index } else { size - 1 - index };
if data[byte_index] != mask {
return unsafe {
crate::error!(
thread,
"%d-byte integer does not fit into Lua Integer",
size as i32
)
}
.map_err(Into::into);
}
}
}
Ok(result as i64)
}
pub(super) fn string_unpack(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
let format = thread.check_string(1)?;
let data = thread.check_string(2)?;
let mut pos = pos_relat(thread.opt_integer(3, 1)?, data.len()) - 1;
if pos < 0 {
pos = 0;
}
if pos as usize > data.len() {
return thread
.lua_arg_error(3, "initial position out of string")
.map_err(Into::into);
}
let mut header = init_pack_header(thread);
let mut index = 0usize;
let mut results = 0i32;
while index < format.len() {
let mut size = 0usize;
let mut not_to_align = 0usize;
let option = get_pack_details(
&mut header,
pos as usize,
format,
&mut index,
&mut size,
&mut not_to_align,
)?;
if not_to_align + size > data.len() - pos as usize {
return thread
.lua_arg_error(2, "data string too short")
.map_err(Into::into);
}
pos += not_to_align as i32;
thread.lua_check_stack(2, Some("too many results"))?;
results += 1;
match option {
PackOption::Int => {
let result =
unpack_int(thread, &data[pos as usize..], header.is_little, size, true)?;
thread.push_number(result as f64)?;
}
PackOption::Uint => {
let result =
unpack_int(thread, &data[pos as usize..], header.is_little, size, false)?;
thread.push_number(result as u64 as f64)?;
}
PackOption::Float => {
let mut bytes = [0u8; MAX_INT_SIZE];
copy_with_endian(
&mut bytes[..size],
&data[pos as usize..pos as usize + size],
header.is_little,
);
let number = if size == core::mem::size_of::<f32>() {
f32::from_ne_bytes(bytes[..4].try_into().unwrap()) as f64
} else {
f64::from_ne_bytes(bytes[..8].try_into().unwrap())
};
thread.push_number(number)?;
}
PackOption::Char => {
thread.push_string(&data[pos as usize..pos as usize + size])?;
}
PackOption::String => {
let len =
unpack_int(thread, &data[pos as usize..], header.is_little, size, false)?
as usize;
if len > data.len() - pos as usize - size {
return thread
.lua_arg_error(2, "data string too short")
.map_err(Into::into);
}
thread.push_string(&data[pos as usize + size..pos as usize + size + len])?;
pos += len as i32;
}
PackOption::ZString => {
let rest = &data[pos as usize..];
let Some(len) = rest.iter().position(|&byte| byte == 0) else {
return thread
.lua_arg_error(2, "unfinished string for format 'z'")
.map_err(Into::into);
};
thread.push_string(&rest[..len])?;
pos += len as i32 + 1;
}
PackOption::PaddingAlign | PackOption::Padding | PackOption::Nop => {
results -= 1;
}
}
pos += size as i32;
}
thread.push_integer(pos + 1)?;
Ok((results + 1) as usize)
}
}