use crate::lib_registry::LibraryModule;
use crate::lua_value::{LuaValue, UpvalueStore};
use crate::lua_vm::{LuaResult, LuaState};
pub fn create_package_lib() -> LibraryModule {
crate::lib_module!("package", {
"loadlib" => package_loadlib,
"searchpath" => package_searchpath,
})
.with_initializer(init_package_fields)
}
pub fn init_package_fields(l: &mut LuaState) -> LuaResult<()> {
let package_table = l
.get_global("package")?
.ok_or_else(|| l.error("package table not found".to_string()))?;
if !package_table.is_table() {
return Err(l.error("package must be a table".to_string()));
};
let loaded_key = l.create_string("loaded")?;
let preload_key = l.create_string("preload")?;
let path_key = l.create_string("path")?;
let cpath_key = l.create_string("cpath")?;
let config_key = l.create_string("config")?;
let searchers_key = l.create_string("searchers")?;
let loaded_table = l.create_table(0, 0)?;
let preload_table = l.create_table(0, 0)?;
let path_value = l.create_string("./?.lua;./?/init.lua")?;
let cpath_value = l.create_string("./?.so;./?.dll;./?.dylib")?;
#[cfg(windows)]
let config_str = "\\\n;\n?\n!\n-";
#[cfg(not(windows))]
let config_str = "/\n;\n?\n!\n-";
let config_value = l.create_string(config_str)?;
let searchers_table_value = l.create_table(4, 0)?;
let searchers_table = searchers_table_value.as_table_mut().unwrap();
searchers_table.raw_seti(1, LuaValue::cfunction(searcher_preload));
searchers_table.raw_seti(2, LuaValue::cfunction(searcher_lua));
searchers_table.raw_seti(3, LuaValue::cfunction(searcher_c));
searchers_table.raw_seti(4, LuaValue::cfunction(searcher_c_all_in_one));
l.raw_set(&package_table, loaded_key, loaded_table);
l.raw_set(&package_table, preload_key, preload_table);
l.raw_set(&package_table, path_key, path_value);
l.raw_set(&package_table, cpath_key, cpath_value);
l.raw_set(&package_table, config_key, config_value);
l.raw_set(&package_table, searchers_key, searchers_table_value);
let package_mod_key = l.create_string("package")?;
l.raw_set(&loaded_table, package_mod_key, package_table);
let vm = l.vm_mut();
vm.registry_set("_LOADED", loaded_table)?;
vm.registry_set("_PRELOAD", preload_table)?;
vm.registry_set("_PACKAGE", package_table)?;
Ok(())
}
fn get_package_from_registry(l: &mut LuaState) -> LuaResult<LuaValue> {
let vm = l.vm_mut();
vm.registry_get("_PACKAGE")?
.ok_or_else(|| vm.main_state().error("package table not found".to_string()))
}
fn searcher_preload(l: &mut LuaState) -> LuaResult<usize> {
let modname_val = l
.get_arg(1)
.ok_or_else(|| l.error("module name expected".to_string()))?;
let vm = l.vm_mut();
let preload_val = vm.registry_get("_PRELOAD")?.unwrap_or(LuaValue::nil());
let Some(preload_table) = preload_val.as_table_mut() else {
return Err(l.error("package.preload is not a table".to_string()));
};
let loader = preload_table
.raw_get(&modname_val)
.unwrap_or(LuaValue::nil());
if loader.is_nil() {
let modname_str = modname_val.as_str().unwrap_or("?");
let err_msg =
l.create_string(&format!("\n\tno field package.preload['{}']", modname_str))?;
l.push_value(err_msg)?;
Ok(1)
} else {
l.push_value(loader)?;
let preload_str = l.create_string(":preload:")?;
l.push_value(preload_str)?;
Ok(2)
}
}
fn searcher_lua(l: &mut LuaState) -> LuaResult<usize> {
let modname_val = l
.get_arg(1)
.ok_or_else(|| l.error("module name expected".to_string()))?;
let Some(modname) = modname_val.as_str() else {
return Err(l.error("module name expected".to_string()));
};
let package_val = get_package_from_registry(l)?;
let Some(package_table) = package_val.as_table() else {
return Err(l.error("Invalid package table".to_string()));
};
let path_key = l.create_string("path")?;
let Some(path_value) = package_table.raw_get(&path_key) else {
return Err(l.error("'package.path' must be a string".to_string()));
};
let Some(path_str) = path_value.as_str() else {
return Err(l.error("'package.path' must be a string".to_string()));
};
let dirsep = std::path::MAIN_SEPARATOR_STR;
let result = search_path(modname, path_str, ".", dirsep)?;
match result {
Some(filepath) => {
l.push_value(LuaValue::cfunction(lua_file_loader))?;
let filepath_str = l.create_string(&filepath)?;
l.push_value(filepath_str)?;
Ok(2)
}
None => {
let err = format!(
"\n\tno file '{}'",
path_str
.split(';')
.map(|template| { template.replace('?', &modname.replace('.', "/")) })
.collect::<Vec<_>>()
.join("'\n\tno file '")
);
let err_str = l.create_string(&err)?;
l.push_value(err_str)?;
Ok(1)
}
}
}
fn lua_file_loader(l: &mut LuaState) -> LuaResult<usize> {
let modname_val = l
.get_arg(1)
.ok_or_else(|| l.error("module name expected".to_string()))?;
let filepath_val = l
.get_arg(2)
.ok_or_else(|| l.error("file path expected".to_string()))?;
let Some(filepath_str) = filepath_val.as_str() else {
return Err(l.error("file path must be a string".to_string()));
};
let vm = l.vm_mut();
let proto = vm.load_proto_from_file(filepath_str)?;
let env_upvalue = vm.create_upvalue_closed(vm.global)?;
let func = vm.create_function(proto, UpvalueStore::from_single(env_upvalue))?;
l.push_value(func)?;
l.push_value(modname_val)?;
l.push_value(filepath_val)?;
let func_idx = l.get_top() - 3;
let (success, result_count) = l.pcall_stack_based(func_idx, 2)?;
if !success {
let error_val = l.stack_get(func_idx).unwrap_or_default();
let error_msg = if let Some(err) = error_val.as_str() {
err.to_string()
} else {
"error loading module".to_string()
};
return Err(l.error(format!(
"error loading module from '{}': {}",
filepath_str, error_msg
)));
}
if result_count > 0 {
Ok(1)
} else {
l.push_value(LuaValue::nil())?;
Ok(1)
}
}
fn searcher_c(l: &mut LuaState) -> LuaResult<usize> {
let modname_val = l
.get_arg(1)
.ok_or_else(|| l.error("module name expected".to_string()))?;
let Some(modname) = modname_val.as_str() else {
return Err(l.error("module name expected".to_string()));
};
let package_val = get_package_from_registry(l)?;
let Some(package_table) = package_val.as_table() else {
return Err(l.error("Invalid package table".to_string()));
};
let cpath_key = l.create_string("cpath")?;
let Some(cpath_value) = package_table.raw_get(&cpath_key) else {
return Err(l.error("'package.cpath' must be a string".to_string()));
};
let Some(cpath_str) = cpath_value.as_str() else {
return Err(l.error("'package.cpath' must be a string".to_string()));
};
let err = format!(
"\n\tno file '{}'",
cpath_str
.split(';')
.map(|template| { template.replace('?', &modname.replace('.', "/")) })
.collect::<Vec<_>>()
.join("'\n\tno file '")
);
let err_str = l.create_string(&err)?;
l.push_value(err_str)?;
Ok(1)
}
fn searcher_c_all_in_one(l: &mut LuaState) -> LuaResult<usize> {
let modname_val = l
.get_arg(1)
.ok_or_else(|| l.error("module name expected".to_string()))?;
let Some(modname) = modname_val.as_str() else {
return Err(l.error("module name expected".to_string()));
};
if !modname.contains('.') {
return Ok(0);
}
let root_modname = modname.split('.').next().unwrap_or(modname);
let package_val = get_package_from_registry(l)?;
let Some(package_table) = package_val.as_table() else {
return Err(l.error("Invalid package table".to_string()));
};
let cpath_key = l.create_string("cpath")?;
let Some(cpath_value) = package_table.raw_get(&cpath_key) else {
return Err(l.error("'package.cpath' must be a string".to_string()));
};
let Some(cpath_str) = cpath_value.as_str() else {
return Err(l.error("'package.cpath' must be a string".to_string()));
};
let err = format!(
"\n\tno file '{}'",
cpath_str
.split(';')
.map(|template| { template.replace('?', &root_modname.replace('.', "/")) })
.collect::<Vec<_>>()
.join("'\n\tno file '")
);
let err_str = l.create_string(&err)?;
l.push_value(err_str)?;
Ok(1)
}
fn search_path(name: &str, path: &str, sep: &str, rep: &str) -> LuaResult<Option<String>> {
let searchname = name.replace(sep, rep);
let templates: Vec<&str> = path.split(';').collect();
for template in templates {
let filepath = template.replace('?', &searchname);
if std::path::Path::new(&filepath).exists() {
return Ok(Some(filepath));
}
}
Ok(None)
}
fn package_loadlib(l: &mut LuaState) -> LuaResult<usize> {
let err = l.create_string("loadlib not implemented")?;
l.push_value(LuaValue::nil())?;
l.push_value(err)?;
Ok(2)
}
fn package_searchpath(l: &mut LuaState) -> LuaResult<usize> {
let name_val = l
.get_arg(1)
.ok_or_else(|| l.error("bad argument #1 to 'searchpath' (string expected)".to_string()))?;
let path_val = l
.get_arg(2)
.ok_or_else(|| l.error("bad argument #2 to 'searchpath' (string expected)".to_string()))?;
let Some(name_str) = name_val.as_str() else {
return Err(l.error("bad argument #1 to 'searchpath' (string expected)".to_string()));
};
let Some(path_str) = path_val.as_str() else {
return Err(l.error("bad argument #2 to 'searchpath' (string expected)".to_string()));
};
let sep_val = l.get_arg(3);
let sep = if let Some(sep_val) = &sep_val {
sep_val.as_str().unwrap_or(".")
} else {
"."
};
let rep_val = l.get_arg(4);
#[cfg(windows)]
let default_rep = "\\";
#[cfg(not(windows))]
let default_rep = "/";
let rep = if let Some(rep_val) = &rep_val {
rep_val.as_str().unwrap_or(default_rep)
} else {
default_rep
};
match search_path(name_str, path_str, sep, rep)? {
Some(filepath) => {
let filepath_str = l.create_string(&filepath)?;
l.push_value(filepath_str)?;
Ok(1)
}
None => {
let searchname = name_str.replace(sep, rep);
let err = format!(
"\n\tno file '{}'",
path_str
.split(';')
.map(|template| { template.replace('?', &searchname) })
.collect::<Vec<_>>()
.join("'\n\tno file '")
);
l.push_value(LuaValue::nil())?;
let err_str = l.create_string(&err)?;
l.push_value(err_str)?;
Ok(2)
}
}
}