Skip to main content

ferro_babe/
lib.rs

1#![warn(missing_docs)]
2//! FerroBabe disassembles individual Java class files without requiring a Java runtime.
3//!
4//! [`Disassembler`] is the entry point. It parses bytes or a [`std::io::Read`] source into a
5//! [`Disassembly`], which exposes either a complete [`Class`] or the header and diagnostics that
6//! were available before decoding stopped. Use [`FerroFormatter`] for FerroBabe's compact text
7//! output, or implement [`Formatter`] to render the structured model yourself.
8
9mod decode;
10mod diagnostic;
11mod error;
12/// Text-formatting APIs for parsed class files.
13pub mod format;
14mod input;
15/// Borrowed views over class-file metadata, instructions, and constant-pool entries.
16pub mod model;
17mod options;
18
19pub use diagnostic::{Diagnostic, DiagnosticSeverity, DiagnosticStage};
20pub use error::FerroBabeError;
21pub use format::{FerroFormatter, Formatter};
22pub use model::{Class, ClassVersion, Disassembly};
23pub use options::{DisassemblerBuilder, DisassemblerOptions, RecoveryMode};
24
25use decode::decode_class;
26use input::ClassHeader;
27use std::io::Read;
28
29#[derive(Debug, Clone, Default)]
30/// Parses Java class files into a structured reverse-engineering model.
31///
32/// The default instance uses [`RecoveryMode::BestEffort`]. It preserves a readable class-file
33/// header and records a [`Diagnostic`] when complete decoding cannot continue.
34pub struct Disassembler {
35    options: DisassemblerOptions,
36}
37
38impl Disassembler {
39    /// Creates a builder for configuring a disassembler.
40    ///
41    /// The builder starts with [`RecoveryMode::BestEffort`].
42    #[must_use]
43    pub fn builder() -> DisassemblerBuilder {
44        DisassemblerBuilder::default()
45    }
46
47    /// Creates a disassembler with the supplied options.
48    #[must_use]
49    pub fn new(options: DisassemblerOptions) -> Self {
50        Self { options }
51    }
52
53    /// Returns the options used by this disassembler.
54    #[must_use]
55    pub fn options(&self) -> &DisassemblerOptions {
56        &self.options
57    }
58
59    /// Parses one complete Java class-file byte sequence.
60    ///
61    /// In best-effort mode, an error after the class-file header is read returns a partial
62    /// [`Disassembly`] with diagnostics. In strict mode, the same decoding failure is returned as
63    /// [`FerroBabeError`]. Invalid, incomplete, or unsupported headers always return an error.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error when the input has no readable supported class-file header, or when
68    /// strict recovery is selected and decoding fails.
69    pub fn parse(&self, bytes: &[u8]) -> Result<Disassembly, FerroBabeError> {
70        let header = ClassHeader::read(bytes)?;
71        match decode_class(bytes, header) {
72            Ok(class) => Ok(Disassembly::complete(class)),
73            Err(error) if self.options.recovery() == RecoveryMode::BestEffort => Ok(
74                Disassembly::partial(header.into(), vec![Diagnostic::from_decode_error(&error)]),
75            ),
76            Err(error) => Err(error),
77        }
78    }
79
80    /// Reads and parses one Java class file from `reader`.
81    ///
82    /// The complete reader is buffered before parsing, so the returned [`Disassembly`] does not
83    /// borrow from `reader`.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`FerroBabeError::InputRead`] when reading fails, or any error documented by
88    /// [`Self::parse`] after the bytes have been read.
89    pub fn parse_reader<R: Read>(&self, mut reader: R) -> Result<Disassembly, FerroBabeError> {
90        let mut bytes = Vec::new();
91        reader
92            .read_to_end(&mut bytes)
93            .map_err(|source| FerroBabeError::InputRead { source })?;
94        self.parse(&bytes)
95    }
96
97    /// Parses `bytes` and renders a complete class with [`FerroFormatter`].
98    ///
99    /// # Errors
100    ///
101    /// Returns parse errors, [`FerroBabeError::IncompleteDisassembly`] for a recovered partial
102    /// result, or a formatting error from the default formatter.
103    pub fn disassemble(&self, bytes: &[u8]) -> Result<String, FerroBabeError> {
104        self.disassemble_with(bytes, &FerroFormatter)
105    }
106
107    /// Parses `bytes` and renders the resulting complete class with `formatter`.
108    ///
109    /// `formatter` receives the same [`Class`] view available through [`Disassembly::class`].
110    ///
111    /// # Errors
112    ///
113    /// Returns parse errors, [`FerroBabeError::IncompleteDisassembly`] for a partial result, or
114    /// [`FerroBabeError::Format`] when `formatter` cannot write its output.
115    pub fn disassemble_with<F: Formatter>(
116        &self,
117        bytes: &[u8],
118        formatter: &F,
119    ) -> Result<String, FerroBabeError> {
120        let disassembly = self.parse(bytes)?;
121        let class = disassembly
122            .class()
123            .ok_or(FerroBabeError::IncompleteDisassembly)?;
124        formatter.format(class)
125    }
126}