pub mod opcode;
mod parse;
mod query;
pub mod stack;
mod types;
pub use opcode::{OP_NUM_OPCODES, OP_PARAMS, OP_VARIABLE_LENGTH, OpcodeMap, operand_cells};
pub use types::{AmxDbg, DbgError, DbgFile, DbgLine, DbgSymDim, DbgSymbol, DbgTag, Ident, VClass};
pub const AMX_DBG_MAGIC: u16 = 0xf1ef;
#[cfg(test)]
mod tests {
use super::{AMX_DBG_MAGIC, AmxDbg, Ident, VClass};
fn sample_block() -> Vec<u8> {
let mut b = Vec::new();
let mut tables = Vec::new();
push_u32(&mut tables, 0); push_cstr(&mut tables, "a.pwn");
push_u32(&mut tables, 100); push_cstr(&mut tables, "b.inc");
for (addr, line) in [(0u32, 1i32), (8, 2), (20, 3), (104, 10)] {
push_u32(&mut tables, addr);
push_i32(&mut tables, line);
}
push_symbol(&mut tables, 0, 0, 0, 40, 9, 0, "main", &[]);
push_symbol(&mut tables, 200, 1, 0, 0, 1, 0, "g_score", &[]);
push_symbol(
&mut tables,
(-4i32).cast_unsigned(),
0,
8,
40,
1,
1,
"tmp",
&[],
);
push_i16(&mut tables, 1);
push_cstr(&mut tables, "Float");
let total = 22 + tables.len();
push_i32(&mut b, i32::try_from(total).unwrap()); push_u16(&mut b, AMX_DBG_MAGIC); b.push(1); b.push(1); push_i16(&mut b, 0); push_i16(&mut b, 2); push_i16(&mut b, 4); push_i16(&mut b, 3); push_i16(&mut b, 1); push_i16(&mut b, 0); push_i16(&mut b, 0); assert_eq!(b.len(), 22);
b.extend_from_slice(&tables);
b
}
fn push_u16(v: &mut Vec<u8>, x: u16) {
v.extend_from_slice(&x.to_le_bytes());
}
fn push_i16(v: &mut Vec<u8>, x: i16) {
v.extend_from_slice(&x.to_le_bytes());
}
fn push_u32(v: &mut Vec<u8>, x: u32) {
v.extend_from_slice(&x.to_le_bytes());
}
fn push_i32(v: &mut Vec<u8>, x: i32) {
v.extend_from_slice(&x.to_le_bytes());
}
fn push_cstr(v: &mut Vec<u8>, s: &str) {
v.extend_from_slice(s.as_bytes());
v.push(0);
}
#[allow(clippy::too_many_arguments)]
fn push_symbol(
v: &mut Vec<u8>,
address: u32,
tag: i16,
codestart: u32,
codeend: u32,
ident: u8,
vclass: u8,
name: &str,
dims: &[(i16, u32)],
) {
push_u32(v, address);
push_i16(v, tag);
push_u32(v, codestart);
push_u32(v, codeend);
v.push(ident);
v.push(vclass);
push_i16(v, i16::try_from(dims.len()).unwrap());
push_cstr(v, name);
for &(dtag, size) in dims {
push_i16(v, dtag);
push_u32(v, size);
}
}
#[test]
fn parses_header_and_tables() {
let dbg = AmxDbg::parse(&sample_block()).expect("parse");
assert_eq!(dbg.files.len(), 2);
assert_eq!(dbg.lines.len(), 4);
assert_eq!(dbg.symbols.len(), 3);
assert_eq!(dbg.tags.len(), 1);
assert_eq!(dbg.files[1].name, "b.inc");
assert_eq!(dbg.files[1].address, 100);
assert_eq!(dbg.tag_name(1), Some("Float"));
}
#[test]
fn lookup_line_and_file() {
let dbg = AmxDbg::parse(&sample_block()).unwrap();
assert_eq!(dbg.lookup_line(0), Some(2));
assert_eq!(dbg.lookup_line(10), Some(3));
assert_eq!(dbg.lookup_line(25), Some(4));
assert_eq!(dbg.lookup_line(104), Some(11));
assert_eq!(dbg.lookup_file(5), Some("a.pwn"));
assert_eq!(dbg.lookup_file(150), Some("b.inc"));
}
#[test]
fn lookup_function_by_address() {
let dbg = AmxDbg::parse(&sample_block()).unwrap();
assert_eq!(dbg.lookup_function(10), Some("main")); assert_eq!(dbg.lookup_function(50), None); }
#[test]
fn line_to_address_for_breakpoint() {
let dbg = AmxDbg::parse(&sample_block()).unwrap();
assert_eq!(dbg.line_to_address(4, Some("a.pwn")), Some(20));
assert_eq!(dbg.line_to_address(11, Some("b.inc")), Some(104));
}
#[test]
fn symbols_in_scope_filters_by_address() {
let dbg = AmxDbg::parse(&sample_block()).unwrap();
let names: Vec<&str> = dbg
.symbols_in_scope(10)
.iter()
.map(|s| s.name.as_str())
.collect();
assert!(names.contains(&"g_score"));
assert!(names.contains(&"tmp"));
let names: Vec<&str> = dbg
.symbols_in_scope(5)
.iter()
.map(|s| s.name.as_str())
.collect();
assert!(names.contains(&"g_score"));
assert!(!names.contains(&"tmp"));
}
#[test]
fn symbol_classes_decoded() {
let dbg = AmxDbg::parse(&sample_block()).unwrap();
let g = dbg.symbols.iter().find(|s| s.name == "g_score").unwrap();
assert_eq!(g.ident, Ident::Variable);
assert_eq!(g.vclass, VClass::Global);
let f = dbg.symbols.iter().find(|s| s.name == "main").unwrap();
assert_eq!(f.ident, Ident::Function);
}
#[test]
fn effective_address_global_and_local() {
let dbg = AmxDbg::parse(&sample_block()).unwrap();
let g = dbg.symbols.iter().find(|s| s.name == "g_score").unwrap();
let tmp = dbg.symbols.iter().find(|s| s.name == "tmp").unwrap();
assert_eq!(g.effective_address(9_999), 200);
assert_eq!(tmp.effective_address(100), 96);
assert!(!g.is_array());
}
#[test]
fn rejects_bad_magic() {
let mut bad = sample_block();
bad[4] = 0x00; bad[5] = 0x00;
assert!(AmxDbg::parse(&bad).is_err());
}
#[test]
fn rejects_truncated() {
let block = sample_block();
assert!(AmxDbg::parse(&block[..10]).is_err());
}
#[test]
fn handles_line_count_overflow() {
const TOTAL: u32 = 98304; let mut tables = Vec::new();
push_u32(&mut tables, 0);
push_cstr(&mut tables, "big.pwn");
for i in 0..TOTAL {
push_u32(&mut tables, i * 4);
push_i32(&mut tables, i.cast_signed() + 1);
}
let mut b = Vec::new();
push_i32(&mut b, i32::try_from(22 + tables.len()).unwrap_or(i32::MAX));
push_u16(&mut b, AMX_DBG_MAGIC);
b.push(1);
b.push(1);
push_i16(&mut b, 0); push_i16(&mut b, 1); push_u16(&mut b, (TOTAL & 0xFFFF) as u16);
push_i16(&mut b, 0); push_i16(&mut b, 0); push_i16(&mut b, 0); push_i16(&mut b, 0); b.extend_from_slice(&tables);
let dbg = AmxDbg::parse(&b).expect("parse with line overflow");
assert_eq!(dbg.lines.len(), TOTAL as usize, "read the real line total");
assert_eq!(
dbg.lookup_line((TOTAL - 1) * 4),
Some(TOTAL.cast_signed() + 1)
);
}
#[test]
fn corrupt_count_does_not_oom() {
let mut b = Vec::new();
push_i32(&mut b, 22); push_u16(&mut b, AMX_DBG_MAGIC);
b.push(1);
b.push(1);
push_i16(&mut b, 0); push_i16(&mut b, 0); push_u16(&mut b, 0xFFFF); push_i16(&mut b, 0x7FFF); push_i16(&mut b, 0);
push_i16(&mut b, 0);
push_i16(&mut b, 0);
let _ = AmxDbg::parse(&b); }
}