Skip to main content

sim_codec_classfile/
runtime.rs

1//! Runtime registration and bounded inspection projection.
2
3use std::sync::Arc;
4
5use sim_codec::{Decoder, DomainCodecLib, Encoder, Input, Output};
6use sim_kernel::{
7    CodecId, Error, Expr, Lib, LibManifest, Linker, NumberLiteral, Result, SourceId, Symbol,
8};
9use sim_shape::{
10    ExprKind, ExprKindShape, TableExtraPolicy, TableFieldSpec, TableShape, shape_value,
11};
12
13use crate::{
14    AttributeShell, ByteReader, ClassShell, CodeAttribute, ConstantSlot, ShellBudget,
15    decode_instructions,
16};
17
18const CLASSFILE_TAG: &str = "Classfile";
19
20/// Binary JVM classfile decoder/encoder exposed as `codec/classfile`.
21pub struct ClassfileCodec;
22
23impl Decoder for ClassfileCodec {
24    fn decode(&self, cx: &mut sim_codec::ReadCx<'_>, input: Input) -> Result<Expr> {
25        let bytes = match input {
26            Input::Bytes(bytes) => bytes,
27            Input::Text(_) => return Err(codec_error(cx.codec, "classfile input must be bytes")),
28        };
29        cx.limits
30            .max_input_bytes
31            .checked_sub(bytes.len())
32            .ok_or_else(|| {
33                codec_error(
34                    cx.codec,
35                    "classfile exceeds the configured input-byte bound",
36                )
37            })?;
38        inspect_classfile(cx.codec, bytes, cx.limits.max_collection_len)
39    }
40}
41
42impl Encoder for ClassfileCodec {
43    fn encode(&self, cx: &mut sim_kernel::WriteCx<'_>, expr: &Expr) -> Result<Output> {
44        let Expr::Extension { tag, payload } = expr else {
45            return Err(codec_error(
46                cx.codec,
47                "expected a retained Classfile projection",
48            ));
49        };
50        if tag != &Symbol::qualified("classfile", CLASSFILE_TAG) {
51            return Err(codec_error(
52                cx.codec,
53                "expected a retained Classfile projection",
54            ));
55        }
56        let Expr::Map(entries) = payload.as_ref() else {
57            return Err(codec_error(cx.codec, "malformed Classfile projection"));
58        };
59        entries
60            .iter()
61            .find_map(|(key, value)| match (key, value) {
62                (Expr::Symbol(key), Expr::Bytes(bytes)) if key == &Symbol::new("bytes") => {
63                    Some(Output::Bytes(bytes.clone()))
64                }
65                _ => None,
66            })
67            .ok_or_else(|| codec_error(cx.codec, "Classfile projection has no retained bytes"))
68    }
69}
70
71/// Decode retained classfile bytes into bounded Table/Dir-compatible data.
72///
73/// Every instruction row carries both its method-local `code-offset` and its
74/// absolute `byte-offset`, allowing a browse result to navigate back to the
75/// retained byte string without consulting a JVM.
76pub fn inspect_classfile(codec: CodecId, bytes: Vec<u8>, bound: usize) -> Result<Expr> {
77    let cap = bound.min(65_536);
78    let budget = ShellBudget {
79        interfaces: cap,
80        fields: cap,
81        methods: cap,
82        attributes: cap,
83        attribute_bytes: bytes.len(),
84    };
85    let shell = ClassShell::decode(
86        &bytes,
87        bytes.len().saturating_mul(4).max(1024),
88        budget,
89        codec,
90        SourceId("classfile".into()),
91    )
92    .map_err(|error| codec_error(codec, error.to_string()))?;
93    shell
94        .validate()
95        .map_err(|error| codec_error(codec, error.to_string()))?;
96
97    let constants = shell
98        .constant_pool
99        .slots()
100        .iter()
101        .enumerate()
102        .take(cap)
103        .map(|(index, slot)| {
104            map([
105                ("index", number(index)),
106                ("kind", string(constant_kind(slot))),
107            ])
108        })
109        .collect();
110    let mut instructions = Vec::new();
111    let mut attributes = Vec::new();
112    for (method_index, method) in shell.methods.iter().enumerate() {
113        for attribute in &method.attributes {
114            attributes.push(attribute_row("method", method_index, attribute));
115            if is_utf8(&shell, attribute.name_index, "Code") {
116                let code = CodeAttribute::decode(&mut ByteReader::new(
117                    &attribute.bytes,
118                    bytes.len().max(1024),
119                ))
120                .map_err(|error| codec_error(codec, error.to_string()))?;
121                let decoded =
122                    decode_instructions(&code.code, shell.major_version, &shell.constant_pool)
123                        .map_err(|error| codec_error(codec, error.to_string()))?;
124                let code_start = attribute.origin.span.start.saturating_add(14);
125                for located in decoded
126                    .instructions
127                    .into_iter()
128                    .take(cap.saturating_sub(instructions.len()))
129                {
130                    instructions.push(map([
131                        ("method", number(method_index)),
132                        ("code-offset", number(located.offset)),
133                        (
134                            "byte-offset",
135                            number(code_start.saturating_add(located.offset as usize)),
136                        ),
137                        (
138                            "opcode",
139                            string(located.instruction.opcode.metadata().mnemonic),
140                        ),
141                        (
142                            "operands",
143                            string(format!("{:?}", located.instruction.operands)),
144                        ),
145                    ]));
146                }
147            }
148        }
149    }
150    for (index, attribute) in shell.attributes.iter().enumerate().take(cap) {
151        attributes.push(attribute_row("class", index, attribute));
152    }
153    Ok(Expr::Extension {
154        tag: Symbol::qualified("classfile", CLASSFILE_TAG),
155        payload: Box::new(map([
156            ("bytes", Expr::Bytes(bytes)),
157            ("major-version", number(shell.major_version)),
158            ("minor-version", number(shell.minor_version)),
159            ("constants", Expr::Vector(constants)),
160            (
161                "attributes",
162                Expr::Vector(attributes.into_iter().take(cap).collect()),
163            ),
164            ("instructions", Expr::Vector(instructions)),
165        ])),
166    })
167}
168
169fn attribute_row(owner: &str, index: usize, attribute: &AttributeShell) -> Expr {
170    map([
171        ("owner", string(owner)),
172        ("index", number(index)),
173        ("name-index", number(attribute.name_index)),
174        ("byte-offset", number(attribute.origin.span.start)),
175        (
176            "byte-length",
177            number(
178                attribute
179                    .origin
180                    .span
181                    .end
182                    .saturating_sub(attribute.origin.span.start),
183            ),
184        ),
185    ])
186}
187
188fn is_utf8(shell: &ClassShell, index: u16, expected: &str) -> bool {
189    shell.constant_pool.entry(index, index).is_ok_and(|constant| {
190        matches!(constant, crate::Constant::Utf8(value) if value.as_code_units().iter().copied().eq(expected.encode_utf16()))
191    })
192}
193
194fn constant_kind(slot: &ConstantSlot) -> &'static str {
195    match slot {
196        ConstantSlot::Reserved => "reserved",
197        ConstantSlot::Unusable => "unusable",
198        ConstantSlot::Entry(value) => match value {
199            crate::Constant::Utf8(_) => "utf8",
200            crate::Constant::Integer(_) => "integer",
201            crate::Constant::Float(_) => "float",
202            crate::Constant::Long(_) => "long",
203            crate::Constant::Double(_) => "double",
204            crate::Constant::Class { .. } => "class",
205            crate::Constant::String { .. } => "string",
206            crate::Constant::Fieldref { .. } => "fieldref",
207            crate::Constant::Methodref { .. } => "methodref",
208            crate::Constant::InterfaceMethodref { .. } => "interface-methodref",
209            crate::Constant::NameAndType { .. } => "name-and-type",
210            crate::Constant::MethodHandle { .. } => "method-handle",
211            crate::Constant::MethodType { .. } => "method-type",
212            crate::Constant::Dynamic { .. } => "dynamic",
213            crate::Constant::InvokeDynamic { .. } => "invoke-dynamic",
214            crate::Constant::Module { .. } => "module",
215            crate::Constant::Package { .. } => "package",
216        },
217    }
218}
219
220fn map<const N: usize>(entries: [(&str, Expr); N]) -> Expr {
221    Expr::Map(
222        entries
223            .into_iter()
224            .map(|(key, value)| (Expr::Symbol(Symbol::new(key)), value))
225            .collect(),
226    )
227}
228fn string(value: impl Into<String>) -> Expr {
229    Expr::String(value.into())
230}
231fn number(value: impl ToString) -> Expr {
232    Expr::Number(NumberLiteral {
233        domain: Symbol::qualified("numbers", "u64"),
234        canonical: value.to_string(),
235    })
236}
237fn codec_error(codec: CodecId, message: impl Into<String>) -> Error {
238    Error::CodecError {
239        codec,
240        message: message.into(),
241    }
242}
243
244/// Host-registered library that installs the codec object and its browse Shapes.
245pub struct ClassfileCodecLib {
246    symbol: Symbol,
247    codec_id: CodecId,
248}
249impl ClassfileCodecLib {
250    /// Create a classfile library bound to a runtime-assigned codec id.
251    pub fn new(codec_id: CodecId) -> Self {
252        Self {
253            symbol: Symbol::qualified("codec", "classfile"),
254            codec_id,
255        }
256    }
257
258    fn domain_lib(&self) -> DomainCodecLib {
259        let shapes = classfile_shapes()
260            .into_iter()
261            .map(|(symbol, shape)| (symbol.clone(), shape_value(symbol, shape)))
262            .collect();
263        DomainCodecLib::new(
264            self.symbol.clone(),
265            self.codec_id,
266            Arc::new(ClassfileCodec),
267            Arc::new(ClassfileCodec),
268            Symbol::qualified("codec", "Classfile"),
269        )
270        .with_shapes(shapes)
271    }
272}
273impl Lib for ClassfileCodecLib {
274    fn manifest(&self) -> LibManifest {
275        self.domain_lib().manifest()
276    }
277    fn load(&self, cx: &mut sim_kernel::LoadCx, linker: &mut Linker) -> Result<()> {
278        self.domain_lib().load(cx, linker)
279    }
280}
281
282fn classfile_shapes() -> Vec<(Symbol, Arc<dyn sim_kernel::Shape>)> {
283    let number = || Arc::new(ExprKindShape::new(ExprKind::Number)) as Arc<dyn sim_kernel::Shape>;
284    let string = || Arc::new(ExprKindShape::new(ExprKind::String)) as Arc<dyn sim_kernel::Shape>;
285    let row = |fields: Vec<TableFieldSpec>| {
286        Arc::new(TableShape::new(fields, TableExtraPolicy::Reject)) as Arc<dyn sim_kernel::Shape>
287    };
288    let field = |key: &str, shape: Arc<dyn sim_kernel::Shape>| TableFieldSpec {
289        key: Symbol::new(key),
290        shape,
291        required: true,
292    };
293    vec![
294        (
295            Symbol::qualified("codec", "Classfile"),
296            Arc::new(ExprKindShape::new(ExprKind::Extension)),
297        ),
298        (
299            Symbol::qualified("classfile", "ConstantRow"),
300            row(vec![field("index", number()), field("kind", string())]),
301        ),
302        (
303            Symbol::qualified("classfile", "AttributeRow"),
304            row(vec![
305                field("owner", string()),
306                field("index", number()),
307                field("name-index", number()),
308                field("byte-offset", number()),
309                field("byte-length", number()),
310            ]),
311        ),
312        (
313            Symbol::qualified("classfile", "InstructionRow"),
314            row(vec![
315                field("method", number()),
316                field("code-offset", number()),
317                field("byte-offset", number()),
318                field("opcode", string()),
319                field("operands", string()),
320            ]),
321        ),
322    ]
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use sim_codec::{DecodeLimits, Encoder};
329    use sim_kernel::{Cx, DefaultFactory, EagerPolicy, EncodeOptions, WriteCx};
330
331    const POSITIVE: &[u8] = include_bytes!("../fixtures/positive.class");
332
333    #[test]
334    fn runtime_registers_codec_and_inspection_shapes() {
335        let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
336        cx.load_lib(&ClassfileCodecLib::new(CodecId(73))).unwrap();
337        assert!(
338            cx.registry()
339                .codec_by_symbol(&Symbol::qualified("codec", "classfile"))
340                .is_some()
341        );
342        for symbol in [
343            Symbol::qualified("codec", "Classfile"),
344            Symbol::qualified("classfile", "ConstantRow"),
345            Symbol::qualified("classfile", "AttributeRow"),
346            Symbol::qualified("classfile", "InstructionRow"),
347        ] {
348            assert!(cx.registry().shape_by_symbol(&symbol).is_some(), "{symbol}");
349        }
350    }
351
352    #[test]
353    fn inspection_is_bounded_and_instruction_rows_navigate_to_bytes() {
354        let projection = inspect_classfile(CodecId(73), POSITIVE.to_vec(), 4096).unwrap();
355        let Expr::Extension { payload, .. } = &projection else {
356            panic!("not retained")
357        };
358        let Expr::Map(root) = payload.as_ref() else {
359            panic!("not browseable")
360        };
361        let instructions = root
362            .iter()
363            .find_map(|(key, value)| {
364                matches!(key, Expr::Symbol(symbol) if symbol == &Symbol::new("instructions"))
365                    .then_some(value)
366            })
367            .unwrap();
368        let Expr::Vector(rows) = instructions else {
369            panic!("instructions are not a directory")
370        };
371        let Expr::Map(first) = rows.first().expect("fixture has instructions") else {
372            panic!("row is not a table")
373        };
374        let offset = first
375            .iter()
376            .find_map(|(key, value)| match (key, value) {
377                (Expr::Symbol(key), Expr::Number(value)) if key == &Symbol::new("byte-offset") => {
378                    value.canonical.parse::<usize>().ok()
379                }
380                _ => None,
381            })
382            .expect("instruction has an absolute byte offset");
383        assert!(offset < POSITIVE.len());
384
385        let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
386        let codec = ClassfileCodec;
387        let mut write = WriteCx {
388            cx: &mut cx,
389            codec: CodecId(73),
390            options: EncodeOptions::default(),
391        };
392        assert_eq!(
393            codec.encode(&mut write, &projection).unwrap(),
394            Output::Bytes(POSITIVE.to_vec())
395        );
396        assert!(rows.len() <= DecodeLimits::default().max_collection_len);
397    }
398}