Skip to main content

sim_codec_classfile/shell/
model.rs

1/// Limits for allocations made while structurally decoding one classfile shell.
2#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3pub struct ShellBudget {
4    /// Maximum implemented interfaces.
5    pub interfaces: usize,
6    /// Maximum field declarations.
7    pub fields: usize,
8    /// Maximum method declarations.
9    pub methods: usize,
10    /// Maximum attributes across the class and all members.
11    pub attributes: usize,
12    /// Maximum aggregate bytes retained in attribute bodies.
13    pub attribute_bytes: usize,
14}
15/// An uninterpreted attribute spine entry, retained in its original order.
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct AttributeShell {
18    /// Raw constant-pool index of the attribute name.
19    pub name_index: u16,
20    /// Declared attribute body length.
21    pub declared_length: u32,
22    /// Exact uninterpreted attribute body.
23    pub bytes: Vec<u8>,
24    /// Source span covering the complete attribute, including its header.
25    pub origin: Origin,
26    /// Owner and ordinal captured at decode time.
27    pub location: AttributeLocation,
28}
29
30/// The legal classfile owner of an attribute shell.
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum AttributeOwner {
33    /// The class declaration.
34    Class,
35    /// A field declaration at the given classfile ordinal.
36    Field(usize),
37    /// A method declaration at the given classfile ordinal.
38    Method(usize),
39}
40
41/// Stable owner and order evidence for a retained attribute.
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub struct AttributeLocation {
44    /// Attribute owner.
45    pub owner: AttributeOwner,
46    /// Zero-based position within that owner's attribute table.
47    pub order: usize,
48}
49
50/// Checked evidence invalidated by a classfile edit.
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct LayoutInvalidation {
53    /// Declaration path whose original bytes are no longer evidence for current content/layout.
54    pub path: String,
55    /// Whether byte positions after this path moved.
56    pub shifts_following_layout: bool,
57}
58
59/// Result of one checked method-body edit.
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct EditReport {
62    /// Exact layout evidence invalidated by the edit.
63    pub invalidated: Vec<LayoutInvalidation>,
64}
65
66/// A raw field declaration whose indices have not been validated.
67#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct FieldShell {
69    /// Raw JVM field access flags.
70    pub access_flags: u16,
71    /// Raw constant-pool index of the field name.
72    pub name_index: u16,
73    /// Raw constant-pool index of the field descriptor.
74    pub descriptor_index: u16,
75    /// Ordered attribute spine.
76    pub attributes: Vec<AttributeShell>,
77    /// Source span covering the declaration.
78    pub origin: Origin,
79}
80
81/// A raw method declaration whose indices have not been validated.
82#[derive(Clone, Debug, Eq, PartialEq)]
83pub struct MethodShell {
84    /// Raw JVM method access flags.
85    pub access_flags: u16,
86    /// Raw constant-pool index of the method name.
87    pub name_index: u16,
88    /// Raw constant-pool index of the method descriptor.
89    pub descriptor_index: u16,
90    /// Ordered attribute spine.
91    pub attributes: Vec<AttributeShell>,
92    /// Source span covering the declaration.
93    pub origin: Origin,
94}
95
96/// The bounded structural classfile read, before any shell index is checked.
97#[derive(Clone, Debug, PartialEq)]
98pub struct ClassShell {
99    /// Classfile minor version.
100    pub minor_version: u16,
101    /// Classfile major version.
102    pub major_version: u16,
103    /// Structurally decoded constant pool.
104    pub constant_pool: ConstantPool,
105    /// Raw JVM class access flags.
106    pub access_flags: u16,
107    /// Raw constant-pool index of this class.
108    pub this_class: u16,
109    /// Raw constant-pool index of the superclass, or zero for `java/lang/Object`.
110    pub super_class: u16,
111    /// Raw constant-pool indices of directly implemented interfaces.
112    pub interfaces: Vec<u16>,
113    /// Field declarations in classfile order.
114    pub fields: Vec<FieldShell>,
115    /// Method declarations in classfile order.
116    pub methods: Vec<MethodShell>,
117    /// Class attributes in classfile order.
118    pub attributes: Vec<AttributeShell>,
119    /// Source span covering the whole shell.
120    pub origin: Origin,
121}
122
123/// A constant-pool index proven to name a `Class` entry.
124#[derive(Clone, Copy, Debug, Eq, PartialEq)]
125pub struct ClassIndex(pub u16);
126
127/// A constant-pool index proven to name a `Utf8` entry.
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub struct Utf8Index(pub u16);
130
131/// A field projection whose name, descriptor, and attribute names are typed.
132#[derive(Clone, Debug, Eq, PartialEq)]
133pub struct ValidatedFieldShell {
134    /// Validated field-name index.
135    pub name: Utf8Index,
136    /// Validated field-descriptor index.
137    pub descriptor: Utf8Index,
138    /// Validated attribute-name indices in original order.
139    pub attribute_names: Vec<Utf8Index>,
140}
141
142/// A method projection whose name, descriptor, and attribute names are typed.
143#[derive(Clone, Debug, Eq, PartialEq)]
144pub struct ValidatedMethodShell {
145    /// Validated method-name index.
146    pub name: Utf8Index,
147    /// Validated method-descriptor index.
148    pub descriptor: Utf8Index,
149    /// Validated attribute-name indices in original order.
150    pub attribute_names: Vec<Utf8Index>,
151}
152
153/// Typed shell references produced only after structural decoding succeeds.
154#[derive(Clone, Debug, Eq, PartialEq)]
155pub struct ValidatedClassShell {
156    /// Validated index of this class.
157    pub this_class: ClassIndex,
158    /// Validated superclass index, absent only when the raw index is zero.
159    pub super_class: Option<ClassIndex>,
160    /// Validated interface indices in classfile order.
161    pub interfaces: Vec<ClassIndex>,
162    /// Validated field projections.
163    pub fields: Vec<ValidatedFieldShell>,
164    /// Validated method projections.
165    pub methods: Vec<ValidatedMethodShell>,
166    /// Validated class-attribute name indices in classfile order.
167    pub attribute_names: Vec<Utf8Index>,
168}
169
170/// Stable failure category for shell decoding and validation.
171#[derive(Clone, Copy, Debug, Eq, PartialEq)]
172pub enum ShellErrorKind {
173    /// The classfile magic was not `CAFEBABE`.
174    Magic,
175    /// The bounded byte lane failed.
176    Bytes,
177    /// Constant-pool decoding or validation failed.
178    ConstantPool,
179    /// A collection or retained attribute body exceeded its shell budget.
180    Budget,
181    /// A raw index was zero, outside the pool, unusable, or of the wrong category.
182    InvalidIndex,
183    /// Bytes remained after the complete class shell.
184    TrailingBytes,
185    /// A requested checked edit cannot be applied to this shell.
186    Edit,
187}
188
189/// A located shell failure, optionally naming the offending raw index.
190#[derive(Clone, Debug, Eq, PartialEq)]
191pub struct ShellError {
192    /// Stable machine-matchable category.
193    pub kind: ShellErrorKind,
194    /// Absolute byte offset associated with the failure.
195    pub offset: usize,
196    /// Offending raw constant-pool index when applicable.
197    pub index: Option<u16>,
198    /// Declaration path associated with the failure.
199    pub path: String,
200    /// Human-readable detail.
201    pub message: String,
202}
203
204impl fmt::Display for ShellError {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        write!(
207            f,
208            "{} at {} (byte {})",
209            self.message, self.path, self.offset
210        )
211    }
212}
213
214impl std::error::Error for ShellError {}
215
216impl ClassShell {
217    /// Structurally decode a complete classfile without validating shell indices.
218    pub fn decode(
219        bytes: &[u8],
220        allocation_budget: usize,
221        budget: ShellBudget,
222        codec: CodecId,
223        source: SourceId,
224    ) -> Result<Self, ShellError> {
225        let mut reader = ByteReader::new(bytes, allocation_budget);
226        if reader.read_u4().map_err(|e| byte_error("magic", e))? != 0xcafe_babe {
227            return Err(error(
228                ShellErrorKind::Magic,
229                0,
230                None,
231                "magic",
232                "invalid classfile magic",
233            ));
234        }
235        let minor_version = reader
236            .read_u2()
237            .map_err(|e| byte_error("minor_version", e))?;
238        let major_version = reader
239            .read_u2()
240            .map_err(|e| byte_error("major_version", e))?;
241        let constant_pool = ConstantPool::decode(&mut reader, major_version).map_err(pool_error)?;
242        let access_flags = reader
243            .read_u2()
244            .map_err(|e| byte_error("access_flags", e))?;
245        let this_class = reader.read_u2().map_err(|e| byte_error("this_class", e))?;
246        let super_class = reader.read_u2().map_err(|e| byte_error("super_class", e))?;
247        let mut state = DecodeState {
248            budget,
249            attributes: 0,
250            attribute_bytes: 0,
251            codec,
252            source,
253        };
254        let interfaces = read_indices(&mut reader, budget.interfaces, "interfaces")?;
255        let fields = read_members(
256            &mut reader,
257            budget.fields,
258            "fields",
259            AttributeOwner::Field,
260            &mut state,
261        )?
262        .into_iter()
263        .map(Member::into_field)
264        .collect();
265        let methods = read_members(
266            &mut reader,
267            budget.methods,
268            "methods",
269            AttributeOwner::Method,
270            &mut state,
271        )?
272        .into_iter()
273        .map(Member::into_method)
274        .collect();
275        let attributes =
276            read_attributes(&mut reader, "attributes", AttributeOwner::Class, &mut state)?;
277        if reader.remaining() != 0 {
278            return Err(error(
279                ShellErrorKind::TrailingBytes,
280                reader.offset(),
281                None,
282                "class",
283                "trailing bytes after class shell",
284            ));
285        }
286        Ok(Self {
287            minor_version,
288            major_version,
289            constant_pool,
290            access_flags,
291            this_class,
292            super_class,
293            interfaces,
294            fields,
295            methods,
296            attributes,
297            origin: origin(codec, state.source, 0, reader.offset()),
298        })
299    }
300
301    /// Validate every raw shell index and return a separate typed projection.
302    pub fn validate(&self) -> Result<ValidatedClassShell, ShellError> {
303        let this_class = self.class_index(self.this_class, "this_class", &self.origin)?;
304        let super_class = if self.super_class == 0 {
305            None
306        } else {
307            Some(self.class_index(self.super_class, "super_class", &self.origin)?)
308        };
309        let interfaces = self
310            .interfaces
311            .iter()
312            .enumerate()
313            .map(|(position, &index)| {
314                self.class_index(index, &format!("interfaces[{position}]"), &self.origin)
315            })
316            .collect::<Result<_, _>>()?;
317        let fields = self
318            .fields
319            .iter()
320            .enumerate()
321            .map(|(position, member)| {
322                Ok(ValidatedFieldShell {
323                    name: self.utf8_index(
324                        member.name_index,
325                        &format!("fields[{position}].name_index"),
326                        &member.origin,
327                    )?,
328                    descriptor: self.utf8_index(
329                        member.descriptor_index,
330                        &format!("fields[{position}].descriptor_index"),
331                        &member.origin,
332                    )?,
333                    attribute_names: self
334                        .validate_attributes(&member.attributes, &format!("fields[{position}]"))?,
335                })
336            })
337            .collect::<Result<_, ShellError>>()?;
338        let methods = self
339            .methods
340            .iter()
341            .enumerate()
342            .map(|(position, member)| {
343                Ok(ValidatedMethodShell {
344                    name: self.utf8_index(
345                        member.name_index,
346                        &format!("methods[{position}].name_index"),
347                        &member.origin,
348                    )?,
349                    descriptor: self.utf8_index(
350                        member.descriptor_index,
351                        &format!("methods[{position}].descriptor_index"),
352                        &member.origin,
353                    )?,
354                    attribute_names: self
355                        .validate_attributes(&member.attributes, &format!("methods[{position}]"))?,
356                })
357            })
358            .collect::<Result<_, ShellError>>()?;
359        Ok(ValidatedClassShell {
360            this_class,
361            super_class,
362            interfaces,
363            fields,
364            methods,
365            attribute_names: self.validate_attributes(&self.attributes, "class")?,
366        })
367    }
368
369    /// Encode the complete shell, retaining all attribute bytes and table order exactly.
370    pub fn encode(&self, allocation_budget: usize) -> Result<Vec<u8>, ShellError> {
371        self.validate()?;
372        let mut out = ByteWriter::new(allocation_budget);
373        out.write_u4(0xcafe_babe)
374            .map_err(|e| byte_error("magic", e))?;
375        out.write_u2(self.minor_version)
376            .map_err(|e| byte_error("minor_version", e))?;
377        out.write_u2(self.major_version)
378            .map_err(|e| byte_error("major_version", e))?;
379        self.constant_pool
380            .encode(&mut out, self.major_version)
381            .map_err(pool_error)?;
382        out.write_u2(self.access_flags)
383            .map_err(|e| byte_error("access_flags", e))?;
384        out.write_u2(self.this_class)
385            .map_err(|e| byte_error("this_class", e))?;
386        out.write_u2(self.super_class)
387            .map_err(|e| byte_error("super_class", e))?;
388        write_indices(&mut out, &self.interfaces, "interfaces")?;
389        write_members(&mut out, &self.fields, "fields")?;
390        write_members(&mut out, &self.methods, "methods")?;
391        write_attributes(&mut out, &self.attributes, "class")?;
392        Ok(out.into_bytes())
393    }
394
395    /// Replace one method's `Code` byte array without disturbing unrelated raw attributes.
396    ///
397    /// The selected payload is decoded and re-encoded structurally, so malformed code metadata or
398    /// an edit that invalidates exception ranges is rejected. The report precisely distinguishes a
399    /// same-size content edit from an edit that shifts following byte positions.
400    pub fn replace_method_code(
401        &mut self,
402        method_index: usize,
403        code: Vec<u8>,
404        allocation_budget: usize,
405    ) -> Result<EditReport, ShellError> {
406        let code_name = self.constant_pool.slots().iter().position(|slot| {
407            matches!(slot, crate::ConstantSlot::Entry(Constant::Utf8(value)) if value.as_code_units() == ['C' as u16, 'o' as u16, 'd' as u16, 'e' as u16])
408        }).ok_or_else(|| edit_error("constant_pool", "constant pool does not contain Code"))? as u16;
409        let method = self.methods.get_mut(method_index).ok_or_else(|| {
410            edit_error(
411                format!("methods[{method_index}]"),
412                "method index is out of range",
413            )
414        })?;
415        let (attribute_index, attribute) = method
416            .attributes
417            .iter_mut()
418            .enumerate()
419            .find(|(_, attribute)| attribute.name_index == code_name)
420            .ok_or_else(|| {
421                edit_error(
422                    format!("methods[{method_index}]"),
423                    "method has no Code attribute",
424                )
425            })?;
426        let old_len = attribute.bytes.len();
427        let mut structured =
428            CodeAttribute::decode(&mut ByteReader::new(&attribute.bytes, allocation_budget))
429                .map_err(|cause| {
430                    edit_error(
431                        format!("methods[{method_index}].attributes[{attribute_index}]"),
432                        cause.to_string(),
433                    )
434                })?;
435        structured.code = code;
436        let bytes = structured.encode(allocation_budget).map_err(|cause| {
437            edit_error(
438                format!("methods[{method_index}].attributes[{attribute_index}]"),
439                cause.to_string(),
440            )
441        })?;
442        attribute.declared_length = u32::try_from(bytes.len())
443            .map_err(|_| edit_error("Code", "encoded Code attribute exceeds u32"))?;
444        attribute.bytes = bytes;
445        Ok(EditReport {
446            invalidated: vec![LayoutInvalidation {
447                path: format!("methods[{method_index}].attributes[{attribute_index}].bytes"),
448                shifts_following_layout: old_len != attribute.bytes.len(),
449            }],
450        })
451    }
452
453    fn class_index(&self, index: u16, path: &str, at: &Origin) -> Result<ClassIndex, ShellError> {
454        self.expect(index, path, at, |entry| {
455            matches!(entry, Constant::Class { .. })
456        })?;
457        Ok(ClassIndex(index))
458    }
459
460    fn utf8_index(&self, index: u16, path: &str, at: &Origin) -> Result<Utf8Index, ShellError> {
461        self.expect(index, path, at, |entry| matches!(entry, Constant::Utf8(_)))?;
462        Ok(Utf8Index(index))
463    }
464
465    fn expect(
466        &self,
467        index: u16,
468        path: &str,
469        at: &Origin,
470        predicate: impl FnOnce(&Constant) -> bool,
471    ) -> Result<(), ShellError> {
472        let entry = self.constant_pool.entry(index, index).map_err(|cause| {
473            error(
474                ShellErrorKind::InvalidIndex,
475                at.span.start,
476                Some(index),
477                path,
478                format!("invalid constant-pool index {index}: {cause}"),
479            )
480        })?;
481        if !predicate(entry) {
482            return Err(error(
483                ShellErrorKind::InvalidIndex,
484                at.span.start,
485                Some(index),
486                path,
487                format!("constant-pool index {index} has the wrong category"),
488            ));
489        }
490        Ok(())
491    }
492
493    fn validate_attributes(
494        &self,
495        attributes: &[AttributeShell],
496        owner: &str,
497    ) -> Result<Vec<Utf8Index>, ShellError> {
498        attributes
499            .iter()
500            .enumerate()
501            .map(|(position, attribute)| {
502                self.utf8_index(
503                    attribute.name_index,
504                    &format!("{owner}.attributes[{position}].name_index"),
505                    &attribute.origin,
506                )
507            })
508            .collect()
509    }
510}