use mlua::{Lua, Table};
use yo_common::{Code, Error, Result};
pub(in crate::dispatch) fn statics(lua: &Lua, raw: &Table) -> mlua::Result<()> {
raw.raw_set(
"named",
lua.create_function(|_, name: mlua::LuaString| Ok(named(&name.as_bytes())))?,
)
}
pub(in crate::dispatch) const FLAGS: [&str; 5] = [
"no-writes",
"allow-oom",
"allow-stale",
"no-cluster",
"allow-cross-slot-keys",
];
pub(in crate::dispatch) const NO_WRITES: u32 = 1;
pub(in crate::dispatch) const ENGINE: &str = "LUA";
pub(in crate::dispatch) struct Func {
pub name: Box<str>,
pub desc: Option<Box<[u8]>>,
pub flags: u32,
}
pub(in crate::dispatch) struct Library {
pub name: Box<str>,
pub code: Box<[u8]>,
pub at: usize,
pub sha: [u8; 40],
pub funcs: Vec<Func>,
}
#[derive(Default)]
pub(in crate::dispatch) struct Libraries {
held: Vec<Library>,
}
impl Libraries {
pub(in crate::dispatch) fn library(&self, name: &[u8]) -> Option<&Library> {
self.held.iter().find(|l| l.name.as_bytes() == name)
}
pub(in crate::dispatch) fn function(&self, name: &[u8]) -> Option<(&Library, &Func)> {
for lib in &self.held {
for f in &lib.funcs {
if f.name.as_bytes().eq_ignore_ascii_case(name) {
return Some((lib, f));
}
}
}
None
}
pub(in crate::dispatch) fn taken(&self, name: &str, except: &str) -> bool {
self.held.iter().any(|lib| {
&*lib.name != except && lib.funcs.iter().any(|f| f.name.eq_ignore_ascii_case(name))
})
}
pub(in crate::dispatch) fn insert(&mut self, lib: Library) {
self.remove(lib.name.as_bytes());
self.held.push(lib);
}
pub(in crate::dispatch) fn remove(&mut self, name: &[u8]) -> bool {
let before = self.held.len();
self.held.retain(|l| l.name.as_bytes() != name);
self.held.len() != before
}
pub(in crate::dispatch) fn wipe(&mut self) {
self.held.clear();
}
pub(in crate::dispatch) fn counts(&self) -> (usize, usize) {
(
self.held.len(),
self.held.iter().map(|l| l.funcs.len()).sum(),
)
}
pub(in crate::dispatch) fn all(&self) -> &[Library] {
&self.held
}
pub(in crate::dispatch) fn join(&mut self, other: Self, replace: bool) -> Result<()> {
let mut dropped: Vec<&str> = Vec::new();
for lib in &other.held {
if self.library(lib.name.as_bytes()).is_some() {
if !replace {
return Err(Error::fmt(
Code::Unsupported,
format_args!("Library {} already exists", lib.name),
));
}
dropped.push(&lib.name);
}
}
for lib in &other.held {
for f in &lib.funcs {
let clash = self.held.iter().any(|held| {
!dropped.contains(&&*held.name)
&& held
.funcs
.iter()
.any(|g| g.name.eq_ignore_ascii_case(&f.name))
});
if clash {
return Err(Error::fmt(
Code::Unsupported,
format_args!("Function {} already exists", f.name),
));
}
}
}
for lib in other.held {
self.insert(lib);
}
Ok(())
}
}
#[derive(Debug)]
pub(in crate::dispatch) struct Meta<'a> {
pub engine: Vec<u8>,
pub name: Vec<u8>,
pub body: &'a [u8],
}
pub(in crate::dispatch) fn metadata(code: &[u8]) -> Result<Meta<'_>> {
if !code.starts_with(b"#!") {
return Err(Error::new(Code::Invalid, "Missing library metadata"));
}
let Some(nl) = code.iter().position(|&b| b == b'\n') else {
return Err(Error::new(Code::Invalid, "Invalid library metadata"));
};
let Some(parts) = split(&code[..nl]) else {
return Err(Error::new(Code::Invalid, "Invalid library metadata"));
};
let Some(first) = parts.first() else {
return Err(Error::new(Code::Invalid, "Invalid library metadata"));
};
let engine = first[2.min(first.len())..].to_vec();
let mut name: Option<Vec<u8>> = None;
for part in &parts[1..] {
if part.len() >= 5 && part[..5].eq_ignore_ascii_case(b"name=") {
if name.is_some() {
return Err(Error::new(
Code::Invalid,
"Invalid metadata value, name argument was given multiple times",
));
}
name = Some(part[5..].to_vec());
continue;
}
return Err(Error::fmt(
Code::Invalid,
format_args!(
"Invalid metadata value given: {}",
String::from_utf8_lossy(part)
),
));
}
let Some(name) = name else {
return Err(Error::new(Code::Invalid, "Library name was not given"));
};
Ok(Meta {
engine,
name,
body: &code[nl..],
})
}
pub(in crate::dispatch) fn named(name: &[u8]) -> bool {
!name.is_empty() && name.iter().all(|&b| b.is_ascii_alphanumeric() || b == b'_')
}
pub(in crate::dispatch) fn bad_name() -> Error {
Error::new(
Code::Invalid,
"Library names can only contain letters, numbers, or underscores(_) \
and must be at least one character long",
)
}
fn split(line: &[u8]) -> Option<Vec<Vec<u8>>> {
let mut out: Vec<Vec<u8>> = Vec::new();
let mut i = 0;
loop {
while i < line.len() && line[i].is_ascii_whitespace() {
i += 1;
}
if i >= line.len() {
return Some(out);
}
let mut word = Vec::new();
let mut in_double = false;
let mut in_single = false;
let mut done = false;
while !done {
let c = line.get(i).copied();
if in_double {
match c {
Some(b'\\')
if i + 3 < line.len()
&& line[i + 1] == b'x'
&& hex(line[i + 2]).is_some()
&& hex(line[i + 3]).is_some() =>
{
let hi = hex(line[i + 2])?;
let lo = hex(line[i + 3])?;
word.push(hi * 16 + lo);
i += 3;
}
Some(b'\\') if i + 1 < line.len() => {
i += 1;
word.push(match line[i] {
b'n' => b'\n',
b'r' => b'\r',
b't' => b'\t',
b'b' => 0x08,
b'a' => 0x07,
other => other,
});
}
Some(b'"') => {
if line.get(i + 1).is_some_and(|b| !b.is_ascii_whitespace()) {
return None;
}
done = true;
}
None => return None,
Some(ch) => word.push(ch),
}
} else if in_single {
match c {
Some(b'\\') if line.get(i + 1) == Some(&b'\'') => {
i += 1;
word.push(b'\'');
}
Some(b'\'') => {
if line.get(i + 1).is_some_and(|b| !b.is_ascii_whitespace()) {
return None;
}
done = true;
}
None => return None,
Some(ch) => word.push(ch),
}
} else {
match c {
None => done = true,
Some(ch) if ch.is_ascii_whitespace() => done = true,
Some(b'"') => in_double = true,
Some(b'\'') => in_single = true,
Some(ch) => word.push(ch),
}
}
if i < line.len() {
i += 1;
}
}
out.push(word);
}
}
fn hex(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_shebang_says_the_engine_and_the_name() {
let md = metadata(b"#!lua name=x\nreturn 1").expect("read");
assert_eq!(md.engine, b"lua");
assert_eq!(md.name, b"x");
assert_eq!(md.body, b"\nreturn 1");
assert_eq!(metadata(b"#!lua name=\"q\"\nx").expect("read").name, b"q");
assert_eq!(metadata(b"#!LUA name=x\nx").expect("read").engine, b"LUA");
}
#[test]
fn a_shebang_that_is_not_one_says_which_way_it_is_wrong() {
let why = |code: &[u8]| metadata(code).expect_err("refused").to_string();
assert!(why(b"return 1").contains("Missing library metadata"));
assert!(why(b"#!lua name=x").contains("Invalid library metadata"));
assert!(why(b"#!lua name=\"q\nx").contains("Invalid library metadata"));
assert!(why(b"#!lua\nx").contains("Library name was not given"));
assert!(why(b"#!\n").contains("Library name was not given"));
assert!(why(b"#!lua name=a name=b\nx").contains("name argument was given multiple times"));
assert!(why(b"#!lua nome=a\nx").contains("Invalid metadata value given: nome=a"));
}
#[test]
fn a_name_is_letters_numbers_and_underscores() {
assert!(named(b"a"));
assert!(named(b"A_1"));
assert!(named(b"12"));
assert!(!named(b""));
assert!(!named(b"a-b"));
assert!(!named(b"a b"));
assert!(!named("é".as_bytes()));
}
#[test]
fn a_library_is_found_by_case_and_a_function_is_not() {
let mut held = Libraries::default();
held.insert(Library {
name: "mylib".into(),
code: b"#!lua name=mylib\n".to_vec().into_boxed_slice(),
at: 16,
sha: [b'0'; 40],
funcs: vec![
Func {
name: "ping".into(),
desc: Some(b"says pong".to_vec().into_boxed_slice()),
flags: NO_WRITES,
},
Func {
name: "PING".into(),
desc: None,
flags: 0,
},
],
});
assert!(held.library(b"mylib").is_some());
assert!(held.library(b"MYLIB").is_none());
let (lib, f) = held.function(b"PiNg").expect("found");
assert_eq!(&*lib.name, "mylib");
assert_eq!(&*f.name, "ping");
assert_eq!(held.counts(), (1, 2));
assert!(held.taken("PING", "other"));
assert!(!held.taken("PING", "mylib"));
assert!(!held.remove(b"MYLIB"));
assert!(held.remove(b"mylib"));
assert_eq!(held.counts(), (0, 0));
}
}