luau-vm 0.732.0

Pure-Rust Luau virtual machine, garbage collector, and standard libraries
Documentation
use crate::VmResult;
use crate::native::{NativeCallContext, NativeCallResult, NativeFunction};
use crate::thread::{LuaStringBuilder, LuaStringBuilderStorage, Thread};

const MAX_UNICODE: u32 = 0x10ffff;
const UTF8_BUFF_SIZE: usize = 8;
const UTF8_PATTERN: &[u8] = b"[\0-\x7F\xC2-\xF4][\x80-\xBF]*";

static UTF8_FUNCS: [NativeFunction; 5] = [
    NativeFunction {
        name: "offset",
        function: byte_offset,
    },
    NativeFunction {
        name: "codepoint",
        function: codepoint,
    },
    NativeFunction {
        name: "char",
        function: utfchar,
    },
    NativeFunction {
        name: "len",
        function: utf_len,
    },
    NativeFunction {
        name: "codes",
        function: iter_codes,
    },
];

/// `iscont`
fn is_cont(byte: u8) -> bool {
    (byte & 0xc0) == 0x80
}

/// `u_posrelat`
fn u_posrelat(pos: i32, len: usize) -> i32 {
    if pos >= 0 {
        pos
    } else if (0usize).wrapping_sub(pos as usize) > len {
        0
    } else {
        len as i32 + pos + 1
    }
}

/// `utf8_decode`
fn utf8_decode(bytes: &[u8], start: usize) -> Option<(usize, i32)> {
    const LIMITS: [u32; 4] = [0xff, 0x7f, 0x7ff, 0xffff];

    let mut c = *bytes.get(start)? as u32;
    let mut result = 0u32;
    if c < 0x80 {
        return Some((start + 1, c as i32));
    }

    let mut count = 0usize;
    while (c & 0x40) != 0 {
        count += 1;
        let cc = *bytes.get(start + count)? as u32;
        if (cc & 0xc0) != 0x80 {
            return None;
        }
        result = (result << 6) | (cc & 0x3f);
        c <<= 1;
    }

    result |= (c & 0x7f) << (count * 5);
    if count > 3 || result > MAX_UNICODE || result <= LIMITS[count] {
        return None;
    }
    if (0xd800..=0xdfff).contains(&result) {
        return None;
    }

    Some((start + count + 1, result as i32))
}

/// `utflen`
fn utf_len(ctx: NativeCallContext) -> NativeCallResult {
    let thread = ctx.raw_thread();
    unsafe {
        let bytes = thread.check_string(1)?;
        let mut pos_i = u_posrelat(thread.opt_integer(2, 1)?, bytes.len());
        let mut pos_j = u_posrelat(thread.opt_integer(3, -1)?, bytes.len());

        if !(1 <= pos_i && {
            pos_i -= 1;
            pos_i as usize <= bytes.len()
        }) {
            return thread
                .lua_arg_error(2, "initial position out of string")
                .map_err(Into::into);
        }

        pos_j -= 1;
        if !bytes.is_empty() && pos_j as usize >= bytes.len() {
            return thread
                .lua_arg_error(3, "final position out of string")
                .map_err(Into::into);
        }

        let mut count = 0;
        while pos_i <= pos_j {
            let Some((next, _)) = utf8_decode(bytes, pos_i as usize) else {
                thread.push_nil()?;
                thread.push_integer(pos_i + 1)?;
                return Ok(2);
            };
            pos_i = next as i32;
            count += 1;
        }

        thread.push_integer(count)?;
        Ok(1)
    }
}

/// `codepoint`
fn codepoint(ctx: NativeCallContext) -> NativeCallResult {
    let thread = ctx.raw_thread();
    unsafe {
        let bytes = thread.check_string(1)?;
        let pos_i = u_posrelat(thread.opt_integer(2, 1)?, bytes.len());
        let pos_e = u_posrelat(thread.opt_integer(3, pos_i)?, bytes.len());

        if pos_i < 1 {
            return thread.lua_arg_error(2, "out of range").map_err(Into::into);
        }
        if pos_e as usize > bytes.len() {
            return thread.lua_arg_error(3, "out of range").map_err(Into::into);
        }
        if pos_i > pos_e {
            return Ok(0);
        }
        thread.lua_check_stack(pos_e - pos_i + 1, Some("string slice too long"))?;

        let mut count = 0;
        let mut pos = (pos_i - 1) as usize;
        let end = pos_e as usize;
        while pos < end {
            let Some((next, code)) = utf8_decode(bytes, pos) else {
                return crate::error!(thread, "invalid UTF-8 code").map_err(Into::into);
            };
            thread.push_integer(code)?;
            count += 1;
            pos = next;
        }
        Ok(count)
    }
}

/// `luaO_utf8esc`
fn lua_o_utf8esc(buffer: &mut [u8; UTF8_BUFF_SIZE], mut x: u32) -> usize {
    let mut n = 1usize;
    debug_assert!(x <= MAX_UNICODE);
    if x < 0x80 {
        buffer[UTF8_BUFF_SIZE - 1] = x as u8;
    } else {
        let mut mfb = 0x3f;
        while x > mfb {
            buffer[UTF8_BUFF_SIZE - n] = (0x80 | (x & 0x3f)) as u8;
            n += 1;
            x >>= 6;
            mfb >>= 1;
        }
        buffer[UTF8_BUFF_SIZE - n] = (((!mfb) << 1) | x) as u8;
    }
    n
}

