Skip to main content

sim_codec_classfile/
instruction.rs

1//! Manifest-driven decoding of JVM instruction streams.
2
3use core::fmt;
4use std::collections::BTreeMap;
5
6use crate::{Constant, ConstantPool, Opcode};
7
8/// Stable identity assigned in bytecode order within one decoded code array.
9#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
10pub struct InstructionId(pub u32);
11
12/// One decoded operand, retaining its semantic role from the opcode manifest.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum InstructionOperand {
15    /// A signed immediate value.
16    Immediate(i32),
17    /// A local-variable index.
18    Local(u16),
19    /// A constant-pool index.
20    Constant(u16),
21    /// A relative branch displacement.
22    Branch(i32),
23    /// The lowest key accepted by a `tableswitch`.
24    TableLow(i32),
25    /// The highest key accepted by a `tableswitch`.
26    TableHigh(i32),
27    /// One key in a `lookupswitch`, followed by its branch operand.
28    LookupKey(i32),
29    /// The `invokeinterface` argument count.
30    Count(u8),
31    /// The `multianewarray` dimension count.
32    Dimensions(u8),
33    /// The primitive array type discriminator.
34    ArrayType(u8),
35}
36
37/// A decoded instruction whose shape was selected by generated manifest metadata.
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct Instruction {
40    /// Effective opcode (the opcode following `wide` for a widened instruction).
41    pub opcode: Opcode,
42    /// Whether the instruction was encoded with the `wide` prefix.
43    pub wide: bool,
44    /// Decoded operands in manifest order.
45    pub operands: Vec<InstructionOperand>,
46}
47
48/// An instruction paired with its stable identity and first byte offset.
49#[derive(Clone, Debug, Eq, PartialEq)]
50pub struct LocatedInstruction {
51    /// Stable stream-local identity.
52    pub id: InstructionId,
53    /// Offset of the opcode, or of the `wide` prefix when present.
54    pub offset: u32,
55    /// Decoded instruction.
56    pub instruction: Instruction,
57}
58
59/// A decoded code array and its exact first-byte lookup map.
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct DecodedCode {
62    /// Instructions in bytecode order.
63    pub instructions: Vec<LocatedInstruction>,
64    /// Map from every instruction's first byte offset to its stable identity.
65    pub offsets: BTreeMap<u32, InstructionId>,
66}
67
68/// Raw Code-attribute exception-table offsets to validate against decoded instructions.
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub struct ExceptionHandlerRange {
71    /// First protected instruction, inclusive.
72    pub start: u16,
73    /// End of the protected range, exclusive; the code length is permitted.
74    pub end: u16,
75    /// First instruction of the handler.
76    pub handler: u16,
77}
78
79/// Stable instruction decoding failure category.
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum InstructionErrorKind {
82    /// An operand extends beyond the code array.
83    Truncated,
84    /// The opcode is reserved or otherwise invalid in a classfile.
85    InvalidOpcode,
86    /// The opcode is unavailable in the requested classfile version.
87    Version,
88    /// A mandated reserved operand byte was nonzero.
89    ReservedByte,
90    /// The constant-pool index has the wrong category or is unusable.
91    ConstantPool,
92    /// A `wide` prefix modifies an opcode that cannot be widened.
93    IllegalWide,
94    /// A variable layout is malformed or too large to represent safely.
95    VariableLayout,
96    /// A relative control-flow target is outside the code array or not an instruction boundary.
97    InvalidTarget,
98    /// An exception handler range is empty, reversed, or not instruction-aligned.
99    InvalidHandler,
100    /// An instruction cannot be represented by its manifest-declared operand width.
101    WidthOverflow,
102    /// An instruction's operands do not match its manifest-declared layout.
103    InvalidOperands,
104    /// Generated manifest metadata is internally inconsistent.
105    Manifest,
106}
107
108/// A byte-located instruction decoding error.
109#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct InstructionError {
111    /// Stable failure category.
112    pub kind: InstructionErrorKind,
113    /// First byte of the failing instruction.
114    pub offset: u32,
115    /// Human-readable context.
116    pub message: String,
117}
118
119impl fmt::Display for InstructionError {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(f, "{} at code offset {}", self.message, self.offset)
122    }
123}
124
125impl std::error::Error for InstructionError {}
126
127/// Decode JVM instructions using generated metadata plus the three irregular layouts.
128pub fn decode_instructions(
129    code: &[u8],
130    major_version: u16,
131    pool: &ConstantPool,
132) -> Result<DecodedCode, InstructionError> {
133    let mut cursor = 0usize;
134    let mut instructions = Vec::new();
135    let mut offsets = BTreeMap::new();
136    while cursor < code.len() {
137        let start = cursor;
138        let first = read_u1(code, &mut cursor, start)?;
139        let mut opcode = Opcode::from_byte(first);
140        let mut metadata = opcode.metadata();
141        check_metadata(metadata, major_version, start)?;
142        let wide = metadata.operands.starts_with("modified_opcode:");
143        if wide {
144            opcode = Opcode::from_byte(read_u1(code, &mut cursor, start)?);
145            metadata = opcode.metadata();
146            check_metadata(metadata, major_version, start)?;
147            if metadata.operands != "local:u1" && metadata.operands != "local:u1,increment:s1" {
148                return Err(error(
149                    InstructionErrorKind::IllegalWide,
150                    start,
151                    format!("wide cannot modify {}", metadata.mnemonic),
152                ));
153            }
154        }
155        let operands = if opcode == Opcode::Tableswitch {
156            decode_table_switch(code, &mut cursor, start)?
157        } else if opcode == Opcode::Lookupswitch {
158            decode_lookup_switch(code, &mut cursor, start)?
159        } else {
160            if metadata.width == "variable" && !wide {
161                return Err(error(
162                    InstructionErrorKind::VariableLayout,
163                    start,
164                    format!("unsupported variable layout for {}", metadata.mnemonic),
165                ));
166            }
167            let mut operands = Vec::new();
168            for field in metadata.operands.split(',') {
169                match field {
170                    "none" => {}
171                    "value:s1" | "increment:s1" if !wide => operands.push(
172                        InstructionOperand::Immediate(i32::from(
173                            read_u1(code, &mut cursor, start)? as i8,
174                        )),
175                    ),
176                    "increment:s1" => operands.push(InstructionOperand::Immediate(i32::from(
177                        read_u2(code, &mut cursor, start)? as i16,
178                    ))),
179                    "value:s2" | "increment:s2" => operands.push(InstructionOperand::Immediate(
180                        i32::from(read_u2(code, &mut cursor, start)? as i16),
181                    )),
182                    "local:u1" if !wide => operands.push(InstructionOperand::Local(u16::from(
183                        read_u1(code, &mut cursor, start)?,
184                    ))),
185                    "local:u1" => operands.push(InstructionOperand::Local(read_u2(
186                        code,
187                        &mut cursor,
188                        start,
189                    )?)),
190                    "constant_pool:u1" => operands.push(InstructionOperand::Constant(u16::from(
191                        read_u1(code, &mut cursor, start)?,
192                    ))),
193                    "constant_pool:u2" => operands.push(InstructionOperand::Constant(read_u2(
194                        code,
195                        &mut cursor,
196                        start,
197                    )?)),
198                    "branch:s2" => operands.push(InstructionOperand::Branch(i32::from(read_u2(
199                        code,
200                        &mut cursor,
201                        start,
202                    )?
203                        as i16))),
204                    "branch:s4" => {
205                        operands.push(InstructionOperand::Branch(
206                            read_u4(code, &mut cursor, start)? as i32,
207                        ))
208                    }
209                    "count:u1" => operands.push(InstructionOperand::Count(read_u1(
210                        code,
211                        &mut cursor,
212                        start,
213                    )?)),
214                    "dimensions:u1" => operands.push(InstructionOperand::Dimensions(read_u1(
215                        code,
216                        &mut cursor,
217                        start,
218                    )?)),
219                    "atype:u1" => operands.push(InstructionOperand::ArrayType(read_u1(
220                        code,
221                        &mut cursor,
222                        start,
223                    )?)),
224                    "zero:u1" => check_zero(read_u1(code, &mut cursor, start)?, start)?,
225                    "zero:u2" => check_zero(read_u2(code, &mut cursor, start)?, start)?,
226                    other => {
227                        return Err(error(
228                            InstructionErrorKind::Manifest,
229                            start,
230                            format!("unsupported manifest operand {other}"),
231                        ));
232                    }
233                }
234            }
235            operands
236        };
237        if let Some(InstructionOperand::Constant(index)) = operands
238            .iter()
239            .find(|v| matches!(v, InstructionOperand::Constant(_)))
240        {
241            validate_constant(pool, *index, metadata.constant_pool, start)?;
242        }
243        let id = InstructionId(u32::try_from(instructions.len()).map_err(|_| {
244            error(
245                InstructionErrorKind::Manifest,
246                start,
247                "too many instructions",
248            )
249        })?);
250        let offset = u32::try_from(start).map_err(|_| {
251            error(
252                InstructionErrorKind::Manifest,
253                start,
254                "code offset exceeds u32",
255            )
256        })?;
257        offsets.insert(offset, id);
258        instructions.push(LocatedInstruction {
259            id,
260            offset,
261            instruction: Instruction {
262                opcode,
263                wide,
264                operands,
265            },
266        });
267    }
268    let decoded = DecodedCode {
269        instructions,
270        offsets,
271    };
272    validate_branch_targets(&decoded, code.len())?;
273    Ok(decoded)
274}
275
276/// Validate Code-attribute exception ranges against an already decoded code array.
277pub fn validate_exception_handlers(
278    decoded: &DecodedCode,
279    code_length: usize,
280    handlers: &[ExceptionHandlerRange],
281) -> Result<(), InstructionError> {
282    for range in handlers {
283        let start = usize::from(range.start);
284        let end = usize::from(range.end);
285        let handler = usize::from(range.handler);
286        if start >= end {
287            return Err(error(
288                InstructionErrorKind::InvalidHandler,
289                start,
290                format!("exception handler range {start}..{end} is empty or reversed"),
291            ));
292        }
293        require_boundary(decoded, start, code_length, false, "exception range start")?;
294        require_boundary(decoded, end, code_length, true, "exception range end")?;
295        require_boundary(decoded, handler, code_length, false, "exception handler")?;
296    }
297    Ok(())
298}
299
300fn decode_table_switch(
301    code: &[u8],
302    cursor: &mut usize,
303    start: usize,
304) -> Result<Vec<InstructionOperand>, InstructionError> {
305    read_padding(code, cursor, start)?;
306    let default = read_i4(code, cursor, start)?;
307    let low = read_i4(code, cursor, start)?;
308    let high = read_i4(code, cursor, start)?;
309    if high < low {
310        return Err(error(
311            InstructionErrorKind::VariableLayout,
312            start,
313            format!("tableswitch high key {high} precedes low key {low}"),
314        ));
315    }
316    let count = i64::from(high) - i64::from(low) + 1;
317    let count = usize::try_from(count).map_err(|_| {
318        error(
319            InstructionErrorKind::VariableLayout,
320            start,
321            "tableswitch key range overflows addressable input",
322        )
323    })?;
324    ensure_entries_fit(code, *cursor, count, 4, start, "tableswitch")?;
325    let mut operands = Vec::with_capacity(count.saturating_add(3));
326    operands.push(InstructionOperand::Branch(default));
327    operands.push(InstructionOperand::TableLow(low));
328    operands.push(InstructionOperand::TableHigh(high));
329    for _ in 0..count {
330        operands.push(InstructionOperand::Branch(read_i4(code, cursor, start)?));
331    }
332    Ok(operands)
333}
334
335fn decode_lookup_switch(
336    code: &[u8],
337    cursor: &mut usize,
338    start: usize,
339) -> Result<Vec<InstructionOperand>, InstructionError> {
340    read_padding(code, cursor, start)?;
341    let default = read_i4(code, cursor, start)?;
342    let pairs_origin = *cursor;
343    let pair_count = read_i4(code, cursor, start)?;
344    let pair_count = usize::try_from(pair_count).map_err(|_| {
345        error(
346            InstructionErrorKind::VariableLayout,
347            pairs_origin,
348            format!("lookupswitch pair count {pair_count} is negative"),
349        )
350    })?;
351    ensure_entries_fit(code, *cursor, pair_count, 8, start, "lookupswitch")?;
352    let mut operands = Vec::with_capacity(pair_count.saturating_mul(2).saturating_add(1));
353    operands.push(InstructionOperand::Branch(default));
354    let mut previous = None;
355    for _ in 0..pair_count {
356        let key_origin = *cursor;
357        let key = read_i4(code, cursor, start)?;
358        if let Some(prior) = previous
359            && key <= prior
360        {
361            let relation = if key == prior {
362                "duplicate"
363            } else {
364                "out-of-order"
365            };
366            return Err(error(
367                InstructionErrorKind::VariableLayout,
368                key_origin,
369                format!("lookupswitch {relation} key {key} follows {prior}"),
370            ));
371        }
372        let displacement = read_i4(code, cursor, start)?;
373        operands.push(InstructionOperand::LookupKey(key));
374        operands.push(InstructionOperand::Branch(displacement));
375        previous = Some(key);
376    }
377    Ok(operands)
378}
379
380fn read_padding(code: &[u8], cursor: &mut usize, start: usize) -> Result<(), InstructionError> {
381    let padding = (4 - (*cursor % 4)) % 4;
382    for _ in 0..padding {
383        let origin = *cursor;
384        if read_u1(code, cursor, start)? != 0 {
385            return Err(error(
386                InstructionErrorKind::ReservedByte,
387                origin,
388                "switch alignment padding must be zero",
389            ));
390        }
391    }
392    Ok(())
393}
394
395fn ensure_entries_fit(
396    code: &[u8],
397    cursor: usize,
398    count: usize,
399    width: usize,
400    start: usize,
401    layout: &str,
402) -> Result<(), InstructionError> {
403    let bytes = count.checked_mul(width).ok_or_else(|| {
404        error(
405            InstructionErrorKind::VariableLayout,
406            start,
407            format!("{layout} entry byte count overflows"),
408        )
409    })?;
410    let end = cursor.checked_add(bytes).ok_or_else(|| {
411        error(
412            InstructionErrorKind::VariableLayout,
413            start,
414            format!("{layout} end offset overflows"),
415        )
416    })?;
417    if end > code.len() {
418        return Err(error(
419            InstructionErrorKind::Truncated,
420            start,
421            format!("truncated {layout}"),
422        ));
423    }
424    Ok(())
425}
426
427fn validate_branch_targets(
428    decoded: &DecodedCode,
429    code_length: usize,
430) -> Result<(), InstructionError> {
431    for located in &decoded.instructions {
432        for operand in &located.instruction.operands {
433            if let InstructionOperand::Branch(displacement) = operand {
434                let target = i64::from(located.offset) + i64::from(*displacement);
435                let target = usize::try_from(target).map_err(|_| {
436                    error(
437                        InstructionErrorKind::InvalidTarget,
438                        located.offset as usize,
439                        format!("branch target {target} is outside the code array"),
440                    )
441                })?;
442                require_boundary(decoded, target, code_length, false, "branch target")?;
443            }
444        }
445    }
446    Ok(())
447}
448
449fn require_boundary(
450    decoded: &DecodedCode,
451    offset: usize,
452    code_length: usize,
453    allow_end: bool,
454    subject: &str,
455) -> Result<(), InstructionError> {
456    let offset_u32 = u32::try_from(offset).map_err(|_| {
457        error(
458            InstructionErrorKind::InvalidTarget,
459            offset,
460            format!("{subject} {offset} exceeds the classfile offset range"),
461        )
462    })?;
463    if (allow_end && offset == code_length) || decoded.offsets.contains_key(&offset_u32) {
464        return Ok(());
465    }
466    if offset >= code_length {
467        return Err(error(
468            if subject.starts_with("exception") {
469                InstructionErrorKind::InvalidHandler
470            } else {
471                InstructionErrorKind::InvalidTarget
472            },
473            offset,
474            format!("{subject} {offset} is outside the code array"),
475        ));
476    }
477    Err(error(
478        if subject.starts_with("exception") {
479            InstructionErrorKind::InvalidHandler
480        } else {
481            InstructionErrorKind::InvalidTarget
482        },
483        offset,
484        format!("{subject} {offset} is not an instruction boundary"),
485    ))
486}
487
488pub(crate) fn check_metadata(
489    metadata: &crate::OpcodeMetadata,
490    major: u16,
491    offset: usize,
492) -> Result<(), InstructionError> {
493    if metadata.width == "invalid" || metadata.operands == "invalid" {
494        return Err(error(
495            InstructionErrorKind::InvalidOpcode,
496            offset,
497            format!("opcode {} is reserved", metadata.mnemonic),
498        ));
499    }
500    let since = metadata
501        .since
502        .split('.')
503        .next()
504        .and_then(|v| v.parse::<u16>().ok())
505        .ok_or_else(|| {
506            error(
507                InstructionErrorKind::Manifest,
508                offset,
509                "invalid since version",
510            )
511        })?;
512    if major < since {
513        return Err(error(
514            InstructionErrorKind::Version,
515            offset,
516            format!(
517                "opcode {} requires classfile version {}, found {}",
518                metadata.mnemonic, metadata.since, major
519            ),
520        ));
521    }
522    if metadata.until != "unbounded" {
523        let until = metadata
524            .until
525            .split('.')
526            .next()
527            .and_then(|v| v.parse::<u16>().ok())
528            .ok_or_else(|| {
529                error(
530                    InstructionErrorKind::Manifest,
531                    offset,
532                    "invalid until version",
533                )
534            })?;
535        if major > until {
536            return Err(error(
537                InstructionErrorKind::Version,
538                offset,
539                format!(
540                    "opcode {} ended with classfile version {}, found {}",
541                    metadata.mnemonic, metadata.until, major
542                ),
543            ));
544        }
545    }
546    Ok(())
547}
548
549fn validate_constant(
550    pool: &ConstantPool,
551    index: u16,
552    category: &str,
553    offset: usize,
554) -> Result<(), InstructionError> {
555    let value = pool.entry(index, index).map_err(|cause| {
556        error(
557            InstructionErrorKind::ConstantPool,
558            offset,
559            cause.to_string(),
560        )
561    })?;
562    let valid = match category {
563        "Fieldref" => matches!(value, Constant::Fieldref { .. }),
564        "Methodref" => matches!(value, Constant::Methodref { .. }),
565        "InterfaceMethodref" => matches!(value, Constant::InterfaceMethodref { .. }),
566        "Methodref|InterfaceMethodref" => matches!(
567            value,
568            Constant::Methodref { .. } | Constant::InterfaceMethodref { .. }
569        ),
570        "InvokeDynamic" => matches!(value, Constant::InvokeDynamic { .. }),
571        "Class" => matches!(value, Constant::Class { .. }),
572        "loadable-category-1" => !matches!(value, Constant::Long(_) | Constant::Double(_)),
573        "loadable-category-2" => matches!(value, Constant::Long(_) | Constant::Double(_)),
574        "none" => false,
575        _ => false,
576    };
577    if valid {
578        Ok(())
579    } else {
580        Err(error(
581            InstructionErrorKind::ConstantPool,
582            offset,
583            format!("constant pool index {index} is not {category}"),
584        ))
585    }
586}
587
588fn read_u1(code: &[u8], cursor: &mut usize, start: usize) -> Result<u8, InstructionError> {
589    let value = code.get(*cursor).copied().ok_or_else(|| {
590        error(
591            InstructionErrorKind::Truncated,
592            start,
593            "truncated instruction",
594        )
595    })?;
596    *cursor += 1;
597    Ok(value)
598}
599fn read_u2(code: &[u8], cursor: &mut usize, start: usize) -> Result<u16, InstructionError> {
600    Ok(u16::from_be_bytes([
601        read_u1(code, cursor, start)?,
602        read_u1(code, cursor, start)?,
603    ]))
604}
605fn read_u4(code: &[u8], cursor: &mut usize, start: usize) -> Result<u32, InstructionError> {
606    Ok(u32::from_be_bytes([
607        read_u1(code, cursor, start)?,
608        read_u1(code, cursor, start)?,
609        read_u1(code, cursor, start)?,
610        read_u1(code, cursor, start)?,
611    ]))
612}
613fn read_i4(code: &[u8], cursor: &mut usize, start: usize) -> Result<i32, InstructionError> {
614    Ok(read_u4(code, cursor, start)? as i32)
615}
616fn check_zero<T: Default + PartialEq>(value: T, start: usize) -> Result<(), InstructionError> {
617    if value == T::default() {
618        Ok(())
619    } else {
620        Err(error(
621            InstructionErrorKind::ReservedByte,
622            start,
623            "reserved operand bytes must be zero",
624        ))
625    }
626}
627pub(crate) fn error(
628    kind: InstructionErrorKind,
629    offset: usize,
630    message: impl Into<String>,
631) -> InstructionError {
632    InstructionError {
633        kind,
634        offset: u32::try_from(offset).unwrap_or(u32::MAX),
635        message: message.into(),
636    }
637}