use crate::{
error::Error,
util::{read_i16_le, read_i32_le, read_u16_le},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Operand {
Byte(u8),
Int16(i16),
Int32(i32),
StackVar(i16),
ConstPoolIndex(u16),
JumpTarget(u16),
ControlIndex(u16),
VTableRef {
offset: u16,
control: u16,
},
ExternalCall {
import: u16,
arg_info: u16,
},
VariableLength {
byte_count: u16,
},
}
pub fn decode_operands(
format: &str,
stream: &[u8],
pos: &mut usize,
limit: usize,
) -> Result<[Option<Operand>; 4], Error> {
let mut operands = [None; 4];
let mut op_idx = 0usize;
let mut iter = format.bytes();
while op_idx < operands.len() {
let Some(b) = iter.next() else { break };
if b != b'%' {
continue;
}
let Some(spec) = iter.next() else { break };
let operand = match spec {
b'1' => {
ensure_bytes(stream, *pos, 1, limit)?;
let val = stream
.get(*pos)
.copied()
.ok_or(Error::UnexpectedEndOfPCode {
offset: *pos,
needed: 1,
})?;
advance(pos, 1)?;
Operand::Byte(val)
}
b'2' => {
ensure_bytes(stream, *pos, 2, limit)?;
let val = read_i16_le(stream, *pos)?;
advance(pos, 2)?;
Operand::Int16(val)
}
b'4' => {
ensure_bytes(stream, *pos, 4, limit)?;
let val = read_i32_le(stream, *pos)?;
advance(pos, 4)?;
Operand::Int32(val)
}
b'a' => {
ensure_bytes(stream, *pos, 2, limit)?;
let val = read_i16_le(stream, *pos)?;
advance(pos, 2)?;
Operand::StackVar(val)
}
b's' => {
ensure_bytes(stream, *pos, 2, limit)?;
let val = read_u16_le(stream, *pos)?;
advance(pos, 2)?;
Operand::ConstPoolIndex(val)
}
b'l' => {
ensure_bytes(stream, *pos, 2, limit)?;
let val = read_u16_le(stream, *pos)?;
advance(pos, 2)?;
Operand::JumpTarget(val)
}
b'c' => {
ensure_bytes(stream, *pos, 2, limit)?;
let val = read_u16_le(stream, *pos)?;
advance(pos, 2)?;
Operand::ControlIndex(val)
}
b'v' => {
ensure_bytes(stream, *pos, 4, limit)?;
let offset = read_u16_le(stream, *pos)?;
let control_pos = pos.checked_add(2).ok_or(Error::ArithmeticOverflow {
context: "operand %v control offset",
})?;
let control = read_u16_le(stream, control_pos)?;
advance(pos, 4)?;
Operand::VTableRef { offset, control }
}
b'x' => {
ensure_bytes(stream, *pos, 4, limit)?;
let import = read_u16_le(stream, *pos)?;
let arg_pos = pos.checked_add(2).ok_or(Error::ArithmeticOverflow {
context: "operand %x arg_info offset",
})?;
let arg_info = read_u16_le(stream, arg_pos)?;
advance(pos, 4)?;
Operand::ExternalCall { import, arg_info }
}
_ => continue,
};
if let Some(slot) = operands.get_mut(op_idx) {
*slot = Some(operand);
}
op_idx = op_idx.checked_add(1).ok_or(Error::ArithmeticOverflow {
context: "operand index",
})?;
}
Ok(operands)
}
#[inline]
fn advance(pos: &mut usize, delta: usize) -> Result<(), Error> {
*pos = pos.checked_add(delta).ok_or(Error::ArithmeticOverflow {
context: "operand pos advance",
})?;
Ok(())
}
fn ensure_bytes(stream: &[u8], pos: usize, needed: usize, limit: usize) -> Result<(), Error> {
let available = limit
.saturating_sub(pos)
.min(stream.len().saturating_sub(pos));
if available < needed {
return Err(Error::UnexpectedEndOfPCode {
offset: pos,
needed,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_byte_operand() {
let stream = [0x42];
let mut pos = 0;
let ops = decode_operands("%1", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(ops[0], Some(Operand::Byte(0x42)));
assert_eq!(ops[1], None);
assert_eq!(pos, 1);
}
#[test]
fn test_decode_int16_operand() {
let stream = [0x34, 0x12];
let mut pos = 0;
let ops = decode_operands("%2", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(ops[0], Some(Operand::Int16(0x1234)));
assert_eq!(pos, 2);
}
#[test]
fn test_decode_int32_operand() {
let stream = [0x78, 0x56, 0x34, 0x12];
let mut pos = 0;
let ops = decode_operands("%4", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(ops[0], Some(Operand::Int32(0x12345678)));
assert_eq!(pos, 4);
}
#[test]
fn test_decode_stack_var() {
let stream = [0x70, 0xFF];
let mut pos = 0;
let ops = decode_operands("%a", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(ops[0], Some(Operand::StackVar(-144)));
assert_eq!(pos, 2);
}
#[test]
fn test_decode_const_pool_index() {
let stream = [0x10, 0x00];
let mut pos = 0;
let ops = decode_operands("%s", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(ops[0], Some(Operand::ConstPoolIndex(0x0010)));
assert_eq!(pos, 2);
}
#[test]
fn test_decode_jump_target() {
let stream = [0x20, 0x00];
let mut pos = 0;
let ops = decode_operands("%l", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(ops[0], Some(Operand::JumpTarget(0x0020)));
assert_eq!(pos, 2);
}
#[test]
fn test_decode_control_index() {
let stream = [0x05, 0x00];
let mut pos = 0;
let ops = decode_operands("%c", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(ops[0], Some(Operand::ControlIndex(5)));
assert_eq!(pos, 2);
}
#[test]
fn test_decode_vtable_ref() {
let stream = [0x10, 0x00, 0x03, 0x00];
let mut pos = 0;
let ops = decode_operands("%v", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(
ops[0],
Some(Operand::VTableRef {
offset: 0x10,
control: 0x03,
})
);
assert_eq!(pos, 4);
}
#[test]
fn test_decode_external_call() {
let stream = [0x02, 0x00, 0x04, 0x00];
let mut pos = 0;
let ops = decode_operands("%x", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(
ops[0],
Some(Operand::ExternalCall {
import: 2,
arg_info: 4,
})
);
assert_eq!(pos, 4);
}
#[test]
fn test_decode_multiple_operands() {
let stream = [0x70, 0xFF, 0x05, 0x00];
let mut pos = 0;
let ops = decode_operands("%a %2", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(ops[0], Some(Operand::StackVar(-144)));
assert_eq!(ops[1], Some(Operand::Int16(5)));
assert_eq!(ops[2], None);
assert_eq!(pos, 4);
}
#[test]
fn test_decode_empty_format() {
let stream = [0x00];
let mut pos = 0;
let ops = decode_operands("", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(ops[0], None);
assert_eq!(pos, 0);
}
#[test]
fn test_decode_end_of_procedure_marker() {
let stream = [];
let mut pos = 0;
let ops = decode_operands("%}", &stream, &mut pos, stream.len()).unwrap();
assert_eq!(ops[0], None); assert_eq!(pos, 0);
}
#[test]
fn test_decode_truncated_stream() {
let stream = [0x01]; let mut pos = 0;
assert!(matches!(
decode_operands("%2", &stream, &mut pos, stream.len()),
Err(Error::UnexpectedEndOfPCode { .. })
));
}
#[test]
fn test_decode_truncated_at_limit() {
let stream = [0x01, 0x02, 0x03, 0x04];
let mut pos = 0;
assert!(matches!(
decode_operands("%2", &stream, &mut pos, 1),
Err(Error::UnexpectedEndOfPCode { .. })
));
}
#[test]
fn test_decode_max_4_operands() {
let stream = [0x01, 0x02, 0x03, 0x04, 0x05];
let mut pos = 0;
let ops = decode_operands("%1 %1 %1 %1 %1", &stream, &mut pos, stream.len()).unwrap();
assert!(ops[0].is_some());
assert!(ops[1].is_some());
assert!(ops[2].is_some());
assert!(ops[3].is_some());
assert_eq!(pos, 4); }
}