/// `buffutfchar`
fn buff_utfchar<'a>(
    thread: &Thread,
    argument: i32,
    buffer: &'a mut [u8; UTF8_BUFF_SIZE],
) -> VmResult<&'a [u8]> {
    let code = unsafe { thread.check_integer(argument)? };
    if !(0..=MAX_UNICODE as i32).contains(&code) {
        return unsafe { thread.lua_arg_error(argument, "value out of range") }.map_err(Into::into);
    }
    let len = lua_o_utf8esc(buffer, code as u32);
    Ok(&buffer[UTF8_BUFF_SIZE - len..])
}

/// `utfchar`
fn utfchar(ctx: NativeCallContext) -> NativeCallResult {
    let thread = ctx.raw_thread();
    unsafe {
        let count = thread.get_top();
        let mut buffer = [0u8; UTF8_BUFF_SIZE];
        if count == 1 {
            let bytes = buff_utfchar(thread, 1, &mut buffer)?;
            thread.push_string(bytes)?;
        } else {
            let mut out_storage = LuaStringBuilderStorage::uninit();
            let mut out = LuaStringBuilder::new(thread, &mut out_storage);
            for index in 1..=count {
                let bytes = buff_utfchar(thread, index, &mut buffer)?;
                out.push_bytes(bytes)?;
            }
            out.finish()?;
        }
    }
    Ok(1)
}

/// `byteoffset`
fn byte_offset(ctx: NativeCallContext) -> NativeCallResult {
    let thread = ctx.raw_thread();
    unsafe {
        let bytes = thread.check_string(1)?;
        let n = thread.check_integer(2)?;
        let mut pos_i = if n >= 0 { 1 } else { bytes.len() as i32 + 1 };
        pos_i = u_posrelat(thread.opt_integer(3, pos_i)?, bytes.len());

        if !(1 <= pos_i && {
            pos_i -= 1;
            pos_i as usize <= bytes.len()
        }) {
            return thread
                .lua_arg_error(3, "position out of range")
                .map_err(Into::into);
        }

        if n == 0 {
            while pos_i > 0 && is_cont(bytes[pos_i as usize]) {
                pos_i -= 1;
            }
        } else {
            if pos_i < bytes.len() as i32 && is_cont(bytes[pos_i as usize]) {
                return crate::error!(thread, "initial position is a continuation byte")
                    .map_err(Into::into);
            }

            let mut remaining = n;
            if remaining < 0 {
                while remaining < 0 && pos_i > 0 {
                    loop {
                        pos_i -= 1;
                        if pos_i == 0 || !is_cont(bytes[pos_i as usize]) {
                            break;
                        }
                    }
                    remaining += 1;
                }
            } else {
                remaining -= 1;
                while remaining > 0 && pos_i < bytes.len() as i32 {
                    loop {
                        pos_i += 1;
                        if pos_i as usize >= bytes.len() || !is_cont(bytes[pos_i as usize]) {
                            break;
                        }
                    }
                    remaining -= 1;
                }
            }

            if remaining != 0 {
                thread.push_nil()?;
                return Ok(1);
            }
        }

        thread.push_integer(pos_i + 1)?;
        Ok(1)
    }
}

/// `iter_aux`
fn iter_aux(ctx: NativeCallContext) -> NativeCallResult {
    let thread = ctx.raw_thread();
    unsafe {
        let bytes = thread.check_string(1)?;
        let mut n = thread.to_integer(2).unwrap_or(0) - 1;
        if n < 0 {
            n = 0;
        } else if (n as usize) < bytes.len() {
            n += 1;
            while (n as usize) < bytes.len() && is_cont(bytes[n as usize]) {
                n += 1;
            }
        }

        if (n as usize) >= bytes.len() {
            return Ok(0);
        }

        let Some((next, code)) = utf8_decode(bytes, n as usize) else {
            return crate::error!(thread, "invalid UTF-8 code").map_err(Into::into);
        };
        if next < bytes.len() && is_cont(bytes[next]) {
            return crate::error!(thread, "invalid UTF-8 code").map_err(Into::into);
        }

        thread.push_integer(n + 1)?;
        thread.push_integer(code)?;
        Ok(2)
    }
}

/// `iter_codes`
fn iter_codes(ctx: NativeCallContext) -> NativeCallResult {
    let thread = ctx.raw_thread();
    let _ = unsafe { thread.check_string(1)? };
    unsafe {
        thread.push_native_closure_k(iter_aux, None, 0, None)?;
        thread.push_value(1)?;
        thread.push_integer(0)?;
    }
    Ok(3)
}

impl Thread {
    /// `luaopen_utf8`
    pub unsafe fn open_utf8(&self) -> NativeCallResult {
        unsafe {
            self.register(Some(super::LUA_UTF8LIB_NAME), &UTF8_FUNCS[..])?;
            self.push_string(UTF8_PATTERN)?;
            self.raw_set_field(-2, "charpattern")?;
        }
        Ok(1)
    }
}