- Препроцессор GMod: continue внутри for/while/repeat -> goto + уникальная метка; учтены строки/комментарии, вложенные циклы и функции, завершающие break/return (оборачиваются в do ... end). widget_bones.lua грузится: энтити 6 из 6. - include(): поиск рядом с текущим файлом и в lua/ игры, тот же препроцессор, лимиты размера/глубины, защита от циклов и выхода за каталог игры. - LuaRuntime::load_gamemode(): init.lua и cl_init.lua, сбор ошибок без падения. Sandbox init.lua выполняется, cl_init.lua упирается в VGUI (AccessorFunc); реестры list/scripted_ents после загрузки целы. - Безопасные заглушки загрузочного слоя Sandbox: CLIENT/SERVER, is*, FindMetaTable, cleanup, drive, duplicator, net, cvars, player_manager. - Тесты: юнит-тесты препроцессора, include на временных файлах и реальных lua/includes, загрузка sandbox на ассетах.
196 lines
6.3 KiB
Rust
196 lines
6.3 KiB
Rust
//! Настоящий `include()`: поиск в `lua/`, препроцессор, циклы и песочница.
|
||
|
||
use glua::LuaRuntime;
|
||
use std::path::{Path, PathBuf};
|
||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||
|
||
static NEXT_DIR: AtomicUsize = AtomicUsize::new(0);
|
||
|
||
/// Уникальный временный каталог игры для одного теста.
|
||
struct TempGame {
|
||
root: PathBuf,
|
||
}
|
||
|
||
impl TempGame {
|
||
fn new(name: &str) -> Self {
|
||
let root = std::env::temp_dir().join(format!(
|
||
"glua_include_{name}_{}_{}",
|
||
std::process::id(),
|
||
NEXT_DIR.fetch_add(1, Ordering::Relaxed)
|
||
));
|
||
let _ = std::fs::remove_dir_all(&root);
|
||
std::fs::create_dir_all(root.join("lua")).expect("каталог игры");
|
||
Self { root }
|
||
}
|
||
|
||
fn write(&self, relative: &str, contents: &str) {
|
||
let path = self.root.join(relative);
|
||
if let Some(parent) = path.parent() {
|
||
std::fs::create_dir_all(parent).expect("каталог скрипта");
|
||
}
|
||
std::fs::write(path, contents).expect("файл скрипта");
|
||
}
|
||
|
||
fn path(&self) -> &Path {
|
||
&self.root
|
||
}
|
||
}
|
||
|
||
impl Drop for TempGame {
|
||
fn drop(&mut self) {
|
||
let _ = std::fs::remove_dir_all(&self.root);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn include_preprocesses_and_does_not_repeat_files() {
|
||
let game = TempGame::new("basic");
|
||
game.write("lua/inc/counter.lua", "counter = ( counter or 0 ) + 1\n");
|
||
game.write(
|
||
"lua/inc/syntax.lua",
|
||
r#"
|
||
local out = {}
|
||
for i = 1, 3 do
|
||
if ( i == 2 ) then continue end
|
||
out[ #out + 1 ] = i
|
||
end
|
||
continued = table.concat( out, "" )
|
||
if ( !flag ) then not_flag = true end
|
||
"#,
|
||
);
|
||
game.write(
|
||
"lua/inc/cycle_a.lua",
|
||
"cycle_a = ( cycle_a or 0 ) + 1\ninclude( \"inc/cycle_b.lua\" )\n",
|
||
);
|
||
game.write(
|
||
"lua/inc/cycle_b.lua",
|
||
"cycle_b = ( cycle_b or 0 ) + 1\ninclude( \"inc/cycle_a.lua\" )\n",
|
||
);
|
||
|
||
let rt = LuaRuntime::new().expect("рантайм");
|
||
rt.set_file_root(game.path()).expect("корень");
|
||
rt.run(
|
||
"include_test.lua",
|
||
br#"
|
||
flag = false
|
||
include( "inc/counter.lua" )
|
||
include( "inc/counter.lua" )
|
||
include( "inc/syntax.lua" )
|
||
include( "inc/cycle_a.lua" )
|
||
"#,
|
||
)
|
||
.expect("include выполнился");
|
||
|
||
assert_eq!(
|
||
rt.probe("return tostring(counter)").expect("probe"),
|
||
Some("1".into())
|
||
);
|
||
// Препроцессор тот же: continue превратился в goto, `!` — в not.
|
||
assert_eq!(
|
||
rt.probe("return continued").expect("probe"),
|
||
Some("13".into())
|
||
);
|
||
assert_eq!(
|
||
rt.probe("return tostring(not_flag)").expect("probe"),
|
||
Some("true".into())
|
||
);
|
||
// Цикл a -> b -> a не зациклился и не выполнил a второй раз.
|
||
assert_eq!(
|
||
rt.probe("return tostring(cycle_a) .. tostring(cycle_b)")
|
||
.expect("probe"),
|
||
Some("11".into())
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn include_refuses_escape_and_oversized_files() {
|
||
let game = TempGame::new("sandbox");
|
||
game.write("lua/ok.lua", "ok = true\n");
|
||
let secret_name = format!(
|
||
"glua_include_secret_{}_{}.lua",
|
||
std::process::id(),
|
||
NEXT_DIR.fetch_add(1, Ordering::Relaxed)
|
||
);
|
||
let outside = game.path().parent().expect("родитель").join(&secret_name);
|
||
std::fs::write(&outside, "leaked = true\n").expect("файл вне корня");
|
||
|
||
let rt = LuaRuntime::new().expect("рантайм");
|
||
rt.set_file_root(game.path()).expect("корень");
|
||
rt.run("ok.lua", b"include( \"ok.lua\" )")
|
||
.expect("обычный include");
|
||
|
||
let script = format!("include( \"../../{secret_name}\" )");
|
||
let escape = rt.run("escape.lua", script.as_bytes());
|
||
assert!(escape.is_err(), "выход за пределы каталога игры запрещён");
|
||
|
||
game.write(
|
||
"lua/big.lua",
|
||
&"-- большой файл\n".repeat((1 << 20) / 16 + 1),
|
||
);
|
||
let big = rt.run("big.lua", b"include( \"big.lua\" )");
|
||
assert!(big.is_err(), "файл больше лимита должен быть отклонён");
|
||
let _ = std::fs::remove_file(outside);
|
||
}
|
||
|
||
#[test]
|
||
fn load_gamemode_collects_errors_and_continues() {
|
||
let game = TempGame::new("gamemode");
|
||
game.write(
|
||
"gamemodes/test/gamemode/init.lua",
|
||
"error( \"init упал\" )\n",
|
||
);
|
||
game.write(
|
||
"gamemodes/test/gamemode/cl_init.lua",
|
||
"include( \"shared.lua\" )\ncl_loaded = true\n",
|
||
);
|
||
game.write(
|
||
"gamemodes/test/gamemode/shared.lua",
|
||
"shared_loaded = true\n",
|
||
);
|
||
|
||
let rt = LuaRuntime::new().expect("рантайм");
|
||
let report = rt.load_gamemode(game.path(), "test");
|
||
assert_eq!(report.loaded, vec!["cl_init.lua".to_string()]);
|
||
assert_eq!(report.errors.len(), 1, "{:?}", report.errors);
|
||
assert!(
|
||
report.errors[0].contains("init упал"),
|
||
"{:?}",
|
||
report.errors
|
||
);
|
||
assert_eq!(
|
||
rt.probe("return tostring(cl_loaded) .. tostring(shared_loaded)")
|
||
.expect("probe"),
|
||
Some("truetrue".into()),
|
||
"cl_init и include рядом с геймодом должны выполниться"
|
||
);
|
||
|
||
// Имя режима не должно становиться путём.
|
||
let bad = rt.load_gamemode(game.path(), "../escape");
|
||
assert_eq!(bad.loaded.len(), 0);
|
||
assert!(!bad.errors.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn include_limits_depth() {
|
||
let game = TempGame::new("depth");
|
||
for index in 1..=10 {
|
||
game.write(
|
||
&format!("lua/depth/f{index}.lua"),
|
||
&format!(
|
||
"depth = ( depth or 0 ) + 1\ninclude( \"depth/f{}.lua\" )\n",
|
||
index + 1
|
||
),
|
||
);
|
||
}
|
||
// Последний файл цепочки ничего не включает.
|
||
game.write("lua/depth/f11.lua", "depth_end = true\n");
|
||
|
||
let rt = LuaRuntime::new().expect("рантайм");
|
||
rt.set_file_root(game.path()).expect("корень");
|
||
let result = rt.run("depth.lua", b"include( \"depth/f1.lua\" )");
|
||
let error = result.expect_err("глубина должна быть ограничена");
|
||
assert!(
|
||
error.to_string().contains("глубина"),
|
||
"неожиданная ошибка: {error}"
|
||
);
|
||
}
|