1use 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
20pub 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
71pub 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
244pub struct ClassfileCodecLib {
246 symbol: Symbol,
247 codec_id: CodecId,
248}
249impl ClassfileCodecLib {
250 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}