use std::process::Command;
use std::path::Path;
use std::fs::File;
use std::io::{Read, BufReader};
use log::{info, trace, warn};
use crate::config::LUAC_PATH;
pub fn check_executable_lua_file(filename: &str) -> bool {
#[cfg(not(debug_assertions))]
{
todo!();
check_bytecode_pure_rust(filename).unwrap_or_else(|e| {
warn!("检查失败:{}", e);
false
})
}
#[cfg(debug_assertions)]
{
check_bytecode_with_luac(filename).unwrap_or_else(|e| {
warn!("检查失败:{}", e);
false
})
}
}
#[cfg(debug_assertions)]
fn check_bytecode_with_luac<P: AsRef<Path>>(file_path: P) -> Result<bool, String> {
let manifest_dir = std::env!("CARGO_MANIFEST_DIR");
let luac_path = Path::new(manifest_dir).join(LUAC_PATH);
if !luac_path.exists() {
return Err(format!("luac.exe 不存在:{:?}", luac_path));
}
let output = Command::new(&luac_path)
.arg("-l")
.arg(file_path.as_ref())
.output()
.map_err(|e| format!("执行 luac 失败:{}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("luac 执行失败:{}", stderr));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut in_main_function = false;
let mut found_main_header = false;
for (line_num, line) in stdout.lines().enumerate() {
if line.starts_with("main <") && line.contains("instructions") {
in_main_function = true;
found_main_header = true;
trace!("开始检查 main 函数...");
continue;
}
if in_main_function {
if line.trim().is_empty() || line.starts_with("function <") {
trace!("✓ main 函数检查完成,未检测到危险指令");
return Ok(true); }
if line.contains(" CALL ") || line.contains("\tCALL\t") ||
line.contains("\tCALL ") || line.contains(" CALL\t") ||
line.contains(" TAILCALL ") || line.contains("\tTAILCALL\t") ||
line.contains("\tTAILCALL ") || line.contains(" TAILCALL\t") {
warn!("检测到危险指令 @行 {}: {}", line_num + 1, line.trim());
return Ok(false); }
}
}
if !found_main_header {
return Err("luac 输出格式错误:未找到 main 函数".to_string());
}
trace!("✓ 字节码安全,未检测到 CALL/TAILCALL 指令");
Ok(true) }
fn check_bytecode_pure_rust<P: AsRef<Path>>(file_path: P) -> Result<bool, String> {
let file = File::open(file_path.as_ref())
.map_err(|e| format!("无法打开文件:{}", e))?;
let mut reader = BufReader::new(file);
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)
.map_err(|e| format!("读取文件失败:{}", e))?;
if buffer.len() < 4 || &buffer[0..4] != b"\x1B\x4C\x75\x61" {
return Err("无效的 Lua 字节码魔数".to_string());
}
let mut pos = 4;
pos += 8;
if pos >= buffer.len() {
return Err("文件太短,缺少头部信息".to_string());
}
let int_sz = buffer[pos] as usize;
pos += 1 + int_sz;
if pos >= buffer.len() { return Err("格式错误".into()); }
let inst_sz = buffer[pos] as usize;
pos += 1 + inst_sz;
if pos >= buffer.len() { return Err("格式错误".into()); }
let intg_sz = buffer[pos] as usize;
pos += 1 + intg_sz;
if pos >= buffer.len() { return Err("格式错误".into()); }
let num_sz = buffer[pos] as usize;
pos += 1 + num_sz;
let (linedefined, n) = read_varint_at(&buffer, pos)?;
pos += n;
let _ = linedefined;
let (lastlinedefined, n) = read_varint_at(&buffer, pos)?;
pos += n;
let _ = lastlinedefined;
if pos >= buffer.len() { return Err("格式错误".into()); }
let _numparams = buffer[pos];
pos += 1;
if pos >= buffer.len() { return Err("格式错误".into()); }
let _flag = buffer[pos];
pos += 1;
if pos >= buffer.len() { return Err("格式错误".into()); }
let _maxstacksize = buffer[pos];
pos += 1;
let (numupvalues, n) = read_varint_at(&buffer, pos)?;
pos += n;
trace!("ℹnumupvalues = {}", numupvalues);
let (code_size, n) = read_varint_at(&buffer, pos)?;
pos += n;
trace!("解析到 code_size = {}", code_size);
for i in 0..code_size {
if pos + 4 > buffer.len() {
return Err(format!("指令 {} 超出文件范围", i));
}
let instruction = u32::from_le_bytes([
buffer[pos],
buffer[pos + 1],
buffer[pos + 2],
buffer[pos + 3]
]);
pos += 4;
let opcode = (instruction & 0x7F) as u8;
if opcode == 68 || opcode == 69 {
warn!("检测到危险指令 #{}: opcode={} (0x{:02X})", i, opcode, opcode);
return Ok(false); }
}
info!("字节码安全,检查了 {} 条指令", code_size);
Ok(true) }
fn read_varint_at(buffer: &[u8], pos: usize) -> Result<(usize, usize), String> {
let mut result: usize = 0;
let mut shift = 0;
let mut bytes_read = 0;
loop {
if pos + bytes_read >= buffer.len() {
return Err("varint 超出缓冲区".to_string());
}
let byte = buffer[pos + bytes_read];
result |= ((byte & 0x7F) as usize) << shift;
shift += 7;
bytes_read += 1;
if byte < 0x80 {
break;
}
}
Ok((result, bytes_read))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
fn get_samples_dir() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
PathBuf::from(manifest_dir).join("src/security/samples")
}
fn extract_name_without_suffix(wing_path: &Path) -> String {
wing_path
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
.unwrap_or_default()
}
fn collect_unique_names(samples_dir: &Path) -> Vec<String> {
let mut names = std::collections::HashSet::new();
if let Ok(entries) = fs::read_dir(samples_dir) {
for entry in entries.flatten() {
if let Some(name) = entry.path().file_stem().and_then(|s| s.to_str()) {
names.insert(name.to_string());
}
}
}
let mut unique_names: Vec<String> = names.into_iter().collect();
unique_names.sort(); unique_names
}
#[test]
fn test_all_samples_automated() {
let samples_dir = get_samples_dir();
let unique_names = collect_unique_names(&samples_dir);
println!("\n📁 测试目录:{:?}", samples_dir);
println!("📋 发现 {} 个唯一名称", unique_names.len());
let mut passed = 0;
let mut failed = 0;
for name in &unique_names {
let wing_path = samples_dir.join(format!("{}.wing", name));
let txt_path = samples_dir.join(format!("{}.txt", name));
if !wing_path.exists() || !txt_path.exists() {
println!("⏭️ 跳过 {}: 缺少必要文件", name);
continue;
}
let expected_str = match fs::read_to_string(&txt_path) {
Ok(content) => content.trim().to_lowercase(),
Err(e) => {
println!("❌ {}: 无法读取预期结果 - {}", name, e);
failed += 1;
continue;
}
};
let expected_bool = match expected_str.as_str() {
"true" | "1" | "yes" | "safe" => true,
"false" | "0" | "no" | "unsafe" => false,
_ => {
println!("⚠️ {}: 无效的预期结果 '{}'", name, expected_str);
failed += 1;
continue;
}
};
let result = check_executable_lua_file(&wing_path.to_string_lossy());
if result == expected_bool {
println!("✅ {}: 通过 (expected={}, got={})",
name, expected_bool, result);
passed += 1;
} else {
println!("❌ {}: 失败 (expected={}, got={})",
name, expected_bool, result);
failed += 1;
}
}
println!("\n📊 测试结果汇总:{} 通过,{} 失败", passed, failed);
assert_eq!(failed, 0, "有 {} 个测试失败", failed);
}
#[test]
fn test_quick_validation() {
let samples_dir = get_samples_dir();
let safe_path = samples_dir.join("01_safe_def.wing");
if safe_path.exists() {
let result = check_executable_lua_file(&safe_path.to_string_lossy());
assert!(result, "01_safe_def.wing 应该返回 true");
}
let unsafe_path = samples_dir.join("02_unsafe_call.wing");
if unsafe_path.exists() {
let result = check_executable_lua_file(&unsafe_path.to_string_lossy());
assert!(!result, "02_unsafe_call.wing 应该返回 false");
}
}
}