Files
2026-10-22 12:44:55 +07:00

391 lines
12 KiB
Rust

//! Milestone 2 Bytecode Deserializer, LEB128 & Polynomial Unhashing Test Suite.
//!
//! Validates:
//! - T1_F03_01: Biased LEB128 positive integers (bias = 217)
//! - T1_F03_02: Biased LEB128 negative integers (bias = 217)
//! - T1_F03_03: Signed Zig-Zag integer decoding
//! - T1_F03_04: Dynamic opcode polynomial unhashing
//! - T2_B05: Malformed magic header rejection
//! - T2_B06: Truncated LEB128 stream detection
//! - T2_B07: Max 64-bit LEB128 value boundary
//! - T2_B08: Min i32 Zig-Zag (-2^31)
//! - T2_B09: Max i32 Zig-Zag (2^31 - 1)
//! - T2_B44: Opcode unhashing negative intermediate wrap-around
use luraph_deobf_rs::bytecode::deserializer::{
BytecodeDeserializer, DeserializerError, LURAPH_MAGIC_HEADER,
};
use luraph_deobf_rs::bytecode::leb128::{
decode_byte_biased_leb128, decode_leb128_biased, decode_u64, zigzag_decode,
zigzag_encode, Leb128Error, LURAPH_LEB128_BIAS,
};
use luraph_deobf_rs::bytecode::unhashing::{
compute_poly_delta, compute_tz, unhash_opcode_with_custom_tz, OpcodeMap,
POLY_MOD, TA,
};
#[test]
fn test_t1_f03_01_leb128_bias217_pos() {
// Encodes 300: (300 + 217) = 517 -> 0x85 0x04
let buffer = [0x85, 0x04];
let (val, bytes_read) = decode_leb128_biased(&buffer, LURAPH_LEB128_BIAS).unwrap();
assert_eq!(bytes_read, 2);
assert_eq!(val, 300);
}
#[test]
fn test_t1_f03_02_leb128_bias217_neg() {
// Encodes -100: (-100 + 217) = 117 -> 0x75
let buffer = [0x75];
let (val, bytes_read) = decode_leb128_biased(&buffer, LURAPH_LEB128_BIAS).unwrap();
assert_eq!(bytes_read, 1);
assert_eq!(val, -100);
}
#[test]
fn test_byte_biased_leb128_bt_variant() {
// Tests Luraph VM bT: (byte - 217) % 256
// If tw = (byte - 217) % 256, for tw = 42: byte = (42 + 217) % 256 = 3
let buffer = [3u8];
let (val, bytes_read) = decode_byte_biased_leb128(&buffer, 217).unwrap();
assert_eq!(bytes_read, 1);
assert_eq!(val, 42);
}
#[test]
fn test_t1_f03_03_zigzag_decode() {
// Zig-zag sequence: 0 -> 0, 1 -> -1, 2 -> 1, 3 -> -2
assert_eq!(zigzag_decode(0), 0);
assert_eq!(zigzag_decode(1), -1);
assert_eq!(zigzag_decode(2), 1);
assert_eq!(zigzag_decode(3), -2);
// Round-trip verification
for n in -500..=500 {
assert_eq!(zigzag_decode(zigzag_encode(n)), n);
}
}
#[test]
fn test_t2_b08_min_i32_zigzag() {
// Min i32 = -2147483648
let min_i32: i64 = -2147483648;
let enc = zigzag_encode(min_i32);
assert_eq!(zigzag_decode(enc), min_i32);
}
#[test]
fn test_t2_b09_max_i32_zigzag() {
// Max i32 = 2147483647
let max_i32: i64 = 2147483647;
let enc = zigzag_encode(max_i32);
assert_eq!(zigzag_decode(enc), max_i32);
}
#[test]
fn test_t2_b07_max_u64_leb128() {
// [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01] = u64::MAX
let max_leb = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01];
let (val, bytes_read) = decode_u64(&max_leb).unwrap();
assert_eq!(bytes_read, 10);
assert_eq!(val, u64::MAX);
}
#[test]
fn test_t2_b06_truncated_leb128() {
// [0x85, 0x80] has MSB set on both bytes, missing final terminating byte
let truncated = [0x85, 0x80];
let err = decode_u64(&truncated).unwrap_err();
assert_eq!(err, Leb128Error::UnexpectedEof { offset: 2 });
}
#[test]
fn test_leb128_overflow_11_bytes() {
let overflow = [0x80; 11];
let err = decode_u64(&overflow).unwrap_err();
assert!(matches!(err, Leb128Error::Overflow { .. }));
}
#[test]
fn test_t1_f03_04_opcode_unhashing_poly() {
// From fixture t1_f03_opcode_unhashing_poly.lua:
// TA = 1049162819, tz = 42, pc = 5, raw_tj = 1049162850
// poly = (42*42 + 5*117911 + 74301) % 1000003 = 665620
// xor = 1049162850 ^ 1049162819 = 33
// opcode = 33 - 665620 = -665587
let tz: i64 = 42;
let pc: usize = 5;
let raw_tj: u32 = 1049162850;
let opcode = unhash_opcode_with_custom_tz(raw_tj, pc, tz);
assert_eq!(opcode, -665587);
// Test compute_tz with standard formula:
let natural_tz = compute_tz(pc);
let natural_delta = compute_poly_delta(natural_tz, pc);
assert!(natural_tz >= 0 && natural_tz < POLY_MOD);
assert!(natural_delta >= 0 && natural_delta < POLY_MOD);
}
#[test]
fn test_t2_b44_opcode_poly_negative_intermediate() {
// From fixture t2_b44_opcode_poly_negative_intermediate.lua:
// raw = 500, poly = 2000000 -> (500 - 2000000) % 1000003 == 506
let raw: i64 = 500;
let poly: i64 = 2000000;
let res = (raw - poly).rem_euclid(POLY_MOD);
assert_eq!(res, 506);
}
#[test]
fn test_opcode_map_resolution() {
let mut map = OpcodeMap::new();
map.insert(512, 1); // JMP
map.insert(50, 2); // CALL
// Natural resolution for JMP
assert_eq!(map.resolve(512 ^ TA, 1), map.resolve(512 ^ TA, 1));
}
#[test]
fn test_t2_b05_malformed_header() {
let deserializer = BytecodeDeserializer::new();
let malformed = b"\x1bLPH_CORRUPTED_MAGIC_HEADER_0xDEADBEEF\x00\xff";
let err = deserializer.deserialize(malformed).unwrap_err();
assert!(matches!(err, DeserializerError::BadMagicHeader { .. }));
}
#[test]
fn test_deserializer_truncated_empty() {
let deserializer = BytecodeDeserializer::new();
let err = deserializer.deserialize(&[]).unwrap_err();
assert!(matches!(err, DeserializerError::TruncatedPayload { .. }));
}
#[test]
fn test_deserializer_valid_minimal_payload() {
let deserializer = BytecodeDeserializer::new();
let mut payload = Vec::new();
// 1. Magic Header
payload.extend_from_slice(LURAPH_MAGIC_HEADER);
// 2. Constants count = 0
payload.push(0x00);
// 3. Instructions count = 0
payload.push(0x00);
// 4. Upvalues count = 0
payload.push(0x00);
// 5. Max stack = 10
payload.push(0x0A);
// 6. Params = 0, Vararg = 0
payload.push(0x00);
payload.push(0x00);
// 7. Sub-protos = 0
payload.push(0x00);
let proto = deserializer.deserialize(&payload).unwrap();
assert_eq!(proto.id, 0);
assert_eq!(proto.instructions.len(), 0);
assert_eq!(proto.constants.len(), 0);
assert_eq!(proto.sub_protos.len(), 0);
assert_eq!(proto.max_stack, 10);
assert_eq!(proto.num_params, 0);
assert!(!proto.is_vararg);
}
#[test]
fn test_deserializer_nested_prototype_hierarchy() {
let deserializer = BytecodeDeserializer::new();
let mut payload = Vec::new();
// Header
payload.extend_from_slice(LURAPH_MAGIC_HEADER);
// Root proto (ID 0)
// 1. Constants count = 1: Tag 20 (Boolean false)
payload.push(0x01);
payload.push(20);
// 2. Instructions count = 1
payload.push(0x01);
payload.push(0x00); // c = 0
payload.push(0x01); // a = 1
payload.push(0x02); // raw_op = 2
payload.push(0x00); // b = 0
// 3. Upvalues count = 1: (mode 0, index 3 -> word = 12)
payload.push(0x01);
payload.push(12);
// 4. Max stack = 5, params = 2, vararg = 1
payload.push(0x05);
payload.push(0x02);
payload.push(0x01);
// 5. Sub-protos count = 1 (Child proto ID 1)
payload.push(0x01);
// Child proto body:
// Constants count = 0
payload.push(0x00);
// Instructions count = 0
payload.push(0x00);
// Upvalues count = 0
payload.push(0x00);
// Max stack = 2, params = 0, vararg = 0
payload.push(0x02);
payload.push(0x00);
payload.push(0x00);
// Sub-protos count = 0
payload.push(0x00);
let root = deserializer.deserialize(&payload).unwrap();
assert_eq!(root.id, 0);
assert_eq!(root.constants.len(), 1);
assert_eq!(root.instructions.len(), 1);
assert_eq!(root.upvalues.len(), 1);
assert!(root.upvalues[0].in_stack);
assert_eq!(root.upvalues[0].index, 3);
assert_eq!(root.num_params, 2);
assert!(root.is_vararg);
assert_eq!(root.sub_protos.len(), 1);
assert_eq!(root.sub_protos[0].id, 1);
// Flattening check
let flat = root.flatten();
assert_eq!(flat.len(), 2);
assert_eq!(flat[0].id, 0);
assert_eq!(flat[0].child_protos, vec![1]);
assert_eq!(flat[1].id, 1);
}
#[test]
fn test_deserializer_branching_prototype_hierarchy_unique_ids() {
let deserializer = BytecodeDeserializer::new();
let mut payload = Vec::new();
payload.extend_from_slice(LURAPH_MAGIC_HEADER);
// Root proto (ID 0): 2 sub-protos (Child A and Child B)
payload.push(0x00); // constants = 0
payload.push(0x00); // instructions = 0
payload.push(0x00); // upvalues = 0
payload.push(0x02); // max_stack = 2
payload.push(0x00); // num_params = 0
payload.push(0x00); // is_vararg = false
payload.push(0x02); // sub_proto_count = 2
// Child A (first child of Root): has 1 child (Child A.1)
payload.push(0x00); // constants = 0
payload.push(0x00); // instructions = 0
payload.push(0x00); // upvalues = 0
payload.push(0x02); // max_stack = 2
payload.push(0x00); // num_params = 0
payload.push(0x00); // is_vararg = false
payload.push(0x01); // sub_proto_count = 1
// Child A.1 (child of Child A): 0 children
payload.push(0x00); // constants = 0
payload.push(0x00); // instructions = 0
payload.push(0x00); // upvalues = 0
payload.push(0x02); // max_stack = 2
payload.push(0x00); // num_params = 0
payload.push(0x00); // is_vararg = false
payload.push(0x00); // sub_proto_count = 0
// Child B (second child of Root): 0 children
payload.push(0x00); // constants = 0
payload.push(0x00); // instructions = 0
payload.push(0x00); // upvalues = 0
payload.push(0x02); // max_stack = 2
payload.push(0x00); // num_params = 0
payload.push(0x00); // is_vararg = false
payload.push(0x00); // sub_proto_count = 0
let root = deserializer.deserialize(&payload).unwrap();
assert_eq!(root.id, 0);
assert_eq!(root.sub_protos.len(), 2);
let child_a = &root.sub_protos[0];
let child_b = &root.sub_protos[1];
assert_eq!(child_a.id, 1);
assert_eq!(child_a.sub_protos.len(), 1);
let child_a1 = &child_a.sub_protos[0];
assert_eq!(child_a1.id, 2);
// With monotonic counter, Child B must receive ID 3 (NOT ID 2)
assert_eq!(child_b.id, 3);
// Verify flatten() contains all 4 prototypes with strictly unique IDs
let flat = root.flatten();
assert_eq!(flat.len(), 4);
let ids: Vec<usize> = flat.iter().map(|p| p.id).collect();
assert_eq!(ids, vec![0, 1, 2, 3]);
let mut id_set = std::collections::HashSet::new();
for proto in &flat {
assert!(id_set.insert(proto.id), "Duplicate prototype ID detected: {}", proto.id);
}
// Verify parent -> child linkage references
assert_eq!(flat[0].child_protos, vec![1, 3]);
assert_eq!(flat[1].child_protos, vec![2]);
assert_eq!(flat[2].child_protos, Vec::<usize>::new());
assert_eq!(flat[3].child_protos, Vec::<usize>::new());
}
#[test]
fn test_leb128_overflow_10th_byte_high_bit() {
// 10-byte LEB128 stream where 10th byte has low7 = 2 (bit 64 set, exceeding u64)
// Bytes 0..8 have continuation bit set (0x80)
let stream = [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x02];
let err = decode_u64(&stream).unwrap_err();
assert_eq!(
err,
Leb128Error::Overflow {
offset: 9,
max_bytes: 10,
}
);
}
#[test]
fn test_deserializer_huge_count_no_oom() {
let deserializer = BytecodeDeserializer::new();
let mut payload = Vec::new();
payload.extend_from_slice(LURAPH_MAGIC_HEADER);
// Huge constants count (approx 2 billion in LEB128)
payload.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF, 0x07]);
// Followed by only 1 byte
payload.push(0x00);
// Must return an error gracefully without triggering an out-of-memory abort
let res = deserializer.deserialize(&payload);
assert!(res.is_err());
}
#[test]
fn test_deserializer_table_recursion_depth_limit() {
let deserializer = BytecodeDeserializer::new();
let mut payload = Vec::new();
payload.extend_from_slice(LURAPH_MAGIC_HEADER);
// Root proto: 1 constant
payload.push(0x01); // const_count = 1
// Build 66 nested tables: tag 163, arr_len = 1
for _ in 0..66 {
payload.push(163); // Table tag
payload.push(0x01); // arr_len = 1
}
payload.push(17); // Nil for the leaf value
for _ in 0..66 {
payload.push(0x00); // hash_len = 0
}
let err = deserializer.deserialize(&payload).unwrap_err();
assert_eq!(
err,
DeserializerError::MaxRecursionDepthExceeded { max_depth: 64 }
);
